Expressions
Static offsets work for simple variables, but most interesting data has locations that depend on runtime values. Expressions let pointers compute addresses dynamically.
To evaluate expressions against live EVM state interactively, see the Pointer playground.
Why expressions are needed
Consider reading element i from a memory array. The element's location
depends on:
- Where the array starts (might come from the free memory pointer)
- Which element we want (the index
i) - How big each element is (32 bytes for
uint256)
A static pointer can't capture this, but an expression can — for example,
array-start + index × 32 computes the element's offset from the array's
base and the index.
Integers and bytes
Expressions produce two kinds of value. Arithmetic ($sum, $product, and
so on) is unbounded integer math: the result is a number with no byte width.
$read, hexadecimal literals with an even number of digits, and the resize
operations produce bytes of a definite width.
The distinction matters for hashing and concatenation. $keccak256 and
$concat work on bytes, and their results depend on how wide each operand
is, so every operand must already be bytes. A bare number like 5 or an
arithmetic result is not; wrap it in $wordsized (or $sized<N>) first.
That applies to mapping keys and to slot numbers before hashing:
{ "$keccak256": [{ "$wordsized": "key" }, { "$wordsized": "slot" }] }
Resizes are the only way to turn an integer into bytes; nothing pads
implicitly. Going the other way is automatic: where a number is expected (an
arithmetic operand, a slot, offset, length, or list count), bytes are
read as the big-endian integer they encode, so a hash can be used directly as
a slot or added to.
Arithmetic expressions
Basic math operations for computing addresses:
$sum — Addition
Adds all values in an array.
$difference — Subtraction
Subtracts the second value from the first (saturates at zero).
$product — Multiplication
Multiplies all values in an array.
$quotient — Division
Integer division of the first value by the second.
$remainder — Modulo
Remainder after division.
Reading values
$read — Read from a named region
Reads the bytes from a previously defined region; the result is bytes as
wide as the region. For example, a group can name an array-length-slot
region and then use { $read: "array-length-slot" } to retrieve the array's
length at runtime and use it in a later computation.
Region property lookups
Reference properties of named regions with .property syntax:
.offset — Region's offset
{ ".offset": "previous-element" }
Returns the offset of the named region.
.length — Region's length
{ ".length": "previous-element" }
Returns the length of the named region.
.slot — Region's slot
{ ".slot": "base-slot" }
Returns the slot number for storage/stack/transient regions.
Chaining lookups
Property lookups can compute the next element's position from the previous
one. For example, element-1's offset can be computed as the sum of
element-0's .offset and its .length.
Computing storage slots with $keccak256
Solidity uses keccak256 hashing to compute storage locations for dynamic data.
Array element slots
For a dynamic array at slot n, elements start at keccak256(n), so element
i lives at keccak256(n) + i. The slot is word-sized before hashing, and
the 32-byte hash is then read as an integer by $sum:
{
"$sum": [
{ "$keccak256": [{ "$wordsized": 5 }] },
"element-index"
]
}
Mapping value slots
For a mapping at slot n, the value for key k is at keccak256(k, n), with
both the key and the slot word-sized:
{ "$keccak256": [{ "$wordsized": "key" }, { "$wordsized": 3 }] }
Nested mappings
For mapping(address => mapping(uint => uint)) at slot 2, the value is at
keccak256(inner_key, keccak256(outer_key, 2)). The inner hash is already 32
bytes and needs no resize; the keys and the literal slot do:
{
"$keccak256": [
{ "$wordsized": "inner-key" },
{ "$keccak256": [{ "$wordsized": "outer-key" }, { "$wordsized": 2 }] }
]
}
Data manipulation
$concat — Concatenate bytes
Joins byte sequences without padding. Every operand must be bytes. Useful for building hash inputs from multiple values.
$sized<N> — resize to N bytes
Truncates or pads to exactly N bytes. Pads with zeros on the left; truncates from the left if too long.
$wordsized — Resize to word size
Equivalent to $sized32 on the EVM (pads or truncates to 32 bytes).
Variables in expressions
Expressions can reference variables by name. These come from list pointer
contexts: within a list, the variable named by each takes values from 0 to
count-1, computing each element's slot.
Complete example: dynamic array element
To read element i from uint256[] storage arr at slot 5, a pointer:
- Defines the array's base slot
- Computes the element's slot:
keccak256(5) + element_index, word-sizing5before hashing - Returns that storage location
Learn more
- Regions documentation for region structure
- Expression specification for the complete expression language
- Implementation guide for building an expression evaluator