/* 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 /** * Transformation of a Yul AST into a control flow graph. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace solidity; using namespace solidity::yul; namespace { /// Removes edges to blocks that are not reachable. void cleanUnreachable(CFG& _cfg) { // Determine which blocks are reachable from the entry. util::BreadthFirstSearch reachabilityCheck{{_cfg.entry}}; for (auto const& functionInfo: _cfg.functionInfo | ranges::views::values) reachabilityCheck.verticesToTraverse.emplace_back(functionInfo.entry); reachabilityCheck.run([&](CFG::BasicBlock* _node, auto&& _addChild) { visit(util::GenericVisitor{ [&](CFG::BasicBlock::Jump const& _jump) { _addChild(_jump.target); }, [&](CFG::BasicBlock::ConditionalJump const& _jump) { _addChild(_jump.zero); _addChild(_jump.nonZero); }, [](CFG::BasicBlock::FunctionReturn const&) {}, [](CFG::BasicBlock::Terminated const&) {}, [](CFG::BasicBlock::MainExit const&) {} }, _node->exit); }); // Remove all entries from unreachable nodes from the graph. for (CFG::BasicBlock* node: reachabilityCheck.visited) cxx20::erase_if(node->entries, [&](CFG::BasicBlock* entry) -> bool { return !reachabilityCheck.visited.count(entry); }); } /// Sets the ``recursive`` member to ``true`` for all recursive function calls. void markRecursiveCalls(CFG& _cfg) { std::map> callsPerBlock; auto const& findCalls = [&](CFG::BasicBlock* _block) { if (auto* calls = util::valueOrNullptr(callsPerBlock, _block)) return *calls; std::vector& calls = callsPerBlock[_block]; util::BreadthFirstSearch{{_block}}.run([&](CFG::BasicBlock* _block, auto _addChild) { for (auto& operation: _block->operations) if (auto* functionCall = std::get_if(&operation.operation)) calls.emplace_back(functionCall); std::visit(util::GenericVisitor{ [&](CFG::BasicBlock::MainExit const&) {}, [&](CFG::BasicBlock::Jump const& _jump) { _addChild(_jump.target); }, [&](CFG::BasicBlock::ConditionalJump const& _conditionalJump) { _addChild(_conditionalJump.zero); _addChild(_conditionalJump.nonZero); }, [&](CFG::BasicBlock::FunctionReturn const&) {}, [&](CFG::BasicBlock::Terminated const&) {}, }, _block->exit); }); return calls; }; for (auto& functionInfo: _cfg.functionInfo | ranges::views::values) for (CFG::FunctionCall* call: findCalls(functionInfo.entry)) { util::BreadthFirstSearch breadthFirstSearch{{call}}; breadthFirstSearch.run([&](CFG::FunctionCall* _call, auto _addChild) { auto& calledFunctionInfo = _cfg.functionInfo.at(&_call->function.get()); if (&calledFunctionInfo == &functionInfo) { call->recursive = true; breadthFirstSearch.abort(); return; } for (CFG::FunctionCall* nestedCall: findCalls(_cfg.functionInfo.at(&_call->function.get()).entry)) _addChild(nestedCall); }); } } /// Marks each cut-vertex in the CFG, i.e. each block that begins a disconnected sub-graph of the CFG. /// Entering such a block means that control flow will never return to a previously visited block. void markStartsOfSubGraphs(CFG& _cfg) { std::vector entries; entries.emplace_back(_cfg.entry); for (auto&& functionInfo: _cfg.functionInfo | ranges::views::values) entries.emplace_back(functionInfo.entry); for (auto& entry: entries) { /** * Detect bridges following Algorithm 1 in https://arxiv.org/pdf/2108.07346.pdf * and mark the bridge targets as starts of sub-graphs. */ std::set visited; std::map disc; std::map low; std::map parent; size_t time = 0; auto dfs = [&](CFG::BasicBlock* _u, auto _recurse) -> void { visited.insert(_u); disc[_u] = low[_u] = time; time++; std::vector children = _u->entries; visit(util::GenericVisitor{ [&](CFG::BasicBlock::Jump const& _jump) { children.emplace_back(_jump.target); }, [&](CFG::BasicBlock::ConditionalJump const& _jump) { children.emplace_back(_jump.zero); children.emplace_back(_jump.nonZero); }, [&](CFG::BasicBlock::FunctionReturn const&) {}, [&](CFG::BasicBlock::Terminated const&) { _u->isStartOfSubGraph = true; }, [&](CFG::BasicBlock::MainExit const&) { _u->isStartOfSubGraph = true; } }, _u->exit); yulAssert(!util::contains(children, _u)); for (CFG::BasicBlock* v: children) if (!visited.count(v)) { parent[v] = _u; _recurse(v, _recurse); low[_u] = std::min(low[_u], low[v]); if (low[v] > disc[_u]) { // _u <-> v is a cut edge in the undirected graph bool edgeVtoU = util::contains(_u->entries, v); bool edgeUtoV = util::contains(v->entries, _u); if (edgeVtoU && !edgeUtoV) // Cut edge v -> _u _u->isStartOfSubGraph = true; else if (edgeUtoV && !edgeVtoU) // Cut edge _u -> v v->isStartOfSubGraph = true; } } else if (v != parent[_u]) low[_u] = std::min(low[_u], disc[v]); }; dfs(entry, dfs); } } /// Marks each block that needs to maintain a clean stack. That is each block that has an outgoing /// path to a function return. void markNeedsCleanStack(CFG& _cfg) { for (auto& functionInfo: _cfg.functionInfo | ranges::views::values) for (CFG::BasicBlock* exit: functionInfo.exits) util::BreadthFirstSearch{{exit}}.run([&](CFG::BasicBlock* _block, auto _addChild) { _block->needsCleanStack = true; for (CFG::BasicBlock* entry: _block->entries) _addChild(entry); }); } } std::unique_ptr ControlFlowGraphBuilder::build( AsmAnalysisInfo const& _analysisInfo, Dialect const& _dialect, Block const& _block ) { auto result = std::make_unique(); result->entry = &result->makeBlock(debugDataOf(_block)); ControlFlowSideEffectsCollector sideEffects(_dialect, _block); ControlFlowGraphBuilder builder(*result, _analysisInfo, sideEffects.functionSideEffects(), _dialect); builder.m_currentBlock = result->entry; builder(_block); cleanUnreachable(*result); markRecursiveCalls(*result); markStartsOfSubGraphs(*result); markNeedsCleanStack(*result); // TODO: It might be worthwhile to run some further simplifications on the graph itself here. // E.g. if there is a jump to a node that has the jumping node as its only entry, the nodes can be fused, etc. return result; } ControlFlowGraphBuilder::ControlFlowGraphBuilder( CFG& _graph, AsmAnalysisInfo const& _analysisInfo, std::map const& _functionSideEffects, Dialect const& _dialect ): m_graph(_graph), m_info(_analysisInfo), m_functionSideEffects(_functionSideEffects), m_dialect(_dialect) { } StackSlot ControlFlowGraphBuilder::operator()(Literal const& _literal) { return LiteralSlot{valueOfLiteral(_literal), _literal.debugData}; } StackSlot ControlFlowGraphBuilder::operator()(Identifier const& _identifier) { return VariableSlot{lookupVariable(_identifier.name), _identifier.debugData}; } StackSlot ControlFlowGraphBuilder::operator()(Expression const& _expression) { return std::visit(*this, _expression); } StackSlot ControlFlowGraphBuilder::operator()(FunctionCall const& _call) { Stack const& output = visitFunctionCall(_call); yulAssert(output.size() == 1, ""); return output.front(); } void ControlFlowGraphBuilder::operator()(VariableDeclaration const& _varDecl) { yulAssert(m_currentBlock, ""); auto declaredVariables = _varDecl.variables | ranges::views::transform([&](TypedName const& _var) { return VariableSlot{lookupVariable(_var.name), _var.debugData}; }) | ranges::to>; Stack input; if (_varDecl.value) input = visitAssignmentRightHandSide(*_varDecl.value, declaredVariables.size()); else input = Stack(_varDecl.variables.size(), LiteralSlot{0, _varDecl.debugData}); m_currentBlock->operations.emplace_back(CFG::Operation{ std::move(input), declaredVariables | ranges::to, CFG::Assignment{_varDecl.debugData, declaredVariables} }); } void ControlFlowGraphBuilder::operator()(Assignment const& _assignment) { auto assignedVariables = _assignment.variableNames | ranges::views::transform([&](Identifier const& _var) { return VariableSlot{lookupVariable(_var.name), _var.debugData}; }) | ranges::to>; Stack input = visitAssignmentRightHandSide(*_assignment.value, assignedVariables.size()); yulAssert(m_currentBlock); m_currentBlock->operations.emplace_back(CFG::Operation{ std::move(input), // output assignedVariables | ranges::to, // operation CFG::Assignment{_assignment.debugData, assignedVariables} }); } void ControlFlowGraphBuilder::operator()(ExpressionStatement const& _exprStmt) { std::visit(util::GenericVisitor{ [&](FunctionCall const& _call) { Stack const& output = visitFunctionCall(_call); yulAssert(output.empty(), ""); }, [&](auto const&) { yulAssert(false, ""); } }, _exprStmt.expression); } void ControlFlowGraphBuilder::operator()(Block const& _block) { ScopedSaveAndRestore saveScope(m_scope, m_info.scopes.at(&_block).get()); for (auto const& statement: _block.statements) if (auto const* function = std::get_if(&statement)) registerFunction(*function); for (auto const& statement: _block.statements) std::visit(*this, statement); } void ControlFlowGraphBuilder::operator()(If const& _if) { auto& ifBranch = m_graph.makeBlock(debugDataOf(_if.body)); auto& afterIf = m_graph.makeBlock(debugDataOf(*m_currentBlock)); StackSlot condition = std::visit(*this, *_if.condition); makeConditionalJump(debugDataOf(_if), std::move(condition), ifBranch, afterIf); m_currentBlock = &ifBranch; (*this)(_if.body); jump(debugDataOf(_if.body), afterIf); } void ControlFlowGraphBuilder::operator()(Switch const& _switch) { yulAssert(m_currentBlock, ""); std::shared_ptr preSwitchDebugData = debugDataOf(_switch); auto ghostVariableId = m_graph.ghostVariables.size(); YulString ghostVariableName("GHOST[" + std::to_string(ghostVariableId) + "]"); auto& ghostVar = m_graph.ghostVariables.emplace_back(Scope::Variable{""_yulstring, ghostVariableName}); // Artificially generate: // let := VariableSlot ghostVarSlot{ghostVar, debugDataOf(*_switch.expression)}; StackSlot expression = std::visit(*this, *_switch.expression); m_currentBlock->operations.emplace_back(CFG::Operation{ Stack{std::move(expression)}, Stack{ghostVarSlot}, CFG::Assignment{_switch.debugData, {ghostVarSlot}} }); BuiltinFunction const* equalityBuiltin = m_dialect.equalityFunction({}); yulAssert(equalityBuiltin, ""); // Artificially generate: // eq(, ) auto makeValueCompare = [&](Case const& _case) { yul::FunctionCall const& ghostCall = m_graph.ghostCalls.emplace_back(yul::FunctionCall{ debugDataOf(_case), yul::Identifier{{}, "eq"_yulstring}, {*_case.value, Identifier{{}, ghostVariableName}} }); CFG::Operation& operation = m_currentBlock->operations.emplace_back(CFG::Operation{ Stack{ghostVarSlot, LiteralSlot{valueOfLiteral(*_case.value), debugDataOf(*_case.value)}}, Stack{TemporarySlot{ghostCall, 0}}, CFG::BuiltinCall{debugDataOf(_case), *equalityBuiltin, ghostCall, 2}, }); return operation.output.front(); }; CFG::BasicBlock& afterSwitch = m_graph.makeBlock(preSwitchDebugData); yulAssert(!_switch.cases.empty(), ""); for (auto const& switchCase: _switch.cases | ranges::views::drop_last(1)) { yulAssert(switchCase.value, ""); auto& caseBranch = m_graph.makeBlock(debugDataOf(switchCase.body)); auto& elseBranch = m_graph.makeBlock(debugDataOf(_switch)); makeConditionalJump(debugDataOf(switchCase), makeValueCompare(switchCase), caseBranch, elseBranch); m_currentBlock = &caseBranch; (*this)(switchCase.body); jump(debugDataOf(switchCase.body), afterSwitch); m_currentBlock = &elseBranch; } Case const& switchCase = _switch.cases.back(); if (switchCase.value) { CFG::BasicBlock& caseBranch = m_graph.makeBlock(debugDataOf(switchCase.body)); makeConditionalJump(debugDataOf(switchCase), makeValueCompare(switchCase), caseBranch, afterSwitch); m_currentBlock = &caseBranch; } (*this)(switchCase.body); jump(debugDataOf(switchCase.body), afterSwitch); } void ControlFlowGraphBuilder::operator()(ForLoop const& _loop) { std::shared_ptr preLoopDebugData = debugDataOf(_loop); ScopedSaveAndRestore scopeRestore(m_scope, m_info.scopes.at(&_loop.pre).get()); (*this)(_loop.pre); std::optional constantCondition; if (auto const* literalCondition = std::get_if(_loop.condition.get())) constantCondition = valueOfLiteral(*literalCondition) != 0; CFG::BasicBlock& loopCondition = m_graph.makeBlock(debugDataOf(*_loop.condition)); CFG::BasicBlock& loopBody = m_graph.makeBlock(debugDataOf(_loop.body)); CFG::BasicBlock& post = m_graph.makeBlock(debugDataOf(_loop.post)); CFG::BasicBlock& afterLoop = m_graph.makeBlock(preLoopDebugData); ScopedSaveAndRestore scopedSaveAndRestore(m_forLoopInfo, ForLoopInfo{afterLoop, post}); if (constantCondition.has_value()) { if (*constantCondition) { jump(debugDataOf(_loop.pre), loopBody); (*this)(_loop.body); jump(debugDataOf(_loop.body), post); (*this)(_loop.post); jump(debugDataOf(_loop.post), loopBody, true); } else jump(debugDataOf(_loop.pre), afterLoop); } else { jump(debugDataOf(_loop.pre), loopCondition); StackSlot condition = std::visit(*this, *_loop.condition); makeConditionalJump(debugDataOf(*_loop.condition), std::move(condition), loopBody, afterLoop); m_currentBlock = &loopBody; (*this)(_loop.body); jump(debugDataOf(_loop.body), post); (*this)(_loop.post); jump(debugDataOf(_loop.post), loopCondition, true); } m_currentBlock = &afterLoop; } void ControlFlowGraphBuilder::operator()(Break const& _break) { yulAssert(m_forLoopInfo.has_value(), ""); jump(debugDataOf(_break), m_forLoopInfo->afterLoop); m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock)); } void ControlFlowGraphBuilder::operator()(Continue const& _continue) { yulAssert(m_forLoopInfo.has_value(), ""); jump(debugDataOf(_continue), m_forLoopInfo->post); m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock)); } // '_leave' and '__leave' are reserved in VisualStudio void ControlFlowGraphBuilder::operator()(Leave const& leave_) { yulAssert(m_currentFunction.has_value(), ""); m_currentBlock->exit = CFG::BasicBlock::FunctionReturn{debugDataOf(leave_), *m_currentFunction}; (*m_currentFunction)->exits.emplace_back(m_currentBlock); m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock)); } void ControlFlowGraphBuilder::operator()(FunctionDefinition const& _function) { yulAssert(m_scope, ""); yulAssert(m_scope->identifiers.count(_function.name), ""); Scope::Function& function = std::get(m_scope->identifiers.at(_function.name)); m_graph.functions.emplace_back(&function); CFG::FunctionInfo& functionInfo = m_graph.functionInfo.at(&function); ControlFlowGraphBuilder builder{m_graph, m_info, m_functionSideEffects, m_dialect}; builder.m_currentFunction = &functionInfo; builder.m_currentBlock = functionInfo.entry; builder(_function.body); functionInfo.exits.emplace_back(builder.m_currentBlock); builder.m_currentBlock->exit = CFG::BasicBlock::FunctionReturn{debugDataOf(_function), &functionInfo}; } void ControlFlowGraphBuilder::registerFunction(FunctionDefinition const& _functionDefinition) { yulAssert(m_scope, ""); yulAssert(m_scope->identifiers.count(_functionDefinition.name), ""); Scope::Function& function = std::get(m_scope->identifiers.at(_functionDefinition.name)); yulAssert(m_info.scopes.at(&_functionDefinition.body), ""); Scope* virtualFunctionScope = m_info.scopes.at(m_info.virtualBlocks.at(&_functionDefinition).get()).get(); yulAssert(virtualFunctionScope, ""); bool inserted = m_graph.functionInfo.emplace(std::make_pair(&function, CFG::FunctionInfo{ _functionDefinition.debugData, function, _functionDefinition, &m_graph.makeBlock(debugDataOf(_functionDefinition.body)), _functionDefinition.parameters | ranges::views::transform([&](auto const& _param) { return VariableSlot{ std::get(virtualFunctionScope->identifiers.at(_param.name)), _param.debugData }; }) | ranges::to, _functionDefinition.returnVariables | ranges::views::transform([&](auto const& _retVar) { return VariableSlot{ std::get(virtualFunctionScope->identifiers.at(_retVar.name)), _retVar.debugData }; }) | ranges::to, {}, m_functionSideEffects.at(&_functionDefinition).canContinue })).second; yulAssert(inserted); } Stack const& ControlFlowGraphBuilder::visitFunctionCall(FunctionCall const& _call) { yulAssert(m_scope, ""); yulAssert(m_currentBlock, ""); Stack const* output = nullptr; bool canContinue = true; if (BuiltinFunction const* builtin = m_dialect.builtin(_call.functionName.name)) { Stack inputs; for (auto&& [idx, arg]: _call.arguments | ranges::views::enumerate | ranges::views::reverse) if (!builtin->literalArgument(idx).has_value()) inputs.emplace_back(std::visit(*this, arg)); CFG::BuiltinCall builtinCall{_call.debugData, *builtin, _call, inputs.size()}; output = &m_currentBlock->operations.emplace_back(CFG::Operation{ // input std::move(inputs), // output ranges::views::iota(0u, builtin->returns.size()) | ranges::views::transform([&](size_t _i) { return TemporarySlot{_call, _i}; }) | ranges::to, // operation std::move(builtinCall) }).output; canContinue = builtin->controlFlowSideEffects.canContinue; } else { Scope::Function const& function = lookupFunction(_call.functionName.name); canContinue = m_graph.functionInfo.at(&function).canContinue; Stack inputs; if (canContinue) inputs.emplace_back(FunctionCallReturnLabelSlot{_call}); for (auto const& arg: _call.arguments | ranges::views::reverse) inputs.emplace_back(std::visit(*this, arg)); output = &m_currentBlock->operations.emplace_back(CFG::Operation{ // input std::move(inputs), // output ranges::views::iota(0u, function.returns.size()) | ranges::views::transform([&](size_t _i) { return TemporarySlot{_call, _i}; }) | ranges::to, // operation CFG::FunctionCall{_call.debugData, function, _call, /* recursive */ false, canContinue} }).output; } if (!canContinue) { m_currentBlock->exit = CFG::BasicBlock::Terminated{}; m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock)); } return *output; } Stack ControlFlowGraphBuilder::visitAssignmentRightHandSide(Expression const& _expression, size_t _expectedSlotCount) { return std::visit(util::GenericVisitor{ [&](FunctionCall const& _call) -> Stack { Stack const& output = visitFunctionCall(_call); yulAssert(_expectedSlotCount == output.size(), ""); return output; }, [&](auto const& _identifierOrLiteral) -> Stack { yulAssert(_expectedSlotCount == 1, ""); return {(*this)(_identifierOrLiteral)}; } }, _expression); } Scope::Function const& ControlFlowGraphBuilder::lookupFunction(YulString _name) const { Scope::Function const* function = nullptr; yulAssert(m_scope->lookup(_name, util::GenericVisitor{ [](Scope::Variable&) { yulAssert(false, "Expected function name."); }, [&](Scope::Function& _function) { function = &_function; } }), "Function name not found."); yulAssert(function, ""); return *function; } Scope::Variable const& ControlFlowGraphBuilder::lookupVariable(YulString _name) const { yulAssert(m_scope, ""); Scope::Variable const* var = nullptr; if (m_scope->lookup(_name, util::GenericVisitor{ [&](Scope::Variable& _var) { var = &_var; }, [](Scope::Function&) { yulAssert(false, "Function not removed during desugaring."); } })) { yulAssert(var, ""); return *var; }; yulAssert(false, "External identifier access unimplemented."); } void ControlFlowGraphBuilder::makeConditionalJump( std::shared_ptr _debugData, StackSlot _condition, CFG::BasicBlock& _nonZero, CFG::BasicBlock& _zero ) { yulAssert(m_currentBlock, ""); m_currentBlock->exit = CFG::BasicBlock::ConditionalJump{ std::move(_debugData), std::move(_condition), &_nonZero, &_zero }; _nonZero.entries.emplace_back(m_currentBlock); _zero.entries.emplace_back(m_currentBlock); m_currentBlock = nullptr; } void ControlFlowGraphBuilder::jump( std::shared_ptr _debugData, CFG::BasicBlock& _target, bool backwards ) { yulAssert(m_currentBlock, ""); m_currentBlock->exit = CFG::BasicBlock::Jump{std::move(_debugData), &_target, backwards}; _target.entries.emplace_back(m_currentBlock); m_currentBlock = &_target; }