diff options
author | chriseth <chris@ethereum.org> | 2018-10-02 16:46:59 +0800 |
---|---|---|
committer | chriseth <chris@ethereum.org> | 2018-10-16 23:16:03 +0800 |
commit | e2e4a9fe81656724111c444cbf253d39bbb2b67b (patch) | |
tree | bf6b303303ac1c3ab106caeaee476e9c973a9df0 | |
parent | 72b1bb00bd86ea3c647b3f327c416ab8f3421166 (diff) | |
download | dexon-solidity-e2e4a9fe81656724111c444cbf253d39bbb2b67b.tar.gz dexon-solidity-e2e4a9fe81656724111c444cbf253d39bbb2b67b.tar.zst dexon-solidity-e2e4a9fe81656724111c444cbf253d39bbb2b67b.zip |
New full inliner.
12 files changed, 226 insertions, 253 deletions
diff --git a/libyul/optimiser/FullInliner.cpp b/libyul/optimiser/FullInliner.cpp index 4e419987..75cd9d5c 100644 --- a/libyul/optimiser/FullInliner.cpp +++ b/libyul/optimiser/FullInliner.cpp @@ -23,12 +23,13 @@ #include <libyul/optimiser/ASTCopier.h> #include <libyul/optimiser/ASTWalker.h> #include <libyul/optimiser/NameCollector.h> -#include <libyul/optimiser/Semantics.h> +#include <libyul/optimiser/Utilities.h> #include <libyul/Exceptions.h> #include <libsolidity/inlineasm/AsmData.h> #include <libdevcore/CommonData.h> +#include <libdevcore/Visitor.h> #include <boost/range/adaptor/reversed.hpp> @@ -56,7 +57,8 @@ FullInliner::FullInliner(Block& _ast): void FullInliner::run() { assertThrow(m_ast.statements[0].type() == typeid(Block), OptimizerException, ""); - InlineModifier(*this, m_nameDispenser, "").visit(m_ast.statements[0]); + + handleBlock("", boost::get<Block>(m_ast.statements[0])); while (!m_functionsToVisit.empty()) handleFunction(**m_functionsToVisit.begin()); } @@ -66,168 +68,105 @@ void FullInliner::handleFunction(FunctionDefinition& _fun) if (!m_functionsToVisit.count(&_fun)) return; m_functionsToVisit.erase(&_fun); - (InlineModifier(*this, m_nameDispenser, _fun.name))(_fun.body); -} - -void InlineModifier::operator()(FunctionalInstruction& _instruction) -{ - visitArguments(_instruction.arguments); -} -void InlineModifier::operator()(FunctionCall&) -{ - assertThrow(false, OptimizerException, "Should be handled in visit() instead."); + handleBlock(_fun.name, _fun.body); } -void InlineModifier::operator()(ForLoop& _loop) +void FullInliner::handleBlock(string const& _currentFunctionName, Block& _block) { - (*this)(_loop.pre); - // Do not visit the condition because we cannot inline there. - (*this)(_loop.post); - (*this)(_loop.body); + InlineModifier{*this, m_nameDispenser, _currentFunctionName}(_block); } void InlineModifier::operator()(Block& _block) { - vector<Statement> saved; - saved.swap(m_statementsToPrefix); - - // This is only used if needed to minimize the number of move operations. - vector<Statement> modifiedStatements; - for (size_t i = 0; i < _block.statements.size(); ++i) - { - visit(_block.statements.at(i)); - if (!m_statementsToPrefix.empty()) - { - if (modifiedStatements.empty()) - std::move( - _block.statements.begin(), - _block.statements.begin() + i, - back_inserter(modifiedStatements) - ); - modifiedStatements += std::move(m_statementsToPrefix); - m_statementsToPrefix.clear(); - } - if (!modifiedStatements.empty()) - modifiedStatements.emplace_back(std::move(_block.statements[i])); - } - if (!modifiedStatements.empty()) - _block.statements = std::move(modifiedStatements); - - saved.swap(m_statementsToPrefix); + function<boost::optional<vector<Statement>>(Statement&)> f = [&](Statement& _statement) -> boost::optional<vector<Statement>> { + visit(_statement); + return tryInlineStatement(_statement); + }; + iterateReplacing(_block.statements, f); } -void InlineModifier::visit(Expression& _expression) +boost::optional<vector<Statement>> InlineModifier::tryInlineStatement(Statement& _statement) { - if (_expression.type() != typeid(FunctionCall)) - return ASTModifier::visit(_expression); - - FunctionCall& funCall = boost::get<FunctionCall>(_expression); - FunctionDefinition& fun = m_driver.function(funCall.functionName.name); - - m_driver.handleFunction(fun); - - // TODO: Insert good heuristic here. Perhaps implement that inside the driver. - bool doInline = funCall.functionName.name != m_currentFunction; - - if (fun.returnVariables.size() > 1) - doInline = false; - + // Only inline for expression statements, assignments and variable declarations. + Expression* e = boost::apply_visitor(GenericFallbackReturnsVisitor<Expression*, ExpressionStatement, Assignment, VariableDeclaration>( + [](ExpressionStatement& _s) { return &_s.expression; }, + [](Assignment& _s) { return _s.value.get(); }, + [](VariableDeclaration& _s) { return _s.value.get(); } + ), _statement); + if (e) { - vector<string> argNames; - vector<string> argTypes; - for (auto const& arg: fun.parameters) + // Only inline direct function calls. + FunctionCall* funCall = boost::apply_visitor(GenericFallbackReturnsVisitor<FunctionCall*, FunctionCall&>( + [](FunctionCall& _e) { return &_e; } + ), *e); + if (funCall) { - argNames.push_back(fun.name + "_" + arg.name); - argTypes.push_back(arg.type); - } - visitArguments(funCall.arguments, argNames, argTypes, doInline); - } + FunctionDefinition& fun = m_driver.function(funCall->functionName.name); + m_driver.handleFunction(fun); - if (!doInline) - return; + // TODO: Insert good heuristic here. Perhaps implement that inside the driver. + bool doInline = funCall->functionName.name != m_currentFunction; - map<string, string> variableReplacements; - for (size_t i = 0; i < funCall.arguments.size(); ++i) - variableReplacements[fun.parameters[i].name] = boost::get<Identifier>(funCall.arguments[i]).name; - if (fun.returnVariables.empty()) - _expression = noop(funCall.location); - else - { - string returnVariable = fun.returnVariables[0].name; - variableReplacements[returnVariable] = newName(fun.name + "_" + returnVariable); - - m_statementsToPrefix.emplace_back(VariableDeclaration{ - funCall.location, - {{funCall.location, variableReplacements[returnVariable], fun.returnVariables[0].type}}, - {} - }); - _expression = Identifier{funCall.location, variableReplacements[returnVariable]}; - } - m_statementsToPrefix.emplace_back(BodyCopier(m_nameDispenser, fun.name + "_", variableReplacements)(fun.body)); -} - -void InlineModifier::visit(Statement& _statement) -{ - ASTModifier::visit(_statement); - // Replace pop(0) expression statemets (and others) by empty blocks. - if (_statement.type() == typeid(ExpressionStatement)) - { - ExpressionStatement& expSt = boost::get<ExpressionStatement>(_statement); - if (expSt.expression.type() == typeid(FunctionalInstruction)) - { - FunctionalInstruction& funInstr = boost::get<FunctionalInstruction>(expSt.expression); - if (funInstr.instruction == solidity::Instruction::POP) - if (MovableChecker(funInstr.arguments.at(0)).movable()) - _statement = Block{expSt.location, {}}; + if (doInline) + return performInline(_statement, *funCall, fun); } } + return {}; } -void InlineModifier::visitArguments( - vector<Expression>& _arguments, - vector<string> const& _nameHints, - vector<string> const& _types, - bool _moveToFront -) +vector<Statement> InlineModifier::performInline(Statement& _statement, FunctionCall& _funCall, FunctionDefinition& _function) { - // If one of the elements moves parts to the front, all other elements right of it - // also have to be moved to the front to keep the order of evaluation. - vector<Statement> prefix; - for (size_t i = 0; i < _arguments.size(); ++i) - { - auto& arg = _arguments[i]; - // TODO optimize vector operations, check that it actually moves - auto internalPrefix = visitRecursively(arg); - if (!internalPrefix.empty()) + vector<Statement> newStatements; + map<string, string> variableReplacements; + + // helper function to create a new variable that is supposed to model + // an existing variable. + auto newVariable = [&](TypedName const& _existingVariable, Expression* _value) { + string newName = m_nameDispenser.newName(_function.name + "_" + _existingVariable.name); + variableReplacements[_existingVariable.name] = newName; + VariableDeclaration varDecl{_funCall.location, {{_funCall.location, newName, _existingVariable.type}}, {}}; + if (_value) + varDecl.value = make_shared<Expression>(std::move(*_value)); + newStatements.emplace_back(std::move(varDecl)); + }; + + for (size_t i = 0; i < _funCall.arguments.size(); ++i) + newVariable(_function.parameters[i], &_funCall.arguments[i]); + for (auto const& var: _function.returnVariables) + newVariable(var, nullptr); + + Statement newBody = BodyCopier(m_nameDispenser, _function.name + "_", variableReplacements)(_function.body); + newStatements += std::move(boost::get<Block>(newBody).statements); + + boost::apply_visitor(GenericFallbackVisitor<Assignment, VariableDeclaration>{ + [&](Assignment& _assignment) { - _moveToFront = true; - // We go through the arguments left to right, so we have to invert - // the prefixes. - prefix = std::move(internalPrefix) + std::move(prefix); - } - else if (_moveToFront) + for (size_t i = 0; i < _assignment.variableNames.size(); ++i) + newStatements.emplace_back(Assignment{ + _assignment.location, + {_assignment.variableNames[i]}, + make_shared<Expression>(Identifier{ + _assignment.location, + variableReplacements.at(_function.returnVariables[i].name) + }) + }); + }, + [&](VariableDeclaration& _varDecl) { - auto location = locationOf(arg); - string var = newName(i < _nameHints.size() ? _nameHints[i] : ""); - prefix.emplace(prefix.begin(), VariableDeclaration{ - location, - {{TypedName{location, var, i < _types.size() ? _types[i] : ""}}}, - make_shared<Expression>(std::move(arg)) - }); - arg = Identifier{location, var}; + for (size_t i = 0; i < _varDecl.variables.size(); ++i) + newStatements.emplace_back(VariableDeclaration{ + _varDecl.location, + {std::move(_varDecl.variables[i])}, + make_shared<Expression>(Identifier{ + _varDecl.location, + variableReplacements.at(_function.returnVariables[i].name) + }) + }); } - } - m_statementsToPrefix += std::move(prefix); -} - -vector<Statement> InlineModifier::visitRecursively(Expression& _expression) -{ - vector<Statement> saved; - saved.swap(m_statementsToPrefix); - visit(_expression); - saved.swap(m_statementsToPrefix); - return saved; + // nothing to be done for expression statement + }, _statement); + return newStatements; } string InlineModifier::newName(string const& _prefix) @@ -235,13 +174,6 @@ string InlineModifier::newName(string const& _prefix) return m_nameDispenser.newName(_prefix); } -Expression InlineModifier::noop(SourceLocation const& _location) -{ - return FunctionalInstruction{_location, solidity::Instruction::POP, { - Literal{_location, assembly::LiteralKind::Number, "0", ""} - }}; -} - Statement BodyCopier::operator()(VariableDeclaration const& _varDecl) { for (auto const& var: _varDecl.variables) diff --git a/libyul/optimiser/FullInliner.h b/libyul/optimiser/FullInliner.h index 8112fb4b..b69350a6 100644 --- a/libyul/optimiser/FullInliner.h +++ b/libyul/optimiser/FullInliner.h @@ -42,29 +42,31 @@ class NameCollector; /** - * Optimiser component that modifies an AST in place, inlining arbitrary functions. + * Optimiser component that modifies an AST in place, inlining functions. + * Expressions are expected to be split, i.e. the component will only inline + * function calls that are at the root of the expression and that only contains + * variables as arguments. More specifically, it will inline + * - let x1, ..., xn := f(a1, ..., am) + * - x1, ..., xn := f(a1, ..., am) + * f(a1, ..., am) * - * Code of the form + * The transform changes code of the form * * function f(a, b) -> c { ... } - * h(g(x(...), f(arg1(...), arg2(...)), y(...)), z(...)) + * let z := f(x, y) * - * is transformed into + * into * * function f(a, b) -> c { ... } * - * let z1 := z(...) let y1 := y(...) let a2 := arg2(...) let a1 := arg1(...) - * let c1 := 0 - * { code of f, with replacements: a -> a1, b -> a2, c -> c1, d -> d1 } - * h(g(x(...), c1, y1), z1) + * let f_a := x + * let f_b := y + * let f_c + * code of f, with replacements: a -> f_a, b -> f_b, c -> f_c + * let z := f_c * - * No temporary variable is created for expressions that are "movable" - * (i.e. they are "pure", have no side-effects and also do not depend on other code - * that might have side-effects). - * - * This component can only be used on sources with unique names and with hoisted functions, - * i.e. the root node has to be a block that itself contains a single block followed by all - * function definitions. + * Prerequisites: Disambiguator, Function Hoister + * More efficient if run after: Expression Splitter */ class FullInliner: public ASTModifier { @@ -79,6 +81,8 @@ public: FunctionDefinition& function(std::string _name) { return *m_functions.at(_name); } private: + void handleBlock(std::string const& _currentFunctionName, Block& _block); + /// The AST to be modified. The root block itself will not be modified, because /// we store pointers to functions. Block& m_ast; @@ -99,46 +103,15 @@ public: m_driver(_driver), m_nameDispenser(_nameDispenser) { } - ~InlineModifier() - { - assertThrow(m_statementsToPrefix.empty(), OptimizerException, ""); - } - - virtual void operator()(FunctionalInstruction&) override; - virtual void operator()(FunctionCall&) override; - virtual void operator()(ForLoop&) override; - virtual void operator()(Block& _block) override; - using ASTModifier::visit; - virtual void visit(Expression& _expression) override; - virtual void visit(Statement& _st) override; + virtual void operator()(Block& _block) override; private: - - /// Visits a list of expressions (usually an argument list to a function call) and tries - /// to inline them. If one of them is inlined, all right of it have to be moved to the front - /// (to keep the order of evaluation). If @a _moveToFront is true, all elements are moved - /// to the front. @a _nameHints and @_types are used for the newly created variables, but - /// both can be empty. - void visitArguments( - std::vector<Expression>& _arguments, - std::vector<std::string> const& _nameHints = {}, - std::vector<std::string> const& _types = {}, - bool _moveToFront = false - ); - - /// Visits an expression, but saves and restores the current statements to prefix and returns - /// the statements that should be prefixed for @a _expression. - std::vector<Statement> visitRecursively(Expression& _expression); + boost::optional<std::vector<Statement>> tryInlineStatement(Statement& _statement); + std::vector<Statement> performInline(Statement& _statement, FunctionCall& _funCall, FunctionDefinition& _function); std::string newName(std::string const& _prefix); - /// @returns an expression returning nothing. - Expression noop(SourceLocation const& _location); - - /// List of statements that should go in front of the currently visited AST element, - /// at the statement level. - std::vector<Statement> m_statementsToPrefix; std::string m_currentFunction; FullInliner& m_driver; NameDispenser& m_nameDispenser; diff --git a/test/libyul/YulOptimizerTest.cpp b/test/libyul/YulOptimizerTest.cpp index d1990edb..8e4771c8 100644 --- a/test/libyul/YulOptimizerTest.cpp +++ b/test/libyul/YulOptimizerTest.cpp @@ -130,7 +130,11 @@ bool YulOptimizerTest::run(ostream& _stream, string const& _linePrefix, bool con disambiguate(); (FunctionHoister{})(*m_ast); (FunctionGrouper{})(*m_ast); + NameDispenser nameDispenser; + nameDispenser.m_usedNames = NameCollector(*m_ast).names(); + ExpressionSplitter{nameDispenser}(*m_ast); FullInliner(*m_ast).run(); + ExpressionJoiner::run(*m_ast); } else if (m_optimizerStep == "mainFunction") { diff --git a/test/libyul/yulOptimizerTests/fullInliner/double_inline.yul b/test/libyul/yulOptimizerTests/fullInliner/double_inline.yul new file mode 100644 index 00000000..dd1c1f8a --- /dev/null +++ b/test/libyul/yulOptimizerTests/fullInliner/double_inline.yul @@ -0,0 +1,30 @@ +{ + function f(a) -> b, c { let x := mload(a) b := sload(x) c := 3 } + let a1 := calldataload(0) + let b3, c3 := f(a1) + let b4, c4 := f(c3) +} +// ---- +// fullInliner +// { +// { +// let f_a := calldataload(0) +// let f_b +// let f_c +// f_b := sload(mload(f_a)) +// f_c := 3 +// let b3 := f_b +// let f_a_1 := f_c +// let f_b_1 +// let f_c_1 +// f_b_1 := sload(mload(f_a_1)) +// f_c_1 := 3 +// let b4 := f_b_1 +// let c4 := f_c_1 +// } +// function f(a) -> b, c +// { +// b := sload(mload(a)) +// c := 3 +// } +// } diff --git a/test/libyul/yulOptimizerTests/fullInliner/inside_condition.yul b/test/libyul/yulOptimizerTests/fullInliner/inside_condition.yul index 76b6054b..00bb6577 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/inside_condition.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/inside_condition.yul @@ -12,14 +12,12 @@ // fullInliner // { // { -// let _1 := mload(0) +// let _2 := mload(0) // let f_a := mload(1) // let f_r -// { -// f_a := mload(f_a) -// f_r := add(f_a, calldatasize()) -// } -// if gt(f_r, _1) +// f_a := mload(f_a) +// f_r := add(f_a, calldatasize()) +// if gt(f_r, _2) // { // sstore(0, 2) // } diff --git a/test/libyul/yulOptimizerTests/fullInliner/move_up_rightwards_argument.yul b/test/libyul/yulOptimizerTests/fullInliner/move_up_rightwards_argument.yul index e1def585..f3d0b286 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/move_up_rightwards_argument.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/move_up_rightwards_argument.yul @@ -9,16 +9,17 @@ // fullInliner // { // { -// let _1 := mload(5) -// let f_c := mload(4) -// let f_b := mload(3) +// let _2 := mload(5) +// let _4 := mload(4) +// let _6 := mload(3) // let f_a := mload(2) +// let f_b := _6 +// let f_c := _4 // let f_x -// { -// f_x := add(f_a, f_b) -// f_x := mul(f_x, f_c) -// } -// let y := add(mload(1), add(f_x, _1)) +// f_x := add(f_a, f_b) +// f_x := mul(f_x, f_c) +// let _10 := add(f_x, _2) +// let y := add(mload(1), _10) // } // function f(a, b, c) -> x // { diff --git a/test/libyul/yulOptimizerTests/fullInliner/multi_fun.yul b/test/libyul/yulOptimizerTests/fullInliner/multi_fun.yul index 94bbe5dc..40397a43 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/multi_fun.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/multi_fun.yul @@ -7,21 +7,17 @@ // fullInliner // { // { -// let g_c := 7 -// let f_a_1 := 3 -// let f_x_1 -// { -// f_x_1 := add(f_a_1, f_a_1) -// } +// let _1 := 7 +// let f_a := 3 +// let f_x +// f_x := add(f_a, f_a) +// let g_b := f_x +// let g_c := _1 // let g_y -// { -// let g_f_a := f_x_1 -// let g_f_x -// { -// g_f_x := add(g_f_a, g_f_a) -// } -// g_y := mul(mload(g_c), g_f_x) -// } +// let g_f_a_1 := g_b +// let g_f_x_1 +// g_f_x_1 := add(g_f_a_1, g_f_a_1) +// g_y := mul(mload(g_c), g_f_x_1) // let y_1 := g_y // } // function f(a) -> x @@ -30,11 +26,9 @@ // } // function g(b, c) -> y // { -// let f_a := b -// let f_x -// { -// f_x := add(f_a, f_a) -// } -// y := mul(mload(c), f_x) +// let f_a_1 := b +// let f_x_1 +// f_x_1 := add(f_a_1, f_a_1) +// y := mul(mload(c), f_x_1) // } // } diff --git a/test/libyul/yulOptimizerTests/fullInliner/multi_return.yul b/test/libyul/yulOptimizerTests/fullInliner/multi_return.yul index f3c5b0ee..eebdec38 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/multi_return.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/multi_return.yul @@ -1,17 +1,22 @@ -// The full inliner currently does not work with -// functions returning multiple values. { function f(a) -> x, y { x := mul(a, a) y := add(a, x) } - let a, b := f(mload(0)) + let r, s := f(mload(0)) + mstore(r, s) } // ---- // fullInliner // { // { -// let a_1, b := f(mload(0)) +// let f_a := mload(0) +// let f_x +// let f_y +// f_x := mul(f_a, f_a) +// f_y := add(f_a, f_x) +// let r := f_x +// mstore(r, f_y) // } // function f(a) -> x, y // { diff --git a/test/libyul/yulOptimizerTests/fullInliner/no_return.yul b/test/libyul/yulOptimizerTests/fullInliner/no_return.yul index 53fe3527..3708c557 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/no_return.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/no_return.yul @@ -9,11 +9,7 @@ // { // { // let f_a := mload(0) -// { -// sstore(f_a, f_a) -// } -// { -// } +// sstore(f_a, f_a) // } // function f(a) // { diff --git a/test/libyul/yulOptimizerTests/fullInliner/not_inside_for.yul b/test/libyul/yulOptimizerTests/fullInliner/not_inside_for.yul new file mode 100644 index 00000000..44fc7b21 --- /dev/null +++ b/test/libyul/yulOptimizerTests/fullInliner/not_inside_for.yul @@ -0,0 +1,43 @@ +{ + for { let x := f(0) } f(x) { x := f(x) } + { + let t := f(x) + } + function f(a) -> r { + sstore(a, 0) + r := a + } +} +// ---- +// fullInliner +// { +// { +// for { +// let f_a := 0 +// let f_r +// sstore(f_a, 0) +// f_r := f_a +// let x := f_r +// } +// f(x) +// { +// let f_a_1 := x +// let f_r_1 +// sstore(f_a_1, 0) +// f_r_1 := f_a_1 +// x := f_r_1 +// } +// { +// let f_a_2 := x +// let f_r_2 +// sstore(f_a_2, 0) +// f_r_2 := f_a_2 +// let t := f_r_2 +// } +// } +// function f(a) -> r +// { +// sstore(a, 0) +// r := a +// } +// } diff --git a/test/libyul/yulOptimizerTests/fullInliner/pop_result.yul b/test/libyul/yulOptimizerTests/fullInliner/pop_result.yul index 3883c67c..cd9e2746 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/pop_result.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/pop_result.yul @@ -1,4 +1,6 @@ -// This tests that `pop(r)` is removed. +// An earlier version of the inliner produced +// pop(...) statements and explicitly removed them. +// This used to test that they are removed. { function f(a) -> x { let r := mul(a, a) @@ -13,12 +15,9 @@ // let _1 := 2 // let f_a := 7 // let f_x -// { -// let f_r := mul(f_a, f_a) -// f_x := add(f_r, f_r) -// } -// { -// } +// let f_r := mul(f_a, f_a) +// f_x := add(f_r, f_r) +// pop(add(f_x, _1)) // } // function f(a) -> x // { diff --git a/test/libyul/yulOptimizerTests/fullInliner/simple.yul b/test/libyul/yulOptimizerTests/fullInliner/simple.yul index dd1a4e0a..fcdf453b 100644 --- a/test/libyul/yulOptimizerTests/fullInliner/simple.yul +++ b/test/libyul/yulOptimizerTests/fullInliner/simple.yul @@ -9,14 +9,12 @@ // fullInliner // { // { -// let _1 := mload(7) +// let _2 := mload(7) // let f_a := sload(mload(2)) // let f_x -// { -// let f_r := mul(f_a, f_a) -// f_x := add(f_r, f_r) -// } -// let y := add(f_x, _1) +// let f_r := mul(f_a, f_a) +// f_x := add(f_r, f_r) +// let y := add(f_x, _2) // } // function f(a) -> x // { |