mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
1443 lines
65 KiB
ReStructuredText
1443 lines
65 KiB
ReStructuredText
.. _yul:
|
|
|
|
###
|
|
Yul
|
|
###
|
|
|
|
.. index:: ! assembly, ! asm, ! evmasm, ! yul, julia, iulia
|
|
|
|
Yul (previously also called JULIA or IULIA) is an intermediate language that can be
|
|
compiled to bytecode for different backends.
|
|
|
|
Support for EVM 1.0, EVM 1.5 and Ewasm is planned, and it is designed to
|
|
be a usable common denominator of all three
|
|
platforms. It can already be used in stand-alone mode and
|
|
for "inline assembly" inside Solidity
|
|
and there is an experimental implementation of the Solidity compiler
|
|
that uses Yul as an intermediate language. Yul is a good target for
|
|
high-level optimisation stages that can benefit all target platforms equally.
|
|
|
|
Motivation and High-level Description
|
|
=====================================
|
|
|
|
The design of Yul tries to achieve several goals:
|
|
|
|
1. Programs written in Yul should be readable, even if the code is generated by a compiler from Solidity or another high-level language.
|
|
2. Control flow should be easy to understand to help in manual inspection, formal verification and optimization.
|
|
3. The translation from Yul to bytecode should be as straightforward as possible.
|
|
4. Yul should be suitable for whole-program optimization.
|
|
|
|
In order to achieve the first and second goal, Yul provides high-level constructs
|
|
like ``for`` loops, ``if`` and ``switch`` statements and function calls. These should
|
|
be sufficient for adequately representing the control flow for assembly programs.
|
|
Therefore, no explicit statements for ``SWAP``, ``DUP``, ``JUMPDEST``, ``JUMP`` and ``JUMPI``
|
|
are provided, because the first two obfuscate the data flow
|
|
and the last two obfuscate control flow. Furthermore, functional statements of
|
|
the form ``mul(add(x, y), 7)`` are preferred over pure opcode statements like
|
|
``7 y x add mul`` because in the first form, it is much easier to see which
|
|
operand is used for which opcode.
|
|
|
|
Even though it was designed for stack machines, Yul does not expose the complexity of the stack itself.
|
|
The programmer or auditor should not have to worry about the stack.
|
|
|
|
The third goal is achieved by compiling the
|
|
higher level constructs to bytecode in a very regular way.
|
|
The only non-local operation performed
|
|
by the assembler is name lookup of user-defined identifiers (functions, variables, ...)
|
|
and cleanup of local variables from the stack.
|
|
|
|
To avoid confusions between concepts like values and references,
|
|
Yul is statically typed. At the same time, there is a default type
|
|
(usually the integer word of the target machine) that can always
|
|
be omitted to help readability.
|
|
|
|
To keep the language simple and flexible, Yul does not have
|
|
any built-in operations, functions or types in its pure form.
|
|
These are added together with their semantics when specifying a dialect of Yul,
|
|
which allows specializing Yul to the requirements of different
|
|
target platforms and feature sets.
|
|
|
|
Currently, there is only one specified dialect of Yul. This dialect uses
|
|
the EVM opcodes as builtin functions
|
|
(see below) and defines only the type ``u256``, which is the native 256-bit
|
|
type of the EVM. Because of that, we will not provide types in the examples below.
|
|
|
|
|
|
Simple Example
|
|
==============
|
|
|
|
The following example program is written in the EVM dialect and computes exponentiation.
|
|
It can be compiled using ``solc --strict-assembly``. The builtin functions
|
|
``mul`` and ``div`` compute product and division, respectively.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
function power(base, exponent) -> result
|
|
{
|
|
switch exponent
|
|
case 0 { result := 1 }
|
|
case 1 { result := base }
|
|
default
|
|
{
|
|
result := power(mul(base, base), div(exponent, 2))
|
|
switch mod(exponent, 2)
|
|
case 1 { result := mul(base, result) }
|
|
}
|
|
}
|
|
}
|
|
|
|
It is also possible to implement the same function using a for-loop
|
|
instead of with recursion. Here, ``lt(a, b)`` computes whether ``a`` is less than ``b``.
|
|
less-than comparison.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
function power(base, exponent) -> result
|
|
{
|
|
result := 1
|
|
for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
|
|
{
|
|
result := mul(result, base)
|
|
}
|
|
}
|
|
}
|
|
|
|
At the :ref:`end of the section <erc20yul>`, a complete implementation of
|
|
the ERC-20 standard can be found.
|
|
|
|
|
|
|
|
Stand-Alone Usage
|
|
=================
|
|
|
|
You can use Yul in its stand-alone form in the EVM dialect using the Solidity compiler.
|
|
This will use the :ref:`Yul object notation <yul-object>` so that it is possible to refer
|
|
to code as data to deploy contracts. This Yul mode is available for the commandline compiler
|
|
(use ``--strict-assembly``) and for the :ref:`standard-json interface <compiler-api>`:
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"language": "Yul",
|
|
"sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
|
|
"settings": {
|
|
"outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
|
|
"optimizer": { "enabled": true, "details": { "yul": true } }
|
|
}
|
|
}
|
|
|
|
.. warning::
|
|
|
|
Yul is in active development and bytecode generation is only fully implemented for the EVM dialect of Yul
|
|
with EVM 1.0 as target.
|
|
|
|
|
|
Informal Description of Yul
|
|
===========================
|
|
|
|
In the following, we will talk about each individual aspect
|
|
of the Yul language. In examples, we will use the default EVM dialect.
|
|
|
|
Syntax
|
|
------
|
|
|
|
Yul parses comments, literals and identifiers in the same way as Solidity,
|
|
so you can e.g. use ``//`` and ``/* */`` to denote comments.
|
|
There is one exception: Identifiers in Yul can contain dots: ``.``.
|
|
|
|
Yul can specify "objects" that consist of code, data and sub-objects.
|
|
Please see :ref:`Yul Objects <yul-object>` below for details on that.
|
|
In this section, we are only concerned with the code part of such an object.
|
|
This code part always consists of a curly-braces
|
|
delimited block. Most tools support specifying just a code block
|
|
where an object is expected.
|
|
|
|
Inside a code block, the following elements can be used
|
|
(see the later sections for more details):
|
|
|
|
- literals, i.e. ``0x123``, ``42`` or ``"abc"`` (strings up to 32 characters)
|
|
- calls to builtin functions, e.g. ``add(1, mload(0))``
|
|
- variable declarations, e.g. ``let x := 7``, ``let x := add(y, 3)`` or ``let x`` (initial value of 0 is assigned)
|
|
- identifiers (variables), e.g. ``add(3, x)``
|
|
- assignments, e.g. ``x := add(y, 3)``
|
|
- blocks where local variables are scoped inside, e.g. ``{ let x := 3 { let y := add(x, 1) } }``
|
|
- if statements, e.g. ``if lt(a, b) { sstore(0, 1) }``
|
|
- switch statements, e.g. ``switch mload(0) case 0 { revert() } default { mstore(0, 1) }``
|
|
- for loops, e.g. ``for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }``
|
|
- function definitions, e.g. ``function f(a, b) -> c { c := add(a, b) }```
|
|
|
|
Multiple syntactical elements can follow each other simply separated by
|
|
whitespace, i.e. there is no terminating ``;`` or newline required.
|
|
|
|
Literals
|
|
--------
|
|
|
|
As literals, you can use integer constants in decimal or hexadecimal notation
|
|
or strings as ASCII (`"abc"`) or HEX strings (`hex"616263"`) of up to
|
|
32 bytes length.
|
|
|
|
When compiling for the EVM, this will be translated into an
|
|
appropriate ``PUSHi`` instruction. In the following example,
|
|
``3`` and ``2`` are added resulting in 5 and then the
|
|
bitwise ``and`` with the string "abc" is computed.
|
|
The final value is assigned to a local variable called ``x``.
|
|
|
|
Strings are stored left-aligned and cannot be longer than 32 bytes.
|
|
The limit does not apply to string literals passed to builtin functions that require
|
|
literal arguments (e.g. ``setimmutable`` or ``loadimmutable``). Those strings never end up in the
|
|
generated bytecode.
|
|
|
|
.. code-block:: yul
|
|
|
|
let x := and("abc", add(3, 2))
|
|
|
|
Unless it is the default type, the type of a literal
|
|
has to be specified after a colon:
|
|
|
|
.. code-block:: yul
|
|
|
|
// This will not compile (u32 and u256 type not implemented yet)
|
|
let x := and("abc":u32, add(3:u256, 2:u256))
|
|
|
|
|
|
Function Calls
|
|
--------------
|
|
|
|
Both built-in and user-defined functions (see below) can be called
|
|
in the same way as shown in the previous example.
|
|
If the function returns a single value, it can be directly used
|
|
inside an expression again. If it returns multiple values,
|
|
they have to be assigned to local variables.
|
|
|
|
.. code-block:: yul
|
|
|
|
function f(x, y) -> a, b { /* ... */ }
|
|
mstore(0x80, add(mload(0x80), 3))
|
|
// Here, the user-defined function `f` returns two values.
|
|
let x, y := f(1, mload(0))
|
|
|
|
For built-in functions of the EVM, functional expressions
|
|
can be directly translated to a stream of opcodes:
|
|
You just read the expression from right to left to obtain the
|
|
opcodes. In the case of the first line in the example, this
|
|
is ``PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE``.
|
|
|
|
For calls to user-defined functions, the arguments are also
|
|
put on the stack from right to left and this is the order
|
|
in which argument lists are evaluated. The return values,
|
|
though, are expected on the stack from left to right,
|
|
i.e. in this example, ``y`` is on top of the stack and ``x``
|
|
is below it.
|
|
|
|
Variable Declarations
|
|
---------------------
|
|
|
|
You can use the ``let`` keyword to declare variables.
|
|
A variable is only visible inside the
|
|
``{...}``-block it was defined in. When compiling to the EVM,
|
|
a new stack slot is created that is reserved
|
|
for the variable and automatically removed again when the end of the block
|
|
is reached. You can provide an initial value for the variable.
|
|
If you do not provide a value, the variable will be initialized to zero.
|
|
|
|
Since variables are stored on the stack, they do not directly
|
|
influence memory or storage, but they can be used as pointers
|
|
to memory or storage locations in the built-in functions
|
|
``mstore``, ``mload``, ``sstore`` and ``sload``.
|
|
Future dialects might introduce specific types for such pointers.
|
|
|
|
When a variable is referenced, its current value is copied.
|
|
For the EVM, this translates to a ``DUP`` instruction.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
let zero := 0
|
|
let v := calldataload(zero)
|
|
{
|
|
let y := add(sload(v), 1)
|
|
v := y
|
|
} // y is "deallocated" here
|
|
sstore(v, zero)
|
|
} // v and zero are "deallocated" here
|
|
|
|
|
|
If the declared variable should have a type different from the default type,
|
|
you denote that following a colon. You can also declare multiple
|
|
variables in one statement when you assign from a function call
|
|
that returns multiple values.
|
|
|
|
.. code-block:: yul
|
|
|
|
// This will not compile (u32 and u256 type not implemented yet)
|
|
{
|
|
let zero:u32 := 0:u32
|
|
let v:u256, t:u32 := f()
|
|
let x, y := g()
|
|
}
|
|
|
|
Depending on the optimiser settings, the compiler can free the stack slots
|
|
already after the variable has been used for
|
|
the last time, even though it is still in scope.
|
|
|
|
|
|
Assignments
|
|
-----------
|
|
|
|
Variables can be assigned to after their definition using the
|
|
``:=`` operator. It is possible to assign multiple
|
|
variables at the same time. For this, the number and types of the
|
|
values have to match.
|
|
If you want to assign the values returned from a function that has
|
|
multiple return parameters, you have to provide multiple variables.
|
|
The same variable may not occur multiple times on the left-hand side of
|
|
an assignment, e.g. ``x, x := f()`` is invalid.
|
|
|
|
.. code-block:: yul
|
|
|
|
let v := 0
|
|
// re-assign v
|
|
v := 2
|
|
let t := add(v, 2)
|
|
function f() -> a, b { }
|
|
// assign multiple values
|
|
v, t := f()
|
|
|
|
|
|
If
|
|
--
|
|
|
|
The if statement can be used for conditionally executing code.
|
|
No "else" block can be defined. Consider using "switch" instead (see below) if
|
|
you need multiple alternatives.
|
|
|
|
.. code-block:: yul
|
|
|
|
if lt(calldatasize(), 4) { revert(0, 0) }
|
|
|
|
The curly braces for the body are required.
|
|
|
|
Switch
|
|
------
|
|
|
|
You can use a switch statement as an extended version of the if statement.
|
|
It takes the value of an expression and compares it to several literal constants.
|
|
The branch corresponding to the matching constant is taken.
|
|
Contrary to other programming languages, for safety reasons, control flow does
|
|
not continue from one case to the next. There can be a fallback or default
|
|
case called ``default`` which is taken if none of the literal constants matches.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
let x := 0
|
|
switch calldataload(4)
|
|
case 0 {
|
|
x := calldataload(0x24)
|
|
}
|
|
default {
|
|
x := calldataload(0x44)
|
|
}
|
|
sstore(0, div(x, 2))
|
|
}
|
|
|
|
The list of cases is not enclosed by curly braces, but the body of a
|
|
case does require them.
|
|
|
|
Loops
|
|
-----
|
|
|
|
Yul supports for-loops which consist of
|
|
a header containing an initializing part, a condition, a post-iteration
|
|
part and a body. The condition has to be an expression, while
|
|
the other three are blocks. If the initializing part
|
|
declares any variables at the top level, the scope of these variables extends to all other
|
|
parts of the loop.
|
|
|
|
The ``break`` and ``continue`` statements can be used in the body to exit the loop
|
|
or skip to the post-part, respectively.
|
|
|
|
The following example computes the sum of an area in memory.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
let x := 0
|
|
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
|
|
x := add(x, mload(i))
|
|
}
|
|
}
|
|
|
|
For loops can also be used as a replacement for while loops:
|
|
Simply leave the initialization and post-iteration parts empty.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
let x := 0
|
|
let i := 0
|
|
for { } lt(i, 0x100) { } { // while(i < 0x100)
|
|
x := add(x, mload(i))
|
|
i := add(i, 0x20)
|
|
}
|
|
}
|
|
|
|
Function Declarations
|
|
---------------------
|
|
|
|
Yul allows the definition of functions. These should not be confused with functions
|
|
in Solidity since they are never part of an external interface of a contract and
|
|
are part of a namespace separate from the one for Solidity functions.
|
|
|
|
For the EVM, Yul functions take their
|
|
arguments (and a return PC) from the stack and also put the results onto the
|
|
stack. User-defined functions and built-in functions are called in exactly the same way.
|
|
|
|
Functions can be defined anywhere and are visible in the block they are
|
|
declared in. Inside a function, you cannot access local variables
|
|
defined outside of that function.
|
|
|
|
Functions declare parameters and return variables, similar to Solidity.
|
|
To return a value, you assign it to the return variable(s).
|
|
|
|
If you call a function that returns multiple values, you have to assign
|
|
them to multiple variables using ``a, b := f(x)`` or ``let a, b := f(x)``.
|
|
|
|
The ``leave`` statement can be used to exit the current function. It
|
|
works like the ``return`` statement in other languages just that it does
|
|
not take a value to return, it just exits the functions and the function
|
|
will return whatever values are currently assigned to the return variable(s).
|
|
|
|
Note that the EVM dialect has a built-in function called ``return`` that
|
|
quits the full execution context (internal message call) and not just
|
|
the current yul function.
|
|
|
|
The following example implements the power function by square-and-multiply.
|
|
|
|
.. code-block:: yul
|
|
|
|
{
|
|
function power(base, exponent) -> result {
|
|
switch exponent
|
|
case 0 { result := 1 }
|
|
case 1 { result := base }
|
|
default {
|
|
result := power(mul(base, base), div(exponent, 2))
|
|
switch mod(exponent, 2)
|
|
case 1 { result := mul(base, result) }
|
|
}
|
|
}
|
|
}
|
|
|
|
Specification of Yul
|
|
====================
|
|
|
|
This chapter describes Yul code formally. Yul code is usually placed inside Yul objects,
|
|
which are explained in their own chapter.
|
|
|
|
.. code-block:: none
|
|
|
|
Block = '{' Statement* '}'
|
|
Statement =
|
|
Block |
|
|
FunctionDefinition |
|
|
VariableDeclaration |
|
|
Assignment |
|
|
If |
|
|
Expression |
|
|
Switch |
|
|
ForLoop |
|
|
BreakContinue |
|
|
Leave
|
|
FunctionDefinition =
|
|
'function' Identifier '(' TypedIdentifierList? ')'
|
|
( '->' TypedIdentifierList )? Block
|
|
VariableDeclaration =
|
|
'let' TypedIdentifierList ( ':=' Expression )?
|
|
Assignment =
|
|
IdentifierList ':=' Expression
|
|
Expression =
|
|
FunctionCall | Identifier | Literal
|
|
If =
|
|
'if' Expression Block
|
|
Switch =
|
|
'switch' Expression ( Case+ Default? | Default )
|
|
Case =
|
|
'case' Literal Block
|
|
Default =
|
|
'default' Block
|
|
ForLoop =
|
|
'for' Block Expression Block Block
|
|
BreakContinue =
|
|
'break' | 'continue'
|
|
Leave = 'leave'
|
|
FunctionCall =
|
|
Identifier '(' ( Expression ( ',' Expression )* )? ')'
|
|
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]*
|
|
IdentifierList = Identifier ( ',' Identifier)*
|
|
TypeName = Identifier
|
|
TypedIdentifierList = Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )*
|
|
Literal =
|
|
(NumberLiteral | StringLiteral | TrueLiteral | FalseLiteral) ( ':' TypeName )?
|
|
NumberLiteral = HexNumber | DecimalNumber
|
|
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
|
|
TrueLiteral = 'true'
|
|
FalseLiteral = 'false'
|
|
HexNumber = '0x' [0-9a-fA-F]+
|
|
DecimalNumber = [0-9]+
|
|
|
|
|
|
Restrictions on the Grammar
|
|
---------------------------
|
|
|
|
Apart from those directly imposed by the grammar, the following
|
|
restrictions apply:
|
|
|
|
Switches must have at least one case (including the default case).
|
|
All case values need to have the same type and distinct values.
|
|
If all possible values of the expression type are covered, a default case is
|
|
not allowed (i.e. a switch with a ``bool`` expression that has both a
|
|
true and a false case do not allow a default case).
|
|
|
|
Every expression evaluates to zero or more values. Identifiers and Literals
|
|
evaluate to exactly
|
|
one value and function calls evaluate to a number of values equal to the
|
|
number of return variables of the function called.
|
|
|
|
In variable declarations and assignments, the right-hand-side expression
|
|
(if present) has to evaluate to a number of values equal to the number of
|
|
variables on the left-hand-side.
|
|
This is the only situation where an expression evaluating
|
|
to more than one value is allowed.
|
|
The same variable name cannot occur more than once in the left-hand-side of
|
|
an assignment or variable declaration.
|
|
|
|
Expressions that are also statements (i.e. at the block level) have to
|
|
evaluate to zero values.
|
|
|
|
In all other situations, expressions have to evaluate to exactly one value.
|
|
|
|
The ``continue`` and ``break`` statements can only be used inside loop bodies
|
|
and have to be in the same function as the loop (or both have to be at the
|
|
top level). The ``continue`` and ``break`` statements cannot be used
|
|
in other parts of a loop, not even when it is scoped inside a second loop's body.
|
|
|
|
The condition part of the for-loop has to evaluate to exactly one value.
|
|
|
|
The ``leave`` statement can only be used inside a function.
|
|
|
|
Functions cannot be defined anywhere inside for loop init blocks.
|
|
|
|
Literals cannot be larger than their type. The largest type defined is 256-bit wide.
|
|
|
|
During assignments and function calls, the types of the respective values have to match.
|
|
There is no implicit type conversion. Type conversion in general can only be achieved
|
|
if the dialect provides an appropriate built-in function that takes a value of one
|
|
type and returns a value of a different type.
|
|
|
|
Scoping Rules
|
|
-------------
|
|
|
|
Scopes in Yul are tied to Blocks (exceptions are functions and the for loop
|
|
as explained below) and all declarations
|
|
(``FunctionDefinition``, ``VariableDeclaration``)
|
|
introduce new identifiers into these scopes.
|
|
|
|
Identifiers are visible in
|
|
the block they are defined in (including all sub-nodes and sub-blocks).
|
|
|
|
As an exception, the scope of the "init" part of the or-loop
|
|
(the first block) extends across all other parts of the for loop.
|
|
This means that variables declared in the init part (but not inside a
|
|
block inside the init part) are visible in all other parts of the for-loop.
|
|
|
|
Identifiers declared in the other parts of the for loop respect the regular
|
|
syntactical scoping rules.
|
|
|
|
This means a for-loop of the form ``for { I... } C { P... } { B... }`` is equivalent
|
|
to ``{ I... for {} C { P... } { B... } }``.
|
|
|
|
|
|
The parameters and return parameters of functions are visible in the
|
|
function body and their names have to be distinct.
|
|
|
|
Variables can only be referenced after their declaration. In particular,
|
|
variables cannot be referenced in the right hand side of their own variable
|
|
declaration.
|
|
Functions can be referenced already before their declaration (if they are visible).
|
|
|
|
Shadowing is disallowed, i.e. you cannot declare an identifier at a point
|
|
where another identifier with the same name is also visible, even if it is
|
|
not accessible.
|
|
|
|
Inside functions, it is not possible to access a variable that was declared
|
|
outside of that function.
|
|
|
|
Formal Specification
|
|
--------------------
|
|
|
|
We formally specify Yul by providing an evaluation function E overloaded
|
|
on the various nodes of the AST. As builtin functions can have side effects,
|
|
E takes two state objects and the AST node and returns two new
|
|
state objects and a variable number of other values.
|
|
The two state objects are the global state object
|
|
(which in the context of the EVM is the memory, storage and state of the
|
|
blockchain) and the local state object (the state of local variables, i.e. a
|
|
segment of the stack in the EVM).
|
|
|
|
If the AST node is a statement, E returns the two state objects and a "mode",
|
|
which is used for the ``break``, ``continue`` and ``leave`` statements.
|
|
If the AST node is an expression, E returns the two state objects and
|
|
as many values as the expression evaluates to.
|
|
|
|
|
|
The exact nature of the global state is unspecified for this high level
|
|
description. The local state ``L`` is a mapping of identifiers ``i`` to values ``v``,
|
|
denoted as ``L[i] = v``.
|
|
|
|
For an identifier ``v``, let ``$v`` be the name of the identifier.
|
|
|
|
We will use a destructuring notation for the AST nodes.
|
|
|
|
.. code-block:: none
|
|
|
|
E(G, L, <{St1, ..., Stn}>: Block) =
|
|
let G1, L1, mode = E(G, L, St1, ..., Stn)
|
|
let L2 be a restriction of L1 to the identifiers of L
|
|
G1, L2, mode
|
|
E(G, L, St1, ..., Stn: Statement) =
|
|
if n is zero:
|
|
G, L, regular
|
|
else:
|
|
let G1, L1, mode = E(G, L, St1)
|
|
if mode is regular then
|
|
E(G1, L1, St2, ..., Stn)
|
|
otherwise
|
|
G1, L1, mode
|
|
E(G, L, FunctionDefinition) =
|
|
G, L, regular
|
|
E(G, L, <let var_1, ..., var_n := rhs>: VariableDeclaration) =
|
|
E(G, L, <var_1, ..., var_n := rhs>: Assignment)
|
|
E(G, L, <let var_1, ..., var_n>: VariableDeclaration) =
|
|
let L1 be a copy of L where L1[$var_i] = 0 for i = 1, ..., n
|
|
G, L1, regular
|
|
E(G, L, <var_1, ..., var_n := rhs>: Assignment) =
|
|
let G1, L1, v1, ..., vn = E(G, L, rhs)
|
|
let L2 be a copy of L1 where L2[$var_i] = vi for i = 1, ..., n
|
|
G, L2, regular
|
|
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
|
|
if n >= 1:
|
|
let G1, L, mode = E(G, L, i1, ..., in)
|
|
// mode has to be regular or leave due to the syntactic restrictions
|
|
if mode is leave then
|
|
G1, L1 restricted to variables of L, leave
|
|
otherwise
|
|
let G2, L2, mode = E(G1, L1, for {} condition post body)
|
|
G2, L2 restricted to variables of L, mode
|
|
else:
|
|
let G1, L1, v = E(G, L, condition)
|
|
if v is false:
|
|
G1, L1, regular
|
|
else:
|
|
let G2, L2, mode = E(G1, L, body)
|
|
if mode is break:
|
|
G2, L2, regular
|
|
otherwise if mode is leave:
|
|
G2, L2, leave
|
|
else:
|
|
G3, L3, mode = E(G2, L2, post)
|
|
if mode is leave:
|
|
G2, L3, leave
|
|
otherwise
|
|
E(G3, L3, for {} condition post body)
|
|
E(G, L, break: BreakContinue) =
|
|
G, L, break
|
|
E(G, L, continue: BreakContinue) =
|
|
G, L, continue
|
|
E(G, L, leave: Leave) =
|
|
G, L, leave
|
|
E(G, L, <if condition body>: If) =
|
|
let G0, L0, v = E(G, L, condition)
|
|
if v is true:
|
|
E(G0, L0, body)
|
|
else:
|
|
G0, L0, regular
|
|
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
|
|
E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
|
|
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
|
|
let G0, L0, v = E(G, L, condition)
|
|
// i = 1 .. n
|
|
// Evaluate literals, context doesn't matter
|
|
let _, _, v1 = E(G0, L0, l1)
|
|
...
|
|
let _, _, vn = E(G0, L0, ln)
|
|
if there exists smallest i such that vi = v:
|
|
E(G0, L0, sti)
|
|
else:
|
|
E(G0, L0, st')
|
|
|
|
E(G, L, <name>: Identifier) =
|
|
G, L, L[$name]
|
|
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
|
|
G1, L1, vn = E(G, L, argn)
|
|
...
|
|
G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
|
|
Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
|
|
Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
|
|
be the function of name $fname visible at the point of the call.
|
|
Let L' be a new local state such that
|
|
L'[$parami] = vi and L'[$reti] = 0 for all i.
|
|
Let G'', L'', mode = E(Gn, L', block)
|
|
G'', Ln, L''[$ret1], ..., L''[$retm]
|
|
E(G, L, l: StringLiteral) = G, L, utf8EncodeLeftAligned(l),
|
|
where utf8EncodeLeftAligned performs a UTF-8 encoding of l
|
|
and aligns it left into 32 bytes
|
|
E(G, L, n: HexNumber) = G, L, hex(n)
|
|
where hex is the hexadecimal decoding function
|
|
E(G, L, n: DecimalNumber) = G, L, dec(n),
|
|
where dec is the decimal decoding function
|
|
|
|
.. _opcodes:
|
|
|
|
EVM Dialect
|
|
-----------
|
|
|
|
The default dialect of Yul currently is the EVM dialect for the currently selected version of the EVM.
|
|
with a version of the EVM. The only type available in this dialect
|
|
is ``u256``, the 256-bit native type of the Ethereum Virtual Machine.
|
|
Since it is the default type of this dialect, it can be omitted.
|
|
|
|
The following table lists all builtin functions
|
|
(depending on the EVM version) and provides a short description of the
|
|
semantics of the function / opcode.
|
|
This document does not want to be a full description of the Ethereum virtual machine.
|
|
Please refer to a different document if you are interested in the precise semantics.
|
|
|
|
Opcodes marked with ``-`` do not return a result and all others return exactly one value.
|
|
Opcodes marked with ``F``, ``H``, ``B``, ``C`` or ``I`` are present since Frontier, Homestead,
|
|
Byzantium, Constantinople or Istanbul, respectively.
|
|
|
|
In the following, ``mem[a...b)`` signifies the bytes of memory starting at position ``a`` up to
|
|
but not including position ``b`` and ``storage[p]`` signifies the storage contents at slot ``p``.
|
|
|
|
Since Yul manages local variables and control-flow,
|
|
opcodes that interfere with these features are not available. This includes
|
|
the ``dup`` and ``swap`` instructions as well as ``jump`` instructions, labels and the ``push`` instructions.
|
|
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| Instruction | | | Explanation |
|
|
+=========================+=====+===+=================================================================+
|
|
| stop() + `-` | F | stop execution, identical to return(0, 0) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| add(x, y) | | F | x + y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sub(x, y) | | F | x - y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mul(x, y) | | F | x * y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| div(x, y) | | F | x / y or 0 if y == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sdiv(x, y) | | F | x / y, for signed numbers in two's complement, 0 if y == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mod(x, y) | | F | x % y, 0 if y == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| smod(x, y) | | F | x % y, for signed numbers in two's complement, 0 if y == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| exp(x, y) | | F | x to the power of y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| not(x) | | F | bitwise "not" of x (every bit of x is negated) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| lt(x, y) | | F | 1 if x < y, 0 otherwise |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| gt(x, y) | | F | 1 if x > y, 0 otherwise |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| slt(x, y) | | F | 1 if x < y, 0 otherwise, for signed numbers in two's complement |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sgt(x, y) | | F | 1 if x > y, 0 otherwise, for signed numbers in two's complement |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| eq(x, y) | | F | 1 if x == y, 0 otherwise |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| iszero(x) | | F | 1 if x == 0, 0 otherwise |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| and(x, y) | | F | bitwise "and" of x and y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| or(x, y) | | F | bitwise "or" of x and y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| xor(x, y) | | F | bitwise "xor" of x and y |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| byte(n, x) | | F | nth byte of x, where the most significant byte is the 0th byte |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| shl(x, y) | | C | logical shift left y by x bits |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| shr(x, y) | | C | logical shift right y by x bits |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sar(x, y) | | C | signed arithmetic shift right y by x bits |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| addmod(x, y, m) | | F | (x + y) % m with arbitrary precision arithmetic, 0 if m == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mulmod(x, y, m) | | F | (x * y) % m with arbitrary precision arithmetic, 0 if m == 0 |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| signextend(i, x) | | F | sign extend from (i*8+7)th bit counting from least significant |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| keccak256(p, n) | | F | keccak(mem[p...(p+n))) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| pc() | | F | current position in code |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| pop(x) | `-` | F | discard value x |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mload(p) | | F | mem[p...(p+32)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mstore(p, v) | `-` | F | mem[p...(p+32)) := v |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| mstore8(p, v) | `-` | F | mem[p] := v & 0xff (only modifies a single byte) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sload(p) | | F | storage[p] |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| sstore(p, v) | `-` | F | storage[p] := v |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| msize() | | F | size of memory, i.e. largest accessed memory index |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| gas() | | F | gas still available to execution |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| address() | | F | address of the current contract / execution context |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| balance(a) | | F | wei balance at address a |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| selfbalance() | | I | equivalent to balance(address()), but cheaper |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| caller() | | F | call sender (excluding ``delegatecall``) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| callvalue() | | F | wei sent together with the current call |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| calldataload(p) | | F | call data starting from position p (32 bytes) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| calldatasize() | | F | size of call data in bytes |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| calldatacopy(t, f, s) | `-` | F | copy s bytes from calldata at position f to mem at position t |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| codesize() | | F | size of the code of the current contract / execution context |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| codecopy(t, f, s) | `-` | F | copy s bytes from code at position f to mem at position t |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| extcodesize(a) | | F | size of the code at address a |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| extcodecopy(a, t, f, s) | `-` | F | like codecopy(t, f, s) but take code at address a |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| returndatasize() | | B | size of the last returndata |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| returndatacopy(t, f, s) | `-` | B | copy s bytes from returndata at position f to mem at position t |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| extcodehash(a) | | C | code hash of address a |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| create(v, p, n) | | F | create new contract with code mem[p...(p+n)) and send v wei |
|
|
| | | | and return the new address; returns 0 on error |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| create2(v, p, n, s) | | C | create new contract with code mem[p...(p+n)) at address |
|
|
| | | | keccak256(0xff . this . s . keccak256(mem[p...(p+n))) |
|
|
| | | | and send v wei and return the new address, where ``0xff`` is a |
|
|
| | | | 1 byte value, ``this`` is the current contract's address |
|
|
| | | | as a 20 byte value and ``s`` is a big-endian 256-bit value; |
|
|
| | | | returns 0 on error |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| call(g, a, v, in, | | F | call contract at address a with input mem[in...(in+insize)) |
|
|
| insize, out, outsize) | | | providing g gas and v wei and output area |
|
|
| | | | mem[out...(out+outsize)) returning 0 on error (eg. out of gas) |
|
|
| | | | and 1 on success |
|
|
| | | | :ref:`See more <yul-call-return-area>` |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| callcode(g, a, v, in, | | F | identical to ``call`` but only use the code from a and stay |
|
|
| insize, out, outsize) | | | in the context of the current contract otherwise |
|
|
| | | | :ref:`See more <yul-call-return-area>` |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| delegatecall(g, a, in, | | H | identical to ``callcode`` but also keep ``caller`` |
|
|
| insize, out, outsize) | | | and ``callvalue`` |
|
|
| | | | :ref:`See more <yul-call-return-area>` |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| staticcall(g, a, in, | | B | identical to ``call(g, a, 0, in, insize, out, outsize)`` but do |
|
|
| insize, out, outsize) | | | not allow state modifications |
|
|
| | | | :ref:`See more <yul-call-return-area>` |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| return(p, s) | `-` | F | end execution, return data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| revert(p, s) | `-` | B | end execution, revert state changes, return data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| selfdestruct(a) | `-` | F | end execution, destroy current contract and send funds to a |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| invalid() | `-` | F | end execution with invalid instruction |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| log0(p, s) | `-` | F | log without topics and data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| log1(p, s, t1) | `-` | F | log with topic t1 and data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| log2(p, s, t1, t2) | `-` | F | log with topics t1, t2 and data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| log3(p, s, t1, t2, t3) | `-` | F | log with topics t1, t2, t3 and data mem[p...(p+s)) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| log4(p, s, t1, t2, t3, | `-` | F | log with topics t1, t2, t3, t4 and data mem[p...(p+s)) |
|
|
| t4) | | | |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| chainid() | | I | ID of the executing chain (EIP 1344) |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| origin() | | F | transaction sender |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| gasprice() | | F | gas price of the transaction |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| blockhash(b) | | F | hash of block nr b - only for last 256 blocks excluding current |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| coinbase() | | F | current mining beneficiary |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| timestamp() | | F | timestamp of the current block in seconds since the epoch |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| number() | | F | current block number |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| difficulty() | | F | difficulty of the current block |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
| gaslimit() | | F | block gas limit of the current block |
|
|
+-------------------------+-----+---+-----------------------------------------------------------------+
|
|
|
|
.. _yul-call-return-area:
|
|
|
|
.. note::
|
|
The ``call*`` instructions use the ``out`` and ``outsize`` parameters to define an area in memory where
|
|
the return or failure data is placed. This area is written to depending on how many bytes the called contract returns.
|
|
If it returns more data, only the first ``outsize`` bytes are written. You can access the rest of the data
|
|
using the ``returndatacopy`` opcode. If it returns less data, then the remaining bytes are not touched at all.
|
|
You need to use the ``returndatasize`` opcode to check which part of this memory area contains the return data.
|
|
The remaining bytes will retain their values as of before the call.
|
|
|
|
|
|
In some internal dialects, there are additional functions:
|
|
|
|
datasize, dataoffset, datacopy
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The functions ``datasize(x)``, ``dataoffset(x)`` and ``datacopy(t, f, l)``
|
|
are used to access other parts of a Yul object.
|
|
|
|
``datasize`` and ``dataoffset`` can only take string literals (the names of other objects)
|
|
as arguments and return the size and offset in the data area, respectively.
|
|
For the EVM, the ``datacopy`` function is equivalent to ``codecopy``.
|
|
|
|
|
|
setimmutable, loadimmutable
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The functions ``setimmutable(offset, "name", value)`` and ``loadimmutable("name")`` are
|
|
used for the immutable mechanism in Solidity and do not nicely map to pure Yul.
|
|
The call to ``setimmutable(offset, "name", value)`` assumes that the runtime code of the contract
|
|
containing the given named immutable was copied to memory at offset ``offset`` and will write ``value`` to all
|
|
positions in memory (relative to ``offset``) that contain the placeholder that was generated for calls
|
|
to ``loadimmutable("name")`` in the runtime code.
|
|
|
|
|
|
linkersymbol
|
|
^^^^^^^^^^^^
|
|
|
|
The function ``linkersymbol("fq_library_name")`` is a placeholder for an address literal to be
|
|
substituted by the linker. Its first and only argument must be a string literal and represents the
|
|
fully qualified library name used with the ``--libraries`` option.
|
|
|
|
For example this code
|
|
|
|
.. code-block:: yul
|
|
|
|
let a := linkersymbol("file.sol:Math")
|
|
|
|
is equivalent to
|
|
|
|
.. code-block:: yul
|
|
|
|
let a := 0x1234567890123456789012345678901234567890
|
|
|
|
when the linker is invoked with ``--libraries "file.sol:Math=0x1234567890123456789012345678901234567890``
|
|
option.
|
|
|
|
See :ref:`Using the Commandline Compiler <commandline-compiler>` for details about the Solidity linker.
|
|
|
|
memoryguard
|
|
^^^^^^^^^^^
|
|
|
|
This function is available in the EVM dialect with objects. The caller of
|
|
``let ptr := memoryguard(size)`` (where ``size`` has to be a literal number)
|
|
promises that they only use memory in either the range ``[0, size)`` or the
|
|
unbounded range starting at ``ptr``.
|
|
|
|
Since the presence of a ``memoryguard`` call indicates that all memory access
|
|
adheres to this restriction, it allows the optimizer to perform additional
|
|
optimization steps, for example the stack limit evader, which attempts to move
|
|
stack variables that would otherwise be unreachable to memory.
|
|
|
|
The Yul optimizer promises to only use the memory range ``[size, ptr)`` for its purposes.
|
|
If the optimizer does not need to reserve any memory, it holds that ``ptr == size``.
|
|
|
|
``memoryguard`` can be called multiple times, but needs to have the same literal as argument
|
|
within one Yul subobject. If at least one ``memoryguard`` call is found in a subobject,
|
|
the additional optimiser steps will be run on it.
|
|
|
|
|
|
verbatim
|
|
^^^^^^^^
|
|
|
|
The set of ``verbatim...`` builtin functions 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
|
|
|
|
- ``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
|
|
|
|
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)
|
|
|
|
This code will result in a ``dup1`` opcode to retrieve ``x``
|
|
(the optimizer might directly re-use result of the
|
|
``calldataload`` opcode, though)
|
|
directly followed by ``600202``. The code is assumed to
|
|
consume the copied value of ``x`` and produce the result
|
|
on the top of the stack. The compiler then generates code
|
|
to allocate a stack slot for ``double`` and store the result there.
|
|
|
|
As with all opcodes, the arguments are arranged on the stack
|
|
with the leftmost argument on the top, while the return values
|
|
are assumed to be laid out such that the rightmost variable is
|
|
at the top of the stack.
|
|
|
|
Since ``verbatim`` can be used to generate arbitrary opcodes
|
|
or even opcodes unknown to the Solidity compiler, care has to be taken
|
|
when using ``verbatim`` together with the optimizer. Even when the
|
|
optimizer is switched off, the code generator has to determine
|
|
the stack layout, which means that e.g. using ``verbatim`` to modify
|
|
the stack height can lead to undefined behaviour.
|
|
|
|
The following is a non-exhaustive list of restrictions on
|
|
verbatim bytecode that are not checked by
|
|
the compiler. Violations of these restrictions can result in
|
|
undefined behaviour.
|
|
|
|
- Control-flow should not jump into or out of verbatim blocks,
|
|
but it can jump within the same verbatim block.
|
|
- Stack contents apart from the input and output parameters
|
|
should not be accessed.
|
|
- The stack height difference should be exactly ``m - n``
|
|
(output slots minus input slots).
|
|
- Verbatim bytecode cannot make any assumptions about the
|
|
surrounding bytecode. All required parameters have to be
|
|
passed in as stack variables.
|
|
|
|
The optimizer does not analyze verbatim bytecode and always
|
|
assumes that it modifies all aspects of state and thus can only
|
|
do very few optimizations across ``verbatim`` function calls.
|
|
|
|
The optimizer treats verbatim bytecode as an opaque block of code.
|
|
It will not split it but might move, duplicate
|
|
or combine it with identical verbatim bytecode blocks.
|
|
If a verbatim bytecode block is unreachable by the control-flow,
|
|
it can be removed.
|
|
|
|
|
|
.. warning::
|
|
|
|
During discussions about whether or not EVM improvements
|
|
might break existing smart contracts, features inside ``verbatim``
|
|
cannot receive the same consideration as those used by the Solidity
|
|
compiler itself.
|
|
|
|
.. note::
|
|
|
|
To avoid confusion, all identifiers starting with the string ``verbatim`` are reserved
|
|
and cannot be used for user-defined identifiers.
|
|
|
|
.. _yul-object:
|
|
|
|
Specification of Yul Object
|
|
===========================
|
|
|
|
Yul objects are used to group named code and data sections.
|
|
The functions ``datasize``, ``dataoffset`` and ``datacopy``
|
|
can be used to access these sections from within code.
|
|
Hex strings can be used to specify data in hex encoding,
|
|
regular strings in native encoding. For code,
|
|
``datacopy`` will access its assembled binary representation.
|
|
|
|
.. code-block:: none
|
|
|
|
Object = 'object' StringLiteral '{' Code ( Object | Data )* '}'
|
|
Code = 'code' Block
|
|
Data = 'data' StringLiteral ( HexLiteral | StringLiteral )
|
|
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
|
|
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
|
|
|
|
Above, ``Block`` refers to ``Block`` in the Yul code grammar explained in the previous chapter.
|
|
|
|
.. note::
|
|
|
|
Data objects or sub-objects whose names contain a ``.`` can be defined
|
|
but it is not possible to access them through ``datasize``,
|
|
``dataoffset`` or ``datacopy`` because ``.`` is used as a separator
|
|
to access objects inside another object.
|
|
|
|
.. note::
|
|
|
|
The data object called ``".metadata"`` has a special meaning:
|
|
It cannot be accessed from code and is always appended to the very end of the
|
|
bytecode, regardless of its position in the object.
|
|
|
|
Other data objects with special significance might be added in the
|
|
future, but their names will always start with a ``.``.
|
|
|
|
|
|
An example Yul Object is shown below:
|
|
|
|
.. code-block:: yul
|
|
|
|
// A contract consists of a single object with sub-objects representing
|
|
// the code to be deployed or other contracts it can create.
|
|
// The single "code" node is the executable code of the object.
|
|
// Every (other) named object or data section is serialized and
|
|
// made accessible to the special built-in functions datacopy / dataoffset / datasize
|
|
// The current object, sub-objects and data items inside the current object
|
|
// are in scope.
|
|
object "Contract1" {
|
|
// This is the constructor code of the contract.
|
|
code {
|
|
function allocate(size) -> ptr {
|
|
ptr := mload(0x40)
|
|
if iszero(ptr) { ptr := 0x60 }
|
|
mstore(0x40, add(ptr, size))
|
|
}
|
|
|
|
// first create "Contract2"
|
|
let size := datasize("Contract2")
|
|
let offset := allocate(size)
|
|
// This will turn into codecopy for EVM
|
|
datacopy(offset, dataoffset("Contract2"), size)
|
|
// constructor parameter is a single number 0x1234
|
|
mstore(add(offset, size), 0x1234)
|
|
pop(create(offset, add(size, 32), 0))
|
|
|
|
// now return the runtime object (the currently
|
|
// executing code is the constructor code)
|
|
size := datasize("runtime")
|
|
offset := allocate(size)
|
|
// This will turn into a memory->memory copy for Ewasm and
|
|
// a codecopy for EVM
|
|
datacopy(offset, dataoffset("runtime"), size)
|
|
return(offset, size)
|
|
}
|
|
|
|
data "Table2" hex"4123"
|
|
|
|
object "runtime" {
|
|
code {
|
|
function allocate(size) -> ptr {
|
|
ptr := mload(0x40)
|
|
if iszero(ptr) { ptr := 0x60 }
|
|
mstore(0x40, add(ptr, size))
|
|
}
|
|
|
|
// runtime code
|
|
|
|
mstore(0, "Hello, World!")
|
|
return(0, 0x20)
|
|
}
|
|
}
|
|
|
|
// Embedded object. Use case is that the outside is a factory contract,
|
|
// and Contract2 is the code to be created by the factory
|
|
object "Contract2" {
|
|
code {
|
|
// code here ...
|
|
}
|
|
|
|
object "runtime" {
|
|
code {
|
|
// code here ...
|
|
}
|
|
}
|
|
|
|
data "Table1" hex"4123"
|
|
}
|
|
}
|
|
|
|
Yul Optimizer
|
|
=============
|
|
|
|
The Yul optimizer operates on Yul code and uses the same language for input, output and
|
|
intermediate states. This allows for easy debugging and verification of the optimizer.
|
|
|
|
Please refer to the general :ref:`optimizer documentation <optimizer>`
|
|
for more details about the different optimization stages and how to use the optimizer.
|
|
|
|
If you want to use Solidity in stand-alone Yul mode, you activate the optimizer using ``--optimize``:
|
|
|
|
.. code-block:: sh
|
|
|
|
solc --strict-assembly --optimize
|
|
|
|
In Solidity mode, the Yul optimizer is activated together with the regular optimizer.
|
|
|
|
Optimization Step Sequence
|
|
--------------------------
|
|
|
|
By default the Yul optimizer applies its predefined sequence of optimization steps to the generated assembly.
|
|
You can override this sequence and supply your own using the ``--yul-optimizations`` option:
|
|
|
|
.. code-block:: sh
|
|
|
|
solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul'
|
|
|
|
The order of steps is significant and affects the quality of the output.
|
|
Moreover, applying a step may uncover new optimization opportunities for others that were already
|
|
applied so repeating steps is often beneficial.
|
|
By enclosing part of the sequence in square brackets (``[]``) you tell the optimizer to repeatedly
|
|
apply that part until it no longer improves the size of the resulting assembly.
|
|
You can use brackets multiple times in a single sequence but they cannot be nested.
|
|
|
|
The following optimization steps are available:
|
|
|
|
============ ===============================
|
|
Abbreviation Full name
|
|
============ ===============================
|
|
``f`` ``BlockFlattener``
|
|
``l`` ``CircularReferencesPruner``
|
|
``c`` ``CommonSubexpressionEliminator``
|
|
``C`` ``ConditionalSimplifier``
|
|
``U`` ``ConditionalUnsimplifier``
|
|
``n`` ``ControlFlowSimplifier``
|
|
``D`` ``DeadCodeEliminator``
|
|
``v`` ``EquivalentFunctionCombiner``
|
|
``e`` ``ExpressionInliner``
|
|
``j`` ``ExpressionJoiner``
|
|
``s`` ``ExpressionSimplifier``
|
|
``x`` ``ExpressionSplitter``
|
|
``I`` ``ForLoopConditionIntoBody``
|
|
``O`` ``ForLoopConditionOutOfBody``
|
|
``o`` ``ForLoopInitRewriter``
|
|
``i`` ``FullInliner``
|
|
``g`` ``FunctionGrouper``
|
|
``h`` ``FunctionHoister``
|
|
``F`` ``FunctionSpecializer``
|
|
``T`` ``LiteralRematerialiser``
|
|
``L`` ``LoadResolver``
|
|
``M`` ``LoopInvariantCodeMotion``
|
|
``r`` ``RedundantAssignEliminator``
|
|
``R`` ``ReasoningBasedSimplifier`` - highly experimental
|
|
``m`` ``Rematerialiser``
|
|
``V`` ``SSAReverser``
|
|
``a`` ``SSATransform``
|
|
``t`` ``StructuralSimplifier``
|
|
``u`` ``UnusedPruner``
|
|
``d`` ``VarDeclInitializer``
|
|
============ ===============================
|
|
|
|
Some steps depend on properties ensured by ``BlockFlattener``, ``FunctionGrouper``, ``ForLoopInitRewriter``.
|
|
For this reason the Yul optimizer always applies them before applying any steps supplied by the user.
|
|
|
|
The ReasoningBasedSimplifier is an optimizer step that is currently not enabled
|
|
in the default set of steps. It uses an SMT solver to simplify arithmetic expressions
|
|
and boolean conditions. It has not received thorough testing or validation yet and can produce
|
|
non-reproducible results, so please use with care!
|
|
|
|
.. _erc20yul:
|
|
|
|
Complete ERC20 Example
|
|
======================
|
|
|
|
.. code-block:: yul
|
|
|
|
object "Token" {
|
|
code {
|
|
// Store the creator in slot zero.
|
|
sstore(0, caller())
|
|
|
|
// Deploy the contract
|
|
datacopy(0, dataoffset("runtime"), datasize("runtime"))
|
|
return(0, datasize("runtime"))
|
|
}
|
|
object "runtime" {
|
|
code {
|
|
// Protection against sending Ether
|
|
require(iszero(callvalue()))
|
|
|
|
// Dispatcher
|
|
switch selector()
|
|
case 0x70a08231 /* "balanceOf(address)" */ {
|
|
returnUint(balanceOf(decodeAsAddress(0)))
|
|
}
|
|
case 0x18160ddd /* "totalSupply()" */ {
|
|
returnUint(totalSupply())
|
|
}
|
|
case 0xa9059cbb /* "transfer(address,uint256)" */ {
|
|
transfer(decodeAsAddress(0), decodeAsUint(1))
|
|
returnTrue()
|
|
}
|
|
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
|
|
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
|
|
returnTrue()
|
|
}
|
|
case 0x095ea7b3 /* "approve(address,uint256)" */ {
|
|
approve(decodeAsAddress(0), decodeAsUint(1))
|
|
returnTrue()
|
|
}
|
|
case 0xdd62ed3e /* "allowance(address,address)" */ {
|
|
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
|
|
}
|
|
case 0x40c10f19 /* "mint(address,uint256)" */ {
|
|
mint(decodeAsAddress(0), decodeAsUint(1))
|
|
returnTrue()
|
|
}
|
|
default {
|
|
revert(0, 0)
|
|
}
|
|
|
|
function mint(account, amount) {
|
|
require(calledByOwner())
|
|
|
|
mintTokens(amount)
|
|
addToBalance(account, amount)
|
|
emitTransfer(0, account, amount)
|
|
}
|
|
function transfer(to, amount) {
|
|
executeTransfer(caller(), to, amount)
|
|
}
|
|
function approve(spender, amount) {
|
|
revertIfZeroAddress(spender)
|
|
setAllowance(caller(), spender, amount)
|
|
emitApproval(caller(), spender, amount)
|
|
}
|
|
function transferFrom(from, to, amount) {
|
|
decreaseAllowanceBy(from, caller(), amount)
|
|
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 {
|
|
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
|
|
}
|
|
|
|
function decodeAsAddress(offset) -> v {
|
|
v := decodeAsUint(offset)
|
|
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
|
|
revert(0, 0)
|
|
}
|
|
}
|
|
function decodeAsUint(offset) -> v {
|
|
let pos := add(4, mul(offset, 0x20))
|
|
if lt(calldatasize(), add(pos, 0x20)) {
|
|
revert(0, 0)
|
|
}
|
|
v := calldataload(pos)
|
|
}
|
|
/* ---------- calldata encoding functions ---------- */
|
|
function returnUint(v) {
|
|
mstore(0, v)
|
|
return(0, 0x20)
|
|
}
|
|
function returnTrue() {
|
|
returnUint(1)
|
|
}
|
|
|
|
/* -------- events ---------- */
|
|
function emitTransfer(from, to, amount) {
|
|
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
|
|
emitEvent(signatureHash, from, to, amount)
|
|
}
|
|
function emitApproval(from, spender, amount) {
|
|
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
|
|
emitEvent(signatureHash, from, spender, amount)
|
|
}
|
|
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
|
|
mstore(0, nonIndexed)
|
|
log3(0, 0x20, signatureHash, indexed1, indexed2)
|
|
}
|
|
|
|
/* -------- storage layout ---------- */
|
|
function ownerPos() -> p { p := 0 }
|
|
function totalSupplyPos() -> p { p := 1 }
|
|
function accountToStorageOffset(account) -> offset {
|
|
offset := add(0x1000, account)
|
|
}
|
|
function allowanceStorageOffset(account, spender) -> offset {
|
|
offset := accountToStorageOffset(account)
|
|
mstore(0, offset)
|
|
mstore(0x20, spender)
|
|
offset := keccak256(0, 0x40)
|
|
}
|
|
|
|
/* -------- storage access ---------- */
|
|
function owner() -> o {
|
|
o := sload(ownerPos())
|
|
}
|
|
function totalSupply() -> supply {
|
|
supply := sload(totalSupplyPos())
|
|
}
|
|
function mintTokens(amount) {
|
|
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
|
|
}
|
|
function balanceOf(account) -> bal {
|
|
bal := sload(accountToStorageOffset(account))
|
|
}
|
|
function addToBalance(account, amount) {
|
|
let offset := accountToStorageOffset(account)
|
|
sstore(offset, safeAdd(sload(offset), amount))
|
|
}
|
|
function deductFromBalance(account, amount) {
|
|
let offset := accountToStorageOffset(account)
|
|
let bal := sload(offset)
|
|
require(lte(amount, bal))
|
|
sstore(offset, sub(bal, amount))
|
|
}
|
|
function allowance(account, spender) -> amount {
|
|
amount := sload(allowanceStorageOffset(account, spender))
|
|
}
|
|
function setAllowance(account, spender, amount) {
|
|
sstore(allowanceStorageOffset(account, spender), amount)
|
|
}
|
|
function decreaseAllowanceBy(account, spender, amount) {
|
|
let offset := allowanceStorageOffset(account, spender)
|
|
let currentAllowance := sload(offset)
|
|
require(lte(amount, currentAllowance))
|
|
sstore(offset, sub(currentAllowance, amount))
|
|
}
|
|
|
|
/* ---------- utility functions ---------- */
|
|
function lte(a, b) -> r {
|
|
r := iszero(gt(a, b))
|
|
}
|
|
function gte(a, b) -> r {
|
|
r := iszero(lt(a, b))
|
|
}
|
|
function safeAdd(a, b) -> r {
|
|
r := add(a, b)
|
|
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
|
|
}
|
|
function calledByOwner() -> cbo {
|
|
cbo := eq(owner(), caller())
|
|
}
|
|
function revertIfZeroAddress(addr) {
|
|
require(addr)
|
|
}
|
|
function require(condition) {
|
|
if iszero(condition) { revert(0, 0) }
|
|
}
|
|
}
|
|
}
|
|
}
|