Skip to content

Commit 5ba8c10

Browse files
committed
Use side-effects of user defined functions in evm code transform.
1 parent 4100a59 commit 5ba8c10

19 files changed

+182
-122
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Compiler Features:
88
* Commandline Interface: Add `--no-cbor-metadata` that skips CBOR metadata from getting appended at the end of the bytecode.
99
* Natspec: Add event Natspec inheritance for devdoc.
1010
* Standard JSON: Add a boolean field `settings.metadata.appendCBOR` that skips CBOR metadata from getting appended at the end of the bytecode.
11+
* Yul EVM Code Transform: Generate more optimal code for user-defined functions that always terminate a transaction. No return labels will be pushed for calls to functions that always terminate.
1112
* Yul Optimizer: Allow replacing the previously hard-coded cleanup sequence by specifying custom steps after a colon delimiter (``:``) in the sequence string.
1213
* Language Server: Add basic document hover support.
1314
* Optimizer: Added optimization rule ``and(shl(X, Y), shl(X, Z)) => shl(X, and(Y, Z))``.

libyul/backends/evm/ControlFlowGraph.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ struct CFG
148148
/// True, if the call is recursive, i.e. entering the function involves a control flow path (potentially involving
149149
/// more intermediate function calls) that leads back to this very call.
150150
bool recursive = false;
151+
/// True, if the call can return.
152+
bool canContinue = true;
151153
};
152154
struct Assignment
153155
{
@@ -210,10 +212,12 @@ struct CFG
210212
{
211213
std::shared_ptr<DebugData const> debugData;
212214
Scope::Function const& function;
215+
FunctionDefinition const& functionDefinition;
213216
BasicBlock* entry = nullptr;
214217
std::vector<VariableSlot> parameters;
215218
std::vector<VariableSlot> returnVariables;
216219
std::vector<BasicBlock*> exits;
220+
bool canContinue = true;
217221
};
218222

219223
/// The main entry point, i.e. the start of the outermost Yul block.

libyul/backends/evm/ControlFlowGraphBuilder.cpp

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <libyul/AST.h>
2424
#include <libyul/Exceptions.h>
2525
#include <libyul/Utilities.h>
26+
#include <libyul/ControlFlowSideEffectsCollector.h>
2627

2728
#include <libsolutil/cxx20.h>
2829
#include <libsolutil/Visitor.h>
@@ -214,7 +215,8 @@ std::unique_ptr<CFG> ControlFlowGraphBuilder::build(
214215
auto result = std::make_unique<CFG>();
215216
result->entry = &result->makeBlock(debugDataOf(_block));
216217

217-
ControlFlowGraphBuilder builder(*result, _analysisInfo, _dialect);
218+
ControlFlowSideEffectsCollector sideEffects(_dialect, _block);
219+
ControlFlowGraphBuilder builder(*result, _analysisInfo, sideEffects.functionSideEffects(), _dialect);
218220
builder.m_currentBlock = result->entry;
219221
builder(_block);
220222

@@ -232,10 +234,12 @@ std::unique_ptr<CFG> ControlFlowGraphBuilder::build(
232234
ControlFlowGraphBuilder::ControlFlowGraphBuilder(
233235
CFG& _graph,
234236
AsmAnalysisInfo const& _analysisInfo,
237+
map<FunctionDefinition const*, ControlFlowSideEffects> const& _functionSideEffects,
235238
Dialect const& _dialect
236239
):
237240
m_graph(_graph),
238241
m_info(_analysisInfo),
242+
m_functionSideEffects(_functionSideEffects),
239243
m_dialect(_dialect)
240244
{
241245
}
@@ -285,10 +289,10 @@ void ControlFlowGraphBuilder::operator()(Assignment const& _assignment)
285289
return VariableSlot{lookupVariable(_var.name), _var.debugData};
286290
}) | ranges::to<vector<VariableSlot>>;
287291

288-
yulAssert(m_currentBlock, "");
292+
Stack input = visitAssignmentRightHandSide(*_assignment.value, assignedVariables.size());
293+
yulAssert(m_currentBlock);
289294
m_currentBlock->operations.emplace_back(CFG::Operation{
290-
// input
291-
visitAssignmentRightHandSide(*_assignment.value, assignedVariables.size()),
295+
std::move(input),
292296
// output
293297
assignedVariables | ranges::to<Stack>,
294298
// operation
@@ -297,24 +301,13 @@ void ControlFlowGraphBuilder::operator()(Assignment const& _assignment)
297301
}
298302
void ControlFlowGraphBuilder::operator()(ExpressionStatement const& _exprStmt)
299303
{
300-
yulAssert(m_currentBlock, "");
301304
std::visit(util::GenericVisitor{
302305
[&](FunctionCall const& _call) {
303306
Stack const& output = visitFunctionCall(_call);
304307
yulAssert(output.empty(), "");
305308
},
306309
[&](auto const&) { yulAssert(false, ""); }
307310
}, _exprStmt.expression);
308-
309-
// TODO: Ideally this would be done on the expression label and for all functions that always revert,
310-
// not only for builtins.
311-
if (auto const* funCall = get_if<FunctionCall>(&_exprStmt.expression))
312-
if (BuiltinFunction const* builtin = m_dialect.builtin(funCall->functionName.name))
313-
if (builtin->controlFlowSideEffects.terminatesOrReverts())
314-
{
315-
m_currentBlock->exit = CFG::BasicBlock::Terminated{};
316-
m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock));
317-
}
318311
}
319312

320313
void ControlFlowGraphBuilder::operator()(Block const& _block)
@@ -331,7 +324,8 @@ void ControlFlowGraphBuilder::operator()(If const& _if)
331324
{
332325
auto& ifBranch = m_graph.makeBlock(debugDataOf(_if.body));
333326
auto& afterIf = m_graph.makeBlock(debugDataOf(*m_currentBlock));
334-
makeConditionalJump(debugDataOf(_if), std::visit(*this, *_if.condition), ifBranch, afterIf);
327+
StackSlot condition = std::visit(*this, *_if.condition);
328+
makeConditionalJump(debugDataOf(_if), std::move(condition), ifBranch, afterIf);
335329
m_currentBlock = &ifBranch;
336330
(*this)(_if.body);
337331
jump(debugDataOf(_if.body), afterIf);
@@ -349,8 +343,9 @@ void ControlFlowGraphBuilder::operator()(Switch const& _switch)
349343
// Artificially generate:
350344
// let <ghostVariable> := <switchExpression>
351345
VariableSlot ghostVarSlot{ghostVar, debugDataOf(*_switch.expression)};
346+
StackSlot expression = std::visit(*this, *_switch.expression);
352347
m_currentBlock->operations.emplace_back(CFG::Operation{
353-
Stack{std::visit(*this, *_switch.expression)},
348+
Stack{std::move(expression)},
354349
Stack{ghostVarSlot},
355350
CFG::Assignment{_switch.debugData, {ghostVarSlot}}
356351
});
@@ -430,7 +425,8 @@ void ControlFlowGraphBuilder::operator()(ForLoop const& _loop)
430425
else
431426
{
432427
jump(debugDataOf(_loop.pre), loopCondition);
433-
makeConditionalJump(debugDataOf(*_loop.condition), std::visit(*this, *_loop.condition), loopBody, afterLoop);
428+
StackSlot condition = std::visit(*this, *_loop.condition);
429+
makeConditionalJump(debugDataOf(*_loop.condition), std::move(condition), loopBody, afterLoop);
434430
m_currentBlock = &loopBody;
435431
(*this)(_loop.body);
436432
jump(debugDataOf(_loop.body), post);
@@ -473,41 +469,43 @@ void ControlFlowGraphBuilder::operator()(FunctionDefinition const& _function)
473469

474470
CFG::FunctionInfo& functionInfo = m_graph.functionInfo.at(&function);
475471

476-
ControlFlowGraphBuilder builder{m_graph, m_info, m_dialect};
472+
ControlFlowGraphBuilder builder{m_graph, m_info, m_functionSideEffects, m_dialect};
477473
builder.m_currentFunction = &functionInfo;
478474
builder.m_currentBlock = functionInfo.entry;
479475
builder(_function.body);
480476
functionInfo.exits.emplace_back(builder.m_currentBlock);
481477
builder.m_currentBlock->exit = CFG::BasicBlock::FunctionReturn{debugDataOf(_function), &functionInfo};
482478
}
483479

484-
void ControlFlowGraphBuilder::registerFunction(FunctionDefinition const& _function)
480+
void ControlFlowGraphBuilder::registerFunction(FunctionDefinition const& _functionDefinition)
485481
{
486482
yulAssert(m_scope, "");
487-
yulAssert(m_scope->identifiers.count(_function.name), "");
488-
Scope::Function& function = std::get<Scope::Function>(m_scope->identifiers.at(_function.name));
483+
yulAssert(m_scope->identifiers.count(_functionDefinition.name), "");
484+
Scope::Function& function = std::get<Scope::Function>(m_scope->identifiers.at(_functionDefinition.name));
489485

490-
yulAssert(m_info.scopes.at(&_function.body), "");
491-
Scope* virtualFunctionScope = m_info.scopes.at(m_info.virtualBlocks.at(&_function).get()).get();
486+
yulAssert(m_info.scopes.at(&_functionDefinition.body), "");
487+
Scope* virtualFunctionScope = m_info.scopes.at(m_info.virtualBlocks.at(&_functionDefinition).get()).get();
492488
yulAssert(virtualFunctionScope, "");
493489

494490
bool inserted = m_graph.functionInfo.emplace(std::make_pair(&function, CFG::FunctionInfo{
495-
_function.debugData,
491+
_functionDefinition.debugData,
496492
function,
497-
&m_graph.makeBlock(debugDataOf(_function.body)),
498-
_function.parameters | ranges::views::transform([&](auto const& _param) {
493+
_functionDefinition,
494+
&m_graph.makeBlock(debugDataOf(_functionDefinition.body)),
495+
_functionDefinition.parameters | ranges::views::transform([&](auto const& _param) {
499496
return VariableSlot{
500497
std::get<Scope::Variable>(virtualFunctionScope->identifiers.at(_param.name)),
501498
_param.debugData
502499
};
503500
}) | ranges::to<vector>,
504-
_function.returnVariables | ranges::views::transform([&](auto const& _retVar) {
501+
_functionDefinition.returnVariables | ranges::views::transform([&](auto const& _retVar) {
505502
return VariableSlot{
506503
std::get<Scope::Variable>(virtualFunctionScope->identifiers.at(_retVar.name)),
507504
_retVar.debugData
508505
};
509506
}) | ranges::to<vector>,
510-
{}
507+
{},
508+
m_functionSideEffects.at(&_functionDefinition).canContinue
511509
})).second;
512510
yulAssert(inserted);
513511
}
@@ -517,14 +515,16 @@ Stack const& ControlFlowGraphBuilder::visitFunctionCall(FunctionCall const& _cal
517515
yulAssert(m_scope, "");
518516
yulAssert(m_currentBlock, "");
519517

518+
Stack const* output = nullptr;
519+
bool canContinue = true;
520520
if (BuiltinFunction const* builtin = m_dialect.builtin(_call.functionName.name))
521521
{
522522
Stack inputs;
523523
for (auto&& [idx, arg]: _call.arguments | ranges::views::enumerate | ranges::views::reverse)
524524
if (!builtin->literalArgument(idx).has_value())
525525
inputs.emplace_back(std::visit(*this, arg));
526526
CFG::BuiltinCall builtinCall{_call.debugData, *builtin, _call, inputs.size()};
527-
return m_currentBlock->operations.emplace_back(CFG::Operation{
527+
output = &m_currentBlock->operations.emplace_back(CFG::Operation{
528528
// input
529529
std::move(inputs),
530530
// output
@@ -534,24 +534,34 @@ Stack const& ControlFlowGraphBuilder::visitFunctionCall(FunctionCall const& _cal
534534
// operation
535535
std::move(builtinCall)
536536
}).output;
537+
canContinue = builtin->controlFlowSideEffects.canContinue;
537538
}
538539
else
539540
{
540541
Scope::Function const& function = lookupFunction(_call.functionName.name);
541-
Stack inputs{FunctionCallReturnLabelSlot{_call}};
542+
canContinue = m_graph.functionInfo.at(&function).canContinue;
543+
Stack inputs;
544+
if (canContinue)
545+
inputs.emplace_back(FunctionCallReturnLabelSlot{_call});
542546
for (auto const& arg: _call.arguments | ranges::views::reverse)
543547
inputs.emplace_back(std::visit(*this, arg));
544-
return m_currentBlock->operations.emplace_back(CFG::Operation{
548+
output = &m_currentBlock->operations.emplace_back(CFG::Operation{
545549
// input
546550
std::move(inputs),
547551
// output
548552
ranges::views::iota(0u, function.returns.size()) | ranges::views::transform([&](size_t _i) {
549553
return TemporarySlot{_call, _i};
550554
}) | ranges::to<Stack>,
551555
// operation
552-
CFG::FunctionCall{_call.debugData, function, _call}
556+
CFG::FunctionCall{_call.debugData, function, _call, /* recursive */ false, canContinue}
553557
}).output;
554558
}
559+
if (!canContinue)
560+
{
561+
m_currentBlock->exit = CFG::BasicBlock::Terminated{};
562+
m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock));
563+
}
564+
return *output;
555565
}
556566

557567
Stack ControlFlowGraphBuilder::visitAssignmentRightHandSide(Expression const& _expression, size_t _expectedSlotCount)

libyul/backends/evm/ControlFlowGraphBuilder.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#pragma once
2222

2323
#include <libyul/backends/evm/ControlFlowGraph.h>
24+
#include <libyul/ControlFlowSideEffects.h>
2425

2526
namespace solidity::yul
2627
{
@@ -55,6 +56,7 @@ class ControlFlowGraphBuilder
5556
ControlFlowGraphBuilder(
5657
CFG& _graph,
5758
AsmAnalysisInfo const& _analysisInfo,
59+
std::map<FunctionDefinition const*, ControlFlowSideEffects> const& _functionSideEffects,
5860
Dialect const& _dialect
5961
);
6062
void registerFunction(FunctionDefinition const& _function);
@@ -77,6 +79,7 @@ class ControlFlowGraphBuilder
7779
);
7880
CFG& m_graph;
7981
AsmAnalysisInfo const& m_info;
82+
std::map<FunctionDefinition const*, ControlFlowSideEffects> const& m_functionSideEffects;
8083
Dialect const& m_dialect;
8184
CFG::BasicBlock* m_currentBlock = nullptr;
8285
Scope* m_scope = nullptr;

libyul/backends/evm/OptimizedEVMCodeTransform.cpp

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,35 +71,39 @@ void OptimizedEVMCodeTransform::operator()(CFG::FunctionCall const& _call)
7171
// Validate stack.
7272
{
7373
yulAssert(m_assembly.stackHeight() == static_cast<int>(m_stack.size()), "");
74-
yulAssert(m_stack.size() >= _call.function.get().arguments.size() + 1, "");
74+
yulAssert(m_stack.size() >= _call.function.get().arguments.size() + (_call.canContinue ? 1 : 0), "");
7575
// Assert that we got the correct arguments on stack for the call.
7676
for (auto&& [arg, slot]: ranges::zip_view(
7777
_call.functionCall.get().arguments | ranges::views::reverse,
7878
m_stack | ranges::views::take_last(_call.functionCall.get().arguments.size())
7979
))
8080
validateSlot(slot, arg);
8181
// Assert that we got the correct return label on stack.
82-
auto const* returnLabelSlot = get_if<FunctionCallReturnLabelSlot>(
83-
&m_stack.at(m_stack.size() - _call.functionCall.get().arguments.size() - 1)
84-
);
85-
yulAssert(returnLabelSlot && &returnLabelSlot->call.get() == &_call.functionCall.get(), "");
82+
if (_call.canContinue)
83+
{
84+
auto const* returnLabelSlot = get_if<FunctionCallReturnLabelSlot>(
85+
&m_stack.at(m_stack.size() - _call.functionCall.get().arguments.size() - 1)
86+
);
87+
yulAssert(returnLabelSlot && &returnLabelSlot->call.get() == &_call.functionCall.get(), "");
88+
}
8689
}
8790

8891
// Emit code.
8992
{
9093
m_assembly.setSourceLocation(originLocationOf(_call));
9194
m_assembly.appendJumpTo(
9295
getFunctionLabel(_call.function),
93-
static_cast<int>(_call.function.get().returns.size() - _call.function.get().arguments.size()) - 1,
96+
static_cast<int>(_call.function.get().returns.size() - _call.function.get().arguments.size()) - (_call.canContinue ? 1 : 0),
9497
AbstractAssembly::JumpType::IntoFunction
9598
);
96-
m_assembly.appendLabel(m_returnLabels.at(&_call.functionCall.get()));
99+
if (_call.canContinue)
100+
m_assembly.appendLabel(m_returnLabels.at(&_call.functionCall.get()));
97101
}
98102

99103
// Update stack.
100104
{
101105
// Remove arguments and return label from m_stack.
102-
for (size_t i = 0; i < _call.function.get().arguments.size() + 1; ++i)
106+
for (size_t i = 0; i < _call.function.get().arguments.size() + (_call.canContinue ? 1 : 0); ++i)
103107
m_stack.pop_back();
104108
// Push return values to m_stack.
105109
for (size_t index: ranges::views::iota(0u, _call.function.get().returns.size()))
@@ -479,8 +483,9 @@ void OptimizedEVMCodeTransform::operator()(CFG::BasicBlock const& _block)
479483
},
480484
[&](CFG::BasicBlock::FunctionReturn const& _functionReturn)
481485
{
482-
yulAssert(m_currentFunctionInfo, "");
483-
yulAssert(m_currentFunctionInfo == _functionReturn.info, "");
486+
yulAssert(m_currentFunctionInfo);
487+
yulAssert(m_currentFunctionInfo == _functionReturn.info);
488+
yulAssert(m_currentFunctionInfo->canContinue);
484489

485490
// Construct the function return layout, which is fully determined by the function signature.
486491
Stack exitStack = m_currentFunctionInfo->returnVariables | ranges::views::transform([](auto const& _varSlot){
@@ -494,11 +499,13 @@ void OptimizedEVMCodeTransform::operator()(CFG::BasicBlock const& _block)
494499
},
495500
[&](CFG::BasicBlock::Terminated const&)
496501
{
497-
// Assert that the last builtin call was in fact terminating.
498-
yulAssert(!_block.operations.empty(), "");
499-
CFG::BuiltinCall const* builtinCall = get_if<CFG::BuiltinCall>(&_block.operations.back().operation);
500-
yulAssert(builtinCall, "");
501-
yulAssert(builtinCall->builtin.get().controlFlowSideEffects.terminatesOrReverts(), "");
502+
yulAssert(!_block.operations.empty());
503+
if (CFG::BuiltinCall const* builtinCall = get_if<CFG::BuiltinCall>(&_block.operations.back().operation))
504+
yulAssert(builtinCall->builtin.get().controlFlowSideEffects.terminatesOrReverts(), "");
505+
else if (CFG::FunctionCall const* functionCall = get_if<CFG::FunctionCall>(&_block.operations.back().operation))
506+
yulAssert(!functionCall->canContinue);
507+
else
508+
yulAssert(false);
502509
}
503510
}, _block.exit);
504511
// TODO: We could assert that the last emitted assembly item terminated or was an (unconditional) jump.
@@ -515,7 +522,8 @@ void OptimizedEVMCodeTransform::operator()(CFG::FunctionInfo const& _functionInf
515522
yulAssert(m_stack.empty() && m_assembly.stackHeight() == 0, "");
516523

517524
// Create function entry layout in m_stack.
518-
m_stack.emplace_back(FunctionReturnLabelSlot{_functionInfo.function});
525+
if (_functionInfo.canContinue)
526+
m_stack.emplace_back(FunctionReturnLabelSlot{_functionInfo.function});
519527
for (auto const& param: _functionInfo.parameters | ranges::views::reverse)
520528
m_stack.emplace_back(param);
521529
m_assembly.setStackHeight(static_cast<int>(m_stack.size()));

0 commit comments

Comments
 (0)