Yul

Yul

Yul (previously also called JULIA or IULIA) is an intermediate language that can compile to various different backends (EVM 1.0, EVM 1.5 and eWASM are planned). Because of that, it is designed to be a usable common denominator of all three platforms. It can already be used for “inline assembly” inside Solidity and future versions of the Solidity compiler will even use Yul as intermediate language. It should also be easy to build high-level optimizer stages for Yul.

Note that the flavour used for “inline assembly” does not have types (everything is u256) and the built-in functions are identical to the EVM opcodes. Please resort to the inline assembly documentation for details.

The core components of Yul are functions, blocks, variables, literals, for-loops, if-statements, switch-statements, expressions and assignments to variables.

Yul is typed, both variables and literals must specify the type with postfix notation. The supported types are bool, u8, s8, u32, s32, u64, s64, u128, s128, u256 and s256.

Yul in itself does not even provide operators. If the EVM is targeted, opcodes will be available as built-in functions, but they can be reimplemented if the backend changes. For a list of mandatory built-in functions, see the section below.

The following example program assumes that the EVM opcodes mul, div and mod are available either natively or as functions and computes exponentiation.

{
    function power(base:u256, exponent:u256) -> result:u256
    {
        switch exponent
        case 0:u256 { result := 1:u256 }
        case 1:u256 { result := base }
        default
        {
            result := power(mul(base, base), div(exponent, 2:u256))
            switch mod(exponent, 2:u256)
                case 1:u256 { result := mul(base, result) }
        }
    }
}

It is also possible to implement the same function using a for-loop instead of with recursion. Here, we need the EVM opcodes lt (less-than) and add to be available.

{
    function power(base:u256, exponent:u256) -> result:u256
    {
        result := 1:u256
        for { let i := 0:u256 } lt(i, exponent) { i := add(i, 1:u256) }
        {
            result := mul(result, base)
        }
    }
}

Specification of Yul

This chapter describes Yul code. It is usually placed inside a Yul object, which is described in the following chapter.

Grammar:

Block = '{' Statement* '}'
Statement =
    Block |
    FunctionDefinition |
    VariableDeclaration |
    Assignment |
    If |
    Expression |
    Switch |
    ForLoop |
    BreakContinue
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'
FunctionCall =
    Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier | BuiltinTypeName
BuiltinTypeName = 'bool' | [us] ( '8' | '32' | '64' | '128' | '256' )
TypedIdentifierList = Identifier ':' TypeName ( ',' Identifier ':' TypeName )*
Literal =
    (NumberLiteral | StringLiteral | HexLiteral | TrueLiteral | FalseLiteral) ':' TypeName
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

Restrictions on the Grammar

Switches must have at least one case (including the default case). If all possible values of the expression is covered, the default case should not be allowed (i.e. a switch with a bool expression and having both a true and false case should 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 values 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.

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 condition part of the for-loop has to evaluate to exactly one value.

Literals cannot be larger than the their type. The largest type defined is 256-bit wide.

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, identifiers defined in the “init” part of the for-loop (the first block) are visible in all other parts of the for-loop (but not outside of the loop). Identifiers declared in the other parts of the for loop respect the regular syntatical scoping rules. The parameters and return parameters of functions are visible in the function body and their names cannot overlap.

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. Any functions can have side effects, so 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 and continue 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.

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 var1, ..., varn := rhs>: VariableDeclaration) =
    E(G, L, <var1, ..., varn := rhs>: Assignment)
E(G, L, <let var1, ..., varn>: VariableDeclaration) =
    let L1 be a copy of L where L1[$vari] = 0 for i = 1, ..., n
    G, L1, regular
E(G, L, <var1, ..., varn := rhs>: Assignment) =
    let G1, L1, v1, ..., vn = E(G, L, rhs)
    let L2 be a copy of L1 where L2[$vari] = vi for i = 1, ..., n
    G, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
    if n >= 1:
        let G1, L1, mode = E(G, L, i1, ..., in)
        // mode has to be regular due to the syntactic restrictions
        let G2, L2, mode = E(G1, L1, for {} condition post body)
        // mode has to be regular due to the syntactic restrictions
        let L3 be the restriction of L2 to only variables of L
        G2, L3, regular
    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
            else:
                G3, L3, mode = E(G2, L2, post)
                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, <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: HexLiteral) = G, L, hexString(l),
    where hexString decodes l from hex and left-aligns it into 32 bytes
E(G, L, l: StringLiteral) = G, L, utf8EncodeLeftAligned(l),
    where utf8EncodeLeftAligned performs a utf8 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

Type Conversion Functions

Yul has no support for implicit type conversion and therefore functions exist to provide explicit conversion. When converting a larger type to a shorter type a runtime exception can occur in case of an overflow.Truncating conversions are supported between the following types:

  • bool

  • u32

  • u64

  • u256

  • s256

For each of these a type conversion function exists having the prototype in the form ofto(x:) -> y:, such as u32tobool(x:u32) -> y:bool, u256tou32(x:u256) -> y:u32 or s256tou256(x:s256) -> y:u256.

u32tobool(x:u32) -> y:bool can be implemented as y := not(iszerou256(x)) and booltou32(x:bool) -> y:u32 can be implemented as switch x case true:bool { y := 1:u32 } case false:bool { y := 0:u32 }

Low-level Functions

The following functions must be available:

Logic

not(x:bool) -> z:bool

logical not

and(x:bool, y:bool) -> z:bool

logical and

or(x:bool, y:bool) -> z:bool

logical or

xor(x:bool, y:bool) -> z:bool

xor

Arithmetic

addu256(x:u256, y:u256) -> z:u256

x + y

subu256(x:u256, y:u256) -> z:u256

x - y

mulu256(x:u256, y:u256) -> z:u256

x * y

divu256(x:u256, y:u256) -> z:u256

x / y

divs256(x:s256, y:s256) -> z:s256

x / y, for signed numbers in two’s complement

modu256(x:u256, y:u256) -> z:u256

x % y

mods256(x:s256, y:s256) -> z:s256

x % y, for signed numbers in two’s complement

signextendu256(i:u256, x:u256) -> z:u256

sign extend from (i*8+7)th bit counting from least significant

expu256(x:u256, y:u256) -> z:u256

x to the power of y

addmodu256(x:u256, y:u256, m:u256) -> z:u256

(x + y) % m with arbitrary precision arithmetic

mulmodu256(x:u256, y:u256, m:u256) -> z:u256

(x * y) % m with arbitrary precision arithmetic

ltu256(x:u256, y:u256) -> z:bool

true if x < y, false otherwise

gtu256(x:u256, y:u256) -> z:bool

true if x > y, false otherwise

lts256(x:s256, y:s256) -> z:bool

true if x < y, false otherwise (for signed numbers in two’s complement)

gts256(x:s256, y:s256) -> z:bool

true if x > y, false otherwise (for signed numbers in two’s complement)

equ256(x:u256, y:u256) -> z:bool

true if x == y, false otherwise

iszerou256(x:u256) -> z:bool

true if x == 0, false otherwise

notu256(x:u256) -> z:u256

~x, every bit of x is negated

andu256(x:u256, y:u256) -> z:u256

bitwise and of x and y

oru256(x:u256, y:u256) -> z:u256

bitwise or of x and y

xoru256(x:u256, y:u256) -> z:u256

bitwise xor of x and y

shlu256(x:u256, y:u256) -> z:u256

logical left shift of x by y

shru256(x:u256, y:u256) -> z:u256

logical right shift of x by y

sars256(x:s256, y:u256) -> z:u256

arithmetic right shift of x by y

byte(n:u256, x:u256) -> v:u256

nth byte of x, where the most significant byte is the 0th byte Cannot this be just replaced by and256(shr256(n, x), 0xff) and let it be optimised out by the EVM backend?

Memory and storage

mload(p:u256) -> v:u256

mem[p..(p+32))

mstore(p:u256, v:u256)

mem[p..(p+32)) := v

mstore8(p:u256, v:u256)

mem[p] := v & 0xff - only modifies a single byte

sload(p:u256) -> v:u256

storage[p]

sstore(p:u256, v:u256)

storage[p] := v

msize() -> size:u256

size of memory, i.e. largest accessed memory index, albeit due due to the memory extension function, which extends by words, this will always be a multiple of 32 bytes

Execution control

create(v:u256, p:u256, n:u256)

create new contract with code mem[p..(p+n)) and send v wei and return the new address

create2(v:u256, p:u256, n:u256, s:u256)

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 8 byte value, this is the current contract’s address as a 20 byte value and s is a big-endian 256-bit value

call(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256

call contract at address a with input mem[in..(in+insize)) 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

callcode(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256

identical to call but only use the code from a and stay in the context of the current contract otherwise

delegatecall(g:u256, a:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256

identical to callcode, but also keep caller and callvalue

abort()

abort (equals to invalid instruction on EVM)

return(p:u256, s:u256)

end execution, return data mem[p..(p+s))

revert(p:u256, s:u256)

end execution, revert state changes, return data mem[p..(p+s))

selfdestruct(a:u256)

end execution, destroy current contract and send funds to a

log0(p:u256, s:u256)

log without topics and data mem[p..(p+s))

log1(p:u256, s:u256, t1:u256)

log with topic t1 and data mem[p..(p+s))

log2(p:u256, s:u256, t1:u256, t2:u256)

log with topics t1, t2 and data mem[p..(p+s))

log3(p:u256, s:u256, t1:u256, t2:u256, t3:u256)

log with topics t, t2, t3 and data mem[p..(p+s))

log4(p:u256, s:u256, t1:u256, t2:u256, t3:u256, t4:u256)

log with topics t1, t2, t3, t4 and data mem[p..(p+s))

State queries

blockcoinbase() -> address:u256

current mining beneficiary

blockdifficulty() -> difficulty:u256

difficulty of the current block

blockgaslimit() -> limit:u256

block gas limit of the current block

blockhash(b:u256) -> hash:u256

hash of block nr b - only for last 256 blocks excluding current

blocknumber() -> block:u256

current block number

blocktimestamp() -> timestamp:u256

timestamp of the current block in seconds since the epoch

txorigin() -> address:u256

transaction sender

txgasprice() -> price:u256

gas price of the transaction

gasleft() -> gas:u256

gas still available to execution

balance(a:u256) -> v:u256

wei balance at address a

this() -> address:u256

address of the current contract / execution context

caller() -> address:u256

call sender (excluding delegatecall)

callvalue() -> v:u256

wei sent together with the current call

calldataload(p:u256) -> v:u256

call data starting from position p (32 bytes)

calldatasize() -> v:u256

size of call data in bytes

calldatacopy(t:u256, f:u256, s:u256)

copy s bytes from calldata at position f to mem at position t

codesize() -> size:u256

size of the code of the current contract / execution context

codecopy(t:u256, f:u256, s:u256)

copy s bytes from code at position f to mem at position t

extcodesize(a:u256) -> size:u256

size of the code at address a

extcodecopy(a:u256, t:u256, f:u256, s:u256)

like codecopy(t, f, s) but take code at address a

extcodehash(a:u256)

code hash of address a

Others

discard(unused:bool)

discard value

discardu256(unused:u256)

discard value

splitu256tou64(x:u256) -> (x1:u64, x2:u64, x3:u64, x4:u64)

split u256 to four u64’s

combineu64tou256(x1:u64, x2:u64, x3:u64, x4:u64) -> (x:u256)

combine four u64’s into a single u256

keccak256(p:u256, s:u256) -> v:u256

keccak(mem[p…(p+s)))

Object access

datasize(name:string) -> size:u256

size of the data object in bytes, name has to be string literal

dataoffset(name:string) -> offset:u256

offset of the data object inside the data area in bytes, name has to be string literal

datacopy(dst:u256, src:u256, len:u256)

copy len bytes from the data area starting at offset src bytes to memory at position dst

Backends

Backends or targets are the translators from Yul to a specific bytecode. Each of the backends can expose functions prefixed with the name of the backend. We reserve evm_ and ewasm_ prefixes for the two proposed backends.

Backend: EVM

The EVM target will have all the underlying EVM opcodes exposed with the evm_ prefix.

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.

Grammar:

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.

An example Yul Object is shown below:

// Code consists of a single object. A single "code" node is the 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
// Access to nested objects can be performed by joining the names using ``.``.
// The current object and sub-objects and data items inside the current object
// are in scope without nested access.
object "Contract1" {
    code {
        // first create "runtime.Contract2"
        let size = datasize("runtime.Contract2")
        let offset = allocate(size)
        // This will turn into a memory->memory copy for eWASM and
        // a codecopy for EVM
        datacopy(offset, dataoffset("runtime.Contract2"), size)
        // constructor parameter is a single number 0x1234
        mstore(add(offset, size), 0x1234)
        create(offset, add(size, 32))

        // now return the runtime object (this is
        // 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 {
            // runtime code

            let size = datasize("Contract2")
            let offset = allocate(size)
            // This will turn into a memory->memory copy for eWASM and
            // a codecopy for EVM
            datacopy(offset, dataoffset("Contract2"), size)
            // constructor parameter is a single number 0x1234
            mstore(add(offset, size), 0x1234)
            create(offset, add(size, 32))
        }

        // 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"
        }
    }
}

Last updated