Today, Fuel Labs introduces Yul+, which adds various QoL features to Yul, a low-level intermediate language for the Ethereum Virtual Machine.
Yul is an incredible little language written by the Solidity Developers as a compilation target for further optimizations. It features a simplistic and functional low-level grammar. It allows the developer to get much closer to raw EVM than Solidity, and with that comes the promise to drastically improved gas usage.
Fuel Labs has implemented its initial open-beta optimistic rollup contract largely with Yul, but we noticed that with the addition of even a tiny number of basic language additions, our code could become more legible and efficient.
Yul+ can be looked at as an experimental upgrade to Yul, and Yul might aim to integrate some of its features natively at a later time.
Some Yul Basics
A basic Yul contract with a constructor and runtime
object "EmptyContract" {
code { // Your constructor code datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
return(0, datasize("Runtime"))
}
object "Runtime" {
code { // Your runtime code }
}
}
Handling calldata
// copy calldata to memory
// this copies 36 bytes of transaction calldata to memory position 0calldatacopy(0, 0, 36)
Managing memory
// store and read memory
// store 0xaa at memory position 100mstore(100, 0xaa)// load 32 byte chunk from memory position 100 and assign to someVarlet someVar := mload(100)
Hashing
// hash memory position 0 to 0+32, assign result to someHashlet someHash := keccak256(0, 32)
State storage
// store value 0xaa in state storage slot 3sstore(3, 0xaa)// get value from state storage 3 and assign to someVarlet someVar := sload(3)
Functions, conditions, loops, and switches
// Functions and conditionsfunction someMethod(someVar, someOther) -> someResult {
if eq(someVar, someOther) {
someResult := 0x45
}
}// Loopsfor { let i := 0 } lt(i, 100) { i := add(i, 1) } {
// some loop code
}// Switchesswitch someVar
case 0 {
// when someVar == 0
}case 1 {
// when someVar == 1
}default {
// default
}
Yul+ Features
- All existing Yul language features
- Enums (enum)
- Constants (const)
- Ethereum standard ABI signature generation (sig”function …”)
- Booleans (true, false)
- Safe math by default (i.e. over/under flow protection for addition, subtraction, multiplication)
- Injected methods (mslice and require)
- Memory structures (mstruct)
Usage
Enums, constants, and Booleans
enum Colors (
Red, // 0
Blue, // 1
Green // 2
)
// Constant someConst will equal 1const someColor := Colors.Blue
// Constant someBool will equal 0x1const someBool := true
Ethereum standard ABI signature generation for method sigs and topics:
// someVar will equal 4 byte method signature 0x6057361dlet someVar := sig”function store(uint256 val)”// someTopic will equal 32 byte topic hash 0x69404ebde4a368ae324ed310becfefc3edfe9e5ebca74464e37ffffd8309a3c1let someTopic := topic”event Store(uint256 val)”
All maths are now safe by default, which can be disabled in the compiler if desired.
let someVar := add(3, sub(4, 2))// will compile to this, with safeAdd, safeSub methods injectedlet someVar := safeAdd(3, safeSub(4, 2))
We add for convenience a memory slice mslice and require if true
mstore(300, 0xaabbccdd) // note, mstore left pads zeros by 28 byteslet someVal := mslice(328, 3) // will return 0xaabbccrequire(gt(someVal, 0)) // someVal > 0 or revert(0, 0) nicely
Lastly, we enable memory structures. These are used to describe already-existing structures in memory, such as calldata, hash data, or any data with structure written to memory.
It offers a wide range of positioning, offset, hashing, indexing, and organizational features to better handle memory with neat efficient pre-made functions injected on-demand. We still keep to using a functional notation of injected functions, which doesn’t break existing Yul grammer style.
// Let’s assume we assign some calldata to memory position 0// this describes an abstract memory construction:mstruct SomeCalldata(
signature: 4,
value: 32,
)let methodSig := SomeCalldata.signature(0) // slices out sig
let someVal := SomeCalldata.value(0) // slices out value
// we also get some nice indexing and offset featuresSomeCalldata.value.position(0) // equals 4 (i.e. 0 + 4)
// Index ordering values as wellSomeCalldata.signature.index() // equals 0SomeCalldata.value.index() // equal 1
// Keccak hashingSomeCalldata.value.keccak256(0) // equals 32 byte hash of value
// Calculate entire size of calldata structureSomeCalldata.size(0) // equals 36 (i.e. 4 + 32)
Example: Yul+ SimpleStore Contract
object “SimpleStore” {
code {
datacopy(0, dataoffset(“Runtime”), datasize(“Runtime”))
return(0, datasize(“Runtime”))
}
object “Runtime” {
code {
calldatacopy(0, 0, 36) // copy calldata into memory mstruct Calldata( // mstruct describes calldata
sig: 4,
val: 32
) switch Calldata.sig(0) // get signature at positive zero
case sig”function store(uint256 val)” { // store method
sstore(0, Calldata.val(0))
} case sig”function get() returns (uint256)” { // get method
mstore(100, sload(0))
return (100, 32)
}
}
}
}
Try it Now in Your Browser!
Wrapping Up
In conclusion, the Fuel Labs team hopes to expand the possibilities for the Ethereum Virtual Machine by creating more low-level alternatives which we use everyday to build high-performance optimistic rollup scalability for the ecosystem.
In the meantime, for more info and to keep up to date with our work:
Website: https://fuel.sh
Twitter: https://twitter.com/FuelLabs_
GitHub: https://github.com/FuelLabs/yulp