diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index ff94a3905e8f..d56601bf620c 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -106,6 +106,8 @@ add_library(yul optimiser/CircularReferencesPruner.h optimiser/CommonSubexpressionEliminator.cpp optimiser/CommonSubexpressionEliminator.h + optimiser/ConditionalBranchFlattener.cpp + optimiser/ConditionalBranchFlattener.h optimiser/ConditionalSimplifier.cpp optimiser/ConditionalSimplifier.h optimiser/ConditionalUnsimplifier.cpp diff --git a/libyul/optimiser/ConditionalBranchFlattener.cpp b/libyul/optimiser/ConditionalBranchFlattener.cpp new file mode 100644 index 000000000000..40e49defe97b --- /dev/null +++ b/libyul/optimiser/ConditionalBranchFlattener.cpp @@ -0,0 +1,154 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include +#include +#include +#include +#include + +#include + +using namespace solidity; +using namespace solidity::yul; +using namespace solidity::util; + +void ConditionalBranchFlattener::run(OptimiserStepContext& _context, Block& _ast) +{ + ConditionalBranchFlattener{_context}(_ast); +} + +void ConditionalBranchFlattener::operator()(Block& _block) +{ + iterateReplacing( + _block.statements, + [&](Statement& _s) -> std::optional> + { + visit(_s); + + if (!std::holds_alternative(_s)) + return {}; + + If& ifStatement = std::get(_s); + std::vector& bodyStatements = ifStatement.body.statements; + + if (bodyStatements.size() != 1) + return {}; + + Statement& bodyStatement = bodyStatements.front(); + if (!std::holds_alternative(bodyStatement)) + return {}; + + Assignment& assignment = std::get(bodyStatement); + if (assignment.variableNames.size() != 1) + return {}; + + SideEffectsCollector sideEffectsCollector(m_context.dialect); + sideEffectsCollector.visit(*assignment.value); + if (!sideEffectsCollector.movable()) + return {}; + + langutil::DebugData::ConstPtr debugData = ifStatement.debugData; + YulName conditionName = m_context.dispenser.newName({YulName("condition")}); + + auto normalizedCondition = std::make_unique( + createBuiltinCall( + debugData, + "iszero", + make_vector( + createBuiltinCall( + debugData, + "iszero", + make_vector(std::move(*ifStatement.condition)) + ) + ) + ) + ); + + std::vector transformed; + transformed.reserve(2); + + transformed.push_back(VariableDeclaration{ + debugData, + {NameWithDebugData{debugData, conditionName}}, + std::move(normalizedCondition) + }); + + YulName targetName = assignment.variableNames[0].name; + std::unique_ptr rhs = std::move(assignment.value); + + Expression mask = createBuiltinCall( + debugData, + "sub", + make_vector( + m_context.dialect.zeroLiteral(), + Identifier{debugData, conditionName} + ) + ); + + Expression diff = createBuiltinCall( + debugData, + "xor", + make_vector( + Identifier{debugData, targetName}, + std::move(*rhs) + ) + ); + + Expression maskedDiff = createBuiltinCall( + debugData, + "and", + make_vector( + std::move(mask), + std::move(diff) + ) + ); + + Expression finalExpr = createBuiltinCall( + debugData, + "xor", + make_vector( + Identifier{debugData, targetName}, + std::move(maskedDiff) + ) + ); + + transformed.push_back(Assignment{ + debugData, + assignment.variableNames, + std::make_unique(std::move(finalExpr)) + }); + + return transformed; + } + ); +} + +FunctionCall ConditionalBranchFlattener::createBuiltinCall( + langutil::DebugData::ConstPtr _debugData, + std::string const& _name, + std::vector _arguments +) +{ + auto handle = m_context.dialect.findBuiltin(_name); + yulAssert(handle.has_value(), "Builtin not found: " + _name); + return FunctionCall{ + std::move(_debugData), + {BuiltinName{nullptr, *handle}}, + std::move(_arguments) + }; +} diff --git a/libyul/optimiser/ConditionalBranchFlattener.h b/libyul/optimiser/ConditionalBranchFlattener.h new file mode 100644 index 000000000000..eb72108600af --- /dev/null +++ b/libyul/optimiser/ConditionalBranchFlattener.h @@ -0,0 +1,69 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3. +#pragma once + +#include +#include +#include + +namespace solidity::yul +{ + +/** + * Replaces conditional execution with branchless bitwise operations. + * + * Rewrites: + * if c { x := a } + * to: + * let condition := iszero(iszero(c)) + * x := xor(x, and(sub(0, condition), xor(a, x))) + * + * This transformation is applied if: + * - The body of the `if` statement contains a single assignment. + * - All RHS expressions in the assignment are movable. + * - There are no control flow statements in the body. + * + * The logic `xor(x, and(sub(0, condition), xor(a, x)))` effectively implements + * `condition ? a : x` using bitwise operations, relying on `condition` being 0 or 1. + * `sub(0, condition)` generates a mask of all ones if condition is 1, and all zeros if condition + * is 0. + * + * Prerequisites: Disambiguator + */ +class ConditionalBranchFlattener: public ASTModifier +{ +public: + static constexpr char const* name = "ConditionalBranchFlattener"; + static void run(OptimiserStepContext& _context, Block& _ast); + + using ASTModifier::operator(); + void operator()(Block& _block) override; + +private: + ConditionalBranchFlattener(OptimiserStepContext& _context): m_context(_context) {} + + FunctionCall createBuiltinCall( + langutil::DebugData::ConstPtr _debugData, + std::string const& _name, + std::vector _arguments + ); + + OptimiserStepContext& m_context; +}; + +} diff --git a/libyul/optimiser/Suite.cpp b/libyul/optimiser/Suite.cpp index 17698d74e2a0..7e001b1565d0 100644 --- a/libyul/optimiser/Suite.cpp +++ b/libyul/optimiser/Suite.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -232,6 +233,7 @@ std::map> const& OptimiserSuite::all BlockFlattener, CircularReferencesPruner, CommonSubexpressionEliminator, + ConditionalBranchFlattener, ConditionalSimplifier, ConditionalUnsimplifier, ControlFlowSimplifier, @@ -273,6 +275,7 @@ std::map const& OptimiserSuite::stepNameToAbbreviationMap() {BlockFlattener::name, 'f'}, {CircularReferencesPruner::name, 'l'}, {CommonSubexpressionEliminator::name, 'c'}, + {ConditionalBranchFlattener::name, 'B'}, {ConditionalSimplifier::name, 'C'}, {ConditionalUnsimplifier::name, 'U'}, {ControlFlowSimplifier::name, 'n'}, diff --git a/test/libyul/YulOptimizerTestCommon.cpp b/test/libyul/YulOptimizerTestCommon.cpp index cd54318c19ef..7a4148b3a21b 100644 --- a/test/libyul/YulOptimizerTestCommon.cpp +++ b/test/libyul/YulOptimizerTestCommon.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -142,6 +143,12 @@ YulOptimizerTestCommon::YulOptimizerTestCommon(std::shared_ptr _ob CommonSubexpressionEliminator::run(*m_context, block); return block; }}, + {"conditionalBranchFlattener", [&]() { + auto block = disambiguate(); + updateContext(block); + ConditionalBranchFlattener::run(*m_context, block); + return block; + }}, {"conditionalUnsimplifier", [&]() { auto block = disambiguate(); updateContext(block); diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/basic.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/basic.yul new file mode 100644 index 000000000000..4d1ec56cefb8 --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/basic.yul @@ -0,0 +1,12 @@ +{ + let x := mload(0) + if slt(x, 0) { x := sub(0, x) } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let condition := iszero(iszero(slt(x, 0))) +// x := xor(x, and(sub(0, condition), xor(x, sub(0, x)))) +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/generic_add.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/generic_add.yul new file mode 100644 index 000000000000..a68ae6f83d6d --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/generic_add.yul @@ -0,0 +1,14 @@ +{ + let x := 10 + let c := 1 + if c { x := add(x, 1) } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := 10 +// let c := 1 +// let condition := iszero(iszero(c)) +// x := xor(x, and(sub(0, condition), xor(x, add(x, 1)))) +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/multi.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/multi.yul new file mode 100644 index 000000000000..e62e1af19a3d --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/multi.yul @@ -0,0 +1,21 @@ +{ + let x := mload(0) + let y := mload(0) + if slt(x, 0) { + x := sub(0, x) + } + if slt(y, 0) { + y := add(y, 2) + } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let y := mload(0) +// let condition := iszero(iszero(slt(x, 0))) +// x := xor(x, and(sub(0, condition), xor(x, sub(0, x)))) +// let condition_1 := iszero(iszero(slt(y, 0))) +// y := xor(y, and(sub(0, condition_1), xor(y, add(y, 2)))) +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_body.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_body.yul new file mode 100644 index 000000000000..21ef7ed23a5b --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_body.yul @@ -0,0 +1,12 @@ +{ + let x := mload(0) + if slt(x, 0) { x := not(x) } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let condition := iszero(iszero(slt(x, 0))) +// x := xor(x, and(sub(0, condition), xor(x, not(x)))) +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_condition.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_condition.yul new file mode 100644 index 000000000000..c5e23193821e --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/no_match_condition.yul @@ -0,0 +1,12 @@ +{ + let x := mload(0) + if slt(0, x) { x := sub(0, x) } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let condition := iszero(iszero(slt(0, x))) +// x := xor(x, and(sub(0, condition), xor(x, sub(0, x)))) +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/too_many.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/too_many.yul new file mode 100644 index 000000000000..9b6fe731e00d --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/too_many.yul @@ -0,0 +1,20 @@ +{ + let x := mload(0) + let y := mload(0) + if slt(x, 0) { + x := sub(0, x) + y := add(y, 2) + } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let y := mload(0) +// if slt(x, 0) +// { +// x := sub(0, x) +// y := add(y, 2) +// } +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/unsafe_body.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/unsafe_body.yul new file mode 100644 index 000000000000..f4366c122c08 --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/unsafe_body.yul @@ -0,0 +1,11 @@ +{ + let x := 10 + if x { sstore(0, 1) } +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := 10 +// if x { sstore(0, 1) } +// } diff --git a/test/libyul/yulOptimizerTests/conditionalBranchFlattener/with_context.yul b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/with_context.yul new file mode 100644 index 000000000000..b26e927e403f --- /dev/null +++ b/test/libyul/yulOptimizerTests/conditionalBranchFlattener/with_context.yul @@ -0,0 +1,14 @@ +{ + let x := mload(0) + if slt(x, 0) { x := sub(0, x) } + sstore(0, x) +} +// ---- +// step: conditionalBranchFlattener +// +// { +// let x := mload(0) +// let condition := iszero(iszero(slt(x, 0))) +// x := xor(x, and(sub(0, condition), xor(x, sub(0, x)))) +// sstore(0, x) +// }