Expressions¶
Expressions form the right-hand side of let, assignments, return, require, emit, and the condition slots of if / while. Every expression evaluates to a single 256-bit word.
Literals¶
0
42
1_000_000 // underscores ignored
0xCAFE_BABE
true // == 1
false // == 0
"alice" // UTF-8 right-padded to 32 bytes = 0x616c696365000...0
"" // empty string == 0
String literals are sugar for uint: the UTF-8 bytes right-padded to 32 with zeros, interpreted big-endian. Max 32 bytes; no escape sequences in v1. See types.
Fixed-point math (Q64.64)¶
Fourier has no floating-point. Decimal arithmetic uses a Q64.64 fixed-point convention: a uint value V represents the real number V / 2**64. So:
1.0is2**640.5is2**631.5is2**64 + 2**63- the largest exactly representable integer is
2**192 - 1
Four builtins do the arithmetic — from_int lifts an integer, to_int truncates back, and fmul / fdiv handle the 64-bit scale factor that plain * / / would not:
let one_half: uint = from_int(1) / 2; // 0.5 as Q64.64
let three: uint = from_int(3); // 3.0
let half_x_3: uint = fmul(one_half, three); // 1.5
let two: uint = fdiv(half_x_3, fdiv(three, from_int(4))); // 1.5 / 0.75 = 2.0
let as_int: uint = to_int(two); // 2
+ and - on Q64.64 values work without any helper — the scale factor is preserved by addition. Comparisons (==, <, >, …) also work as-is.
Overflow rule. fmul(a, b) computes (a * b) / 2**64 in 256-bit modular arithmetic. If a * b ≥ 2**256 it wraps silently. With Q64.64 both a and b must be below 2**128 (real values below 2**64 ≈ 1.8e19) for multiplication to be exact. For any sensible financial range this is comfortably satisfied. fdiv has the same constraint on a * 2**64.
Variables¶
NAME // local memory load OR storage SLOAD
NAME[KEY] // storage mapping/array load
NAME[K1][K2] // nested mapping load
NAME.FIELD // storage struct field load
Identifier resolution at expression position:
- Local? →
PUSH offset, MLOAD - Storage scalar (not map/array)? →
PUSH slot, SLOAD - Storage struct field access (
name.field)? →PUSH slot+i, SLOAD - Storage map/array indexed (
name[k])? → compute derived slot,SLOAD - Bare storage map name → compile error ("mapping '...' used as scalar")
Operators¶
Precedence¶
Highest to lowest binding (from _BIN_OPS in fourier/parser.py):
| Level | Operators | Notes |
|---|---|---|
| 9 | * / % | DIV / MOD by 0 produce 0 (not a revert) |
| 8 | + - | Wraps modulo 2**256 |
| 7 | < > <= >= | Unsigned |
| 6 | == != | |
| 5 | & | Bitwise AND |
| 4 | ^ | Bitwise XOR |
| 3 | \| | Bitwise OR |
| 2 | && | Logical AND (no short-circuit) |
| 1 | \|\| | Logical OR (no short-circuit) |
Prefix unary (-, !, ~) binds tighter than any binary operator.
Short-circuit?¶
Both sides are always evaluated. && and || collapse to bitwise ops over normalized booleans:
If you need short-circuit for side-effect safety, use an if:
Operator → opcode mapping¶
| Source | Bytecode |
|---|---|
+ | ADD |
- | SWAP 1 then SUB (operand order correction) |
* | MUL |
/ | SWAP 1 then DIV |
% | SWAP 1 then MOD |
== | EQ |
!= | EQ then ISZERO |
< | SWAP 1 then LT |
> | SWAP 1 then GT |
<= | SWAP 1 then GT then ISZERO |
>= | SWAP 1 then LT then ISZERO |
& | AND |
\| | OR |
^ | XOR |
&& | normalized AND (see above) |
\|\| | normalized OR |
Unary:
| Source | Bytecode |
|---|---|
-x | PUSH 0, SWAP 1, SUB |
!x | ISZERO |
~x | NOT |
Calls¶
Function call syntax: NAME(ARG, ARG, ...). Resolved against:
- Builtins (see Builtins below)
- Otherwise →
unknown function 'NAME/ARITY'compile error.
There is no user-defined fn call mechanism in v1. Every NAME(...) in an expression must be a builtin.
Builtins¶
Environment¶
caller() -> address // CALLER
callvalue() -> uint // CALLVALUE
origin() -> address // ORIGIN
block_height() -> uint // BLOCKHEIGHT
timestamp() -> uint // TIMESTAMP
balance(addr) -> uint // BALANCE
Crypto¶
Hashes a single 32-byte word and returns the 256-bit result. The word is written to scratch memory before SHA3 runs:
For multi-word hashes, build a bytes value and use the SHA3-512 precompile (STATICCALL 0x01) directly or via a stdlib helper.
Checked arithmetic¶
Reverts on overflow / underflow / div-by-zero. See SafeMath for semantics.
Storage array operations¶
len(arr) -> uint
push(arr, v) -> uint // returns new length
pop(arr) -> uint // returns popped element; reverts if empty
First arg must be a bare storage array name.
Calldata construction¶
Allocates a memory region holding [length:32][selector_word:32][arg1:32].... Returns the pointer (memory offset) as the bytes value. Layout description in _emit_expr for pack_sel:
heap[off+0 .. off+32] = packed length (1 + N*32)
heap[off+32 .. off+64] = selector_word << 248 (top byte = selector)
heap[off+64 .. off+96] = arg1 (whole 32-byte word, but only first 31 of arg are used due to selector's prefix byte)
heap[off+96 .. off+128] = arg2
...
Note: arguments live at offsets 33, 65, 97, ... from the start of the data, matching the calldata layout the callee's dispatcher expects.
Cross-contract calls¶
call_b(addr, calldata: bytes, value, gas) -> uint // 1 = success, 0 = fail
delegatecall_b(addr, calldata: bytes, gas) -> uint
staticcall_b(addr, calldata: bytes, gas) -> uint
call(addr, calldata_word, value, gas) -> uint // one-word legacy form
Detail in Cross-contract calls.
PQC signature verify¶
scheme_id must be a literal int (not a variable). Known schemes:
scheme_id | Precompile | Algorithm |
|---|---|---|
1 | 0x02 | ML-DSA-87 |
2 | 0x03 | SLH-DSA-SHA2-128s |
Returns 1 if valid, 0 if invalid (the precompile call itself does not revert on bad signature; the return word distinguishes).
Parenthesized expressions¶
Standard. Parentheses around a comma-separated list become a tuple literal:
Tuples are only legal as the RHS of a let (...) = (...) or in return (...). Anywhere else → compile error.
Address-mismatch enforcement¶
Arithmetic and ordering ops between an address and a non-address local raise a compile error. See Types / Address vs uint enforcement.