Skip to content

feat: Convert verbatim into a single builtin with literal arguments #16024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions docs/yul.rst
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ declaration.
Functions can be referenced already before their declaration (if they are visible).

As an exception to the general scoping rule, the scope of the "init" part of the for-loop
(the first block) extends across all other parts of the for loop.
(the first block) extends across all other parts of the for-loop.
This means that variables (and functions) declared in the init part (but not inside a
block inside the init part) are visible in all other parts of the for-loop.

Expand Down Expand Up @@ -1050,23 +1050,36 @@ the additional optimiser steps will be run on it.
verbatim
^^^^^^^^

The set of ``verbatim...`` builtin functions lets you create bytecode for opcodes
The ``verbatim`` builtin function lets you create bytecode for opcodes
that are not known to the Yul compiler. It also allows you to create
bytecode sequences that will not be modified by the optimizer.

The functions are ``verbatim_<n>i_<m>o("<data>", ...)``, where
This function can be used in two ways:

- ``n`` is a decimal between 0 and 99 that specifies the number of input stack slots / variables
- ``m`` is a decimal between 0 and 99 that specifies the number of output stack slots / variables
- ``data`` is a string literal that contains the sequence of bytes
1. (Recommended) Using the newer syntax ``verbatim(n, m, "<data>", ...)``, where:

- ``n`` is a literal number between 0 and 99 that specifies the number of input stack slots / variables
- ``m`` is a literal number between 0 and 99 that specifies the number of output stack slots / variables
- ``data`` is a string literal that contains the sequence of bytes

2. (Legacy) Using the older syntax ``verbatim_<n>i_<m>o("<data>", ...)``, where:

- ``n`` is a decimal between 0 and 99 that specifies the number of input stack slots / variables
- ``m`` is a decimal between 0 and 99 that specifies the number of output stack slots / variables
- ``data`` is a string literal that contains the sequence of bytes

.. note::

The legacy ``verbatim_<n>i_<m>o`` functions are deprecated and will be removed in a future version.
Please use the new ``verbatim`` function.

If you for example want to define a function that multiplies the input
by two, without the optimizer touching the constant two, you can use

.. code-block:: yul

let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)
let double := verbatim(1, 1, hex"600202", x)

This code will result in a ``dup1`` opcode to retrieve ``x``
(the optimizer might directly reuse result of the
Expand Down Expand Up @@ -1344,13 +1357,6 @@ Complete ERC20 Example
executeTransfer(from, to, amount)
}

function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}


/* ---------- calldata decoding functions ----------- */
function selector() -> s {
Expand Down
40 changes: 40 additions & 0 deletions libyul/backends/evm/EVMDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,46 @@ std::vector<std::optional<BuiltinFunctionForEVM>> createBuiltins(langutil::EVMVe
return createFunction(_name, _params, _returns, _sideEffects, _controlFlowSideEffects, std::move(_literalArguments), std::move(_generateCode));
};

builtins.emplace_back(createIfObjectAccess(
"verbatim",
3,
0,
SideEffects::worst(),
ControlFlowSideEffects::worst(), // Worst control flow side effects because verbatim can do anything.
{LiteralKind::Number, LiteralKind::Number, LiteralKind::String},
[](
FunctionCall const& _call,
AbstractAssembly& _assembly,
BuiltinContext&
) {
yulAssert(_call.arguments.size() == 3, "");

// Get the number of inputs from the first argument
Literal const* argsLiteral = std::get_if<Literal>(&_call.arguments[0]);
yulAssert(argsLiteral, "First argument must be a literal");
size_t arguments = static_cast<size_t>(argsLiteral->value.value());

// Get the number of outputs from the second argument
Literal const* retsLiteral = std::get_if<Literal>(&_call.arguments[1]);
yulAssert(retsLiteral, "Second argument must be a literal");
size_t returnVariables = static_cast<size_t>(retsLiteral->value.value());

// Verify that arguments and returnVariables are in the allowed range
yulAssert(arguments <= EVMDialect::verbatimMaxInputSlots, "Too many verbatim input arguments");
yulAssert(returnVariables <= EVMDialect::verbatimMaxOutputSlots, "Too many verbatim return values");

// Get the bytecode from the third argument
Literal const* bytecodeStr = std::get_if<Literal>(&_call.arguments[2]);
yulAssert(bytecodeStr, "Third argument must be a literal");

_assembly.appendVerbatim(
asBytes(formatLiteral(*bytecodeStr)),
arguments,
returnVariables
);
}
));

builtins.emplace_back(createIfObjectAccess("linkersymbol", 1, 1, SideEffects{}, ControlFlowSideEffects{}, {LiteralKind::String}, [](
FunctionCall const& _call,
AbstractAssembly& _assembly,
Expand Down
11 changes: 11 additions & 0 deletions test/cmdlineTests/yul_verbatim_single/input.yul
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
let x := 2
let y := sub(x, 2)
// Using the new verbatim builtin with literal args instead of verbatim_2i_1o
let r := verbatim(0, 1, "def") // No input arguments, 1 output value
let t := verbatim(2, 1, "abc", x, y) // 2 input arguments, 1 output value
sstore(t, x)
verbatim(0, 0, "xyz") // No input arguments, no output values
// more than 32 bytes
verbatim(0, 0, hex"01020304050607090001020304050607090001020304050607090001020102030405060709000102030405060709000102030405060709000102")
}
12 changes: 12 additions & 0 deletions test/cmdlineTests/yul_verbatim_single/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"contracts": {},
"sourceList": [
"input.yul"
],
"sources": {
"input.yul": {
"id": 0
}
},
"version": "<VERSION REMOVED>"
}
7 changes: 7 additions & 0 deletions test/libyul/yulSyntaxTests/verbatim_single.yul
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
let x := verbatim(2, 1, "abc")
verbatim(0, 0, "xyz")
}
// ====
// dialect: evm
// ----
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
// Test what happens when we provide fewer function arguments than specified in the first parameter
let x := 2
let t := verbatim(2, 1, "abc", x) // Expecting 2 inputs but only given 1
}
// ====
// dialect: evm
// ----
// TypeError 4323: (121-142): Function expects 5 arguments but got 4.
11 changes: 11 additions & 0 deletions test/libyul/yulSyntaxTests/verbatim_single_invalid_args.yul
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
// Using variables as arguments which should be literals
let n := 2
let m := 1
let c := "abc"
let x := verbatim(n, m, c)
}
// ====
// dialect: evm
// ----
// TypeError 9114: (92-112): Function expects direct literals as arguments.
7 changes: 7 additions & 0 deletions test/libyul/yulSyntaxTests/verbatim_single_invalid_range.yul
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
let x := verbatim(100, 1, "abc")
}
// ====
// dialect: evm
// ----
// Type of call argument too large (currently disallowed).
11 changes: 11 additions & 0 deletions test/libyul/yulSyntaxTests/verbatim_single_valid.yul
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
// Test that we can provide the correct number of arguments
let x := 2
let y := 3
let t := verbatim(2, 1, "abc", x, y) // Correct: 2 inputs as specified
let z := verbatim(0, 1, "def") // Correct: 0 inputs as specified
verbatim(0, 0, "xyz") // Correct: 0 inputs, 0 outputs
}
// ====
// dialect: evm
// ----