Introducing: Yul+ — A new low-level language for Ethereum
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 itsinitial 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 0
calldatacopy(0, 0, 36)
Managing memory
// store and read memory
// store 0xaa at memory position 100
mstore(100, 0xaa)
// load 32 byte chunk from memory position 100 and assign to someVar
let someVar := mload(100)
Hashing
// hash memory position 0 to 0+32, assign result to someHash
let someHash := keccak256(0, 32)
State storage
// store value 0xaa in state storage slot 3
sstore(3, 0xaa)
// get value from state storage 3 and assign to someVar
let someVar := sload(3)
Functions, conditions, loops, and switches
// Functions and conditions
function someMethod(someVar, someOther) -> someResult {
if eq(someVar, someOther) {
someResult := 0x45
}
}
// Loops
for { let i := 0 } lt(i, 100) { i := add(i, 1) } {
// some loop code
}
// Switches
switch 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 1
const someColor := Colors.Blue
// Constant someBool will equal 0x1
const someBool := true
Ethereum standard ABI signature generation for method sigs and topics:
// someVar will equal 4 byte method signature 0x6057361d
let someVar := sig”function store(uint256 val)”
// someTopic will equal 32 byte topic hash 0x69404ebde4a368ae324ed310becfefc3edfe9e5ebca74464e37ffffd8309a3c1
let 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 injected
let 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 bytes
let someVal := mslice(328, 3) // will return 0xaabbcc
require(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 features
SomeCalldata.value.position(0) // equals 4 (i.e. 0 + 4)
// Index ordering values as well
SomeCalldata.signature.index() // equals 0
SomeCalldata.value.index() // equal 1
// Keccak hashing
SomeCalldata.value.keccak256(0) // equals 32 byte hash of value
// Calculate entire size of calldata structure
SomeCalldata.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