Statements
A block is a sequence of statements in braces, each ending with ;:
| Statement | Form |
|---|---|
| Immutable binding | const name : type := expr; |
| Tuple binding | const (a, b) : (t, u) := expr;, var (a, b) : (t, u) := expr; |
| Mutable binding | var name : type := expr; |
| Assignment | name := expr;, storage.path := expr; |
| Call | f(args);, Module.f(args); for a function that returns nothing; const _ : T := f(args); drops a result |
| Conditional | if cond then { ... } else if cond then { ... } else { ... } |
| Loop | for x : T in xs [until cond] [bound n] do { ... }, or over lo .. hi |
| Tuple assignment | (a, b) := expr; |
| Assertion | assert(cond, Error); |
| Failure | fail Error;, or forward reason; for a failed call's bytes |
| Event | emit Event { field = value; }; |
| Deferred call | defer target.method(args);, with optional with value = v, with gas = g, and then (...) { ... } |
| No-op | skip |
Bindings
const binds a value that never changes. var binds a value that := can reassign, whole or in part: a record's field (p.x := v) or a list's element (xs[i] := v). Either can hold any value, records, tuples, fixed lists, and variants included, and either can name a tuple's elements: const (a, b) : (t, u) := e; or var (a, b) : (t, u) := e;. Every binding states its type, and names cannot shadow other names in scope.
storage is record {
total : uint;
}
external function add(const a : uint; const b : uint) is {
const sum : uint := a + b;
var bonus : uint := 0;
if sum > 100 then {
bonus := 1;
}
storage.total := storage.total + sum + bonus;
}storage is record {
count : uint;
}
external function go() is {
const taken := storage.count;
}local bindings require a typeAssignment
:= writes a local var, a mutable parameter, or a storage location. Storage locations are always qualified:
| Target | Example |
|---|---|
| default storage | storage.count := 1; |
| a map entry | storage.balances[who] := 0; |
| a nested map entry | storage.allowances[owner][spender] := n; |
| a field of a stored record | storage.orders[id].status := uint8(2); |
| named storage | config.fee := 30; |
| a module's storage, from outside it | Ledger.ledger.total := 0; |
An unqualified storage name is an error:
storage is record {
paid : uint;
}
external function pay(const to : address) is {
paid := paid + 1;
}storage field `paid` must be qualified as `storage.paid`delete
delete place; writes the place's zero value: what storage that was never written reads as. Numbers become 0, bool false, addresses Address.zero, strings, bytes, and lists empty, options None, a variant its first case with a zero payload, and a record, tuple, or fixed list each of its parts' zero values:
delete storage.balances[who];
delete storage.orders[id];
delete storage.orders[id].note;
delete storage.pending[i];It is exactly place := zero, so it goes wherever := does, including a var or a T inside a generic module, and nowhere else: a whole map, or a record holding one, has no value to write, and a const can't be written. In storage, it clears every slot the old value used, a long string's data and a list's elements included, so each cleared slot earns the refund for zeroing storage.
storage is record {
balances : map(address, uint);
}
external function reset() is {
delete storage.balances;
}a map cannot be assigned or deletedif
if balance >= amount then {
storage.balance := balance - amount;
} else if storage.overdraft then {
storage.balance := 0;
} else {
fail Insufficient;
}The condition must be a bool. Both branches are blocks, and else if chains another if. To choose a value instead, use an if expression.
assert and fail
assert(cond, Error) reverts with Error when cond is false. fail Error reverts unconditionally. Both take a declared error, never a string:
assert(Evm.sender() = storage.owner, NotOwner);
assert(amount <= limit, TooLarge(amount));
fail InsufficientBalance { available = have; required = amount; };The error must appear in the function's raises clause, or be inferable for an internal function. See Errors.
forward reason reverts with the bytes in reason exactly, to pass a failed call's revert data on. The function raises forwarded. See Forwarded failures.
emit and defer
emit records an event and defer records an external call. Both are statements. See Events and Queries and deferred calls.
skip
skip does nothing. It is useful for an empty branch or body:
| Stop -> { skip }for
A for loop runs its body once per element of a list, or once per number in a half-open range. The number of iterations is fixed when the loop starts, so the gas a loop can use is visible in the source:
storage is record {
owners : list(address, 3);
isOwner : map(address, bool);
total : uint;
}
external function mark() is {
for owner : address in storage.owners do {
storage.isOwner[owner] := true;
}
}
external function triangle(const n : uint) is {
var acc : uint := 0;
for i : uint in 0 .. n do {
acc := acc + i;
}
storage.total := acc;
}| Form | Iterates |
|---|---|
for x : T in xs do { ... } | each element of a list, fixed or dynamic, in storage or local |
for (i, x) : (uint, T) in xs do { ... } | each element with its index |
for i : T in lo .. hi do { ... } | each integer from lo up to, not including, hi |
The loop variables are const, bound afresh each iteration, and typed like every other binding. _ binds nothing. A range's ends have the loop variable's type, which can be any integer type. Maps cannot be iterated: keep an enumerable set.
The count is read once, at entry. A dynamic list that the body appends to does not extend the loop. There is no while, and a function cannot call itself.
until
until cond is checked before each iteration, including the first, and ends the loop when it is true. It is the only early exit:
var hit : uint := 0;
for (i, who) : (uint, address) in storage.members until hit <> 0 do {
if who = target then {
hit := i + 1;
}
}bound
bound n caps the number of iterations at a number fixed when the contract is compiled. A longer list, or a wider range, reverts with Panic(0x01) before the loop starts:
for who : address in storage.members bound 50 do {
storage.isMember[who] := true;
}A loop over a storage list, or a range whose ends come from storage, can grow with the contract's state until an entrypoint no longer fits in a block. The compiler warns about such a loop unless it carries a bound:
warning: the number of iterations depends on storage, so this
loop's gas grows as the collection grows and can exceed the block limit
consider `bound N` to cap itA loop over a parameter is not warned about: the caller chose its size and pays for it. A fixed list's length is in its type.
Effects in a loop
emit, defer, query, and continuations all work inside a loop. A defer records one call per iteration, in order, and a then block sees the bindings of the iteration that deferred it:
for (i, amount) : (uint, uint) in amounts do {
defer storage.exchange.swap(amount) then (out : uint) {
storage.results[i] := out;
};
}Tuple assignment
(a, b) := expr; assigns each element of a tuple to a var, at once, so two values can be stepped together. var (a, b) : (t, u) := expr; binds them the same way:
var (x, y) : (uint, uint) := (a, b);
for _ : uint in 0 .. 256 until y = 0 do {
(x, y) := (y, x % y);
}Function bodies
A function body is one of:
- an expression:
is a + b - a block:
is { ... } - a block followed by a result:
is { ... } with expr - a guard block followed by a
case:is { ... } with case x of { ... }
Names bound in the block are in scope in the with expression.

