Expressions and operators
Operators
| Operator | Meaning | Operands |
|---|---|---|
+ - * | add, subtract, multiply (checked) | two values of the same numeric type |
/ % | divide, remainder (truncating; checked) | two values of the same numeric type |
** | power (checked) | an integer base and an unsigned exponent; the result has the base's type |
-x | negate (checked: -Int.min panics) | int types |
= <> | equal, not equal (=/= also means not equal) | two values of the same type with no map in them; strings, bytes, lists, and what holds them compare by content |
< <= > >= | ordered comparison | numeric types, wei, timestamp, and address and bytesN, which order as unsigned numbers, as in Solidity |
and or not | boolean logic | bool |
& | ^ ~ | bitwise and, or, xor, not | integers of the same type |
<< >> | shift left, right (arithmetic for signed types) | integers |
^ is xor. ** is the power: 10 ** decimals for a uint8 decimals is a uint unless its context expects another integer type, and 0 ** 0 is 1.
Precedence
From tightest to loosest:
| Level | Operators | Associativity |
|---|---|---|
| 1 | ** | right: a ** b ** c is a ** (b ** c) |
| 2 | prefix -, not, ~ | |
| 3 | * / % | left |
| 4 | + - | left |
| 5 | << >> | left |
| 6 | & | left |
| 7 | ^ | left |
| 8 | | | left |
| 9 | = <> < <= > >= | none: a < b < c is an error |
| 10 | and | left |
| 11 | or | left |
Bitwise operators bind tighter than comparisons, so flags & mask = 0 means (flags & mask) = 0. Prefix operators bind tighter than everything but **, so not a = b means (not a) = b and fails to type-check: write a <> b or not (a = b). And -x ** 2 means -(x ** 2).
Arithmetic
Every arithmetic operation is checked against its type's width:
| Failure | Result |
|---|---|
| overflow or underflow | Panic(0x11) |
| division or remainder by zero | Panic(0x12) |
Int.min / -1 | Panic(0x11) |
Division truncates toward zero, so -7 / 3 is -2 and -7 % 3 is -1. To get both results at once, use Uint.divRem(a, b) or Int.divRem(a, b), which return a tuple.
A power is checked the same way, and a negative base works: (-2) ** 3 is -8. A power of constants, such as 10 ** 18, compiles to the number itself.
When every operand is a constant, the check runs at compile time: an operation that must fail is a compile error, not a panic. The constants are number literals, Uint.max, Int.min, and Int.max, named constants, const bindings of constants, and casts and operators over those. So Uint.max + 1, 1 / 0, 2 ** 256, uint8(300), -Int.min, and Uint.mulDiv(1, 1, 0) are all rejected, even in a branch that never runs. Parameters are never constants, and neither is what a condition proves: if x = 0 then 1 / x still panics at run time, and so does Math.ceilDiv(1, 0), since its body divides by a parameter.
storage is record {
n : uint;
}
external function go() is {
storage.n := 2 ** 256;
}`2 ** 256` overflows `uint`Shifts do not overflow. Bits shifted past a type's width are dropped, so for a uint8, 1 << 8 is 0. ~ flips bits within the type's width, so for a uint8, ~0 is 255.
import "std/uint";
storage is record {
q : uint;
r : uint;
masked : uint;
narrow : uint8;
}
external function run(const a : uint; const b : uint) is {
const (q, r) : (uint, uint) := Uint.divRem(a, b);
storage.q := q;
storage.r := r;
storage.masked := (a >> 8) & 0xff;
const x : uint8 := ~0;
storage.narrow := x ^ 0x0f;
}Field access, indexing, and calls
| Expression | Meaning |
|---|---|
r.field | a record field |
xs[i], m[k], m[a][b] | a list element or map entry |
b[i], b[start:end] | a byte of bytes or bytesN, and a slice of bytes. See Bytes and String |
xs.length | the length of a dynamic list, bytes, or string |
f(a, b) | call an internal function |
Module.f(a) | call a module's function |
Evm.sender() | call a built-in |
query c.f(a) | call an external contract's read-only method |
Storage paths can be as deep as the types allow: storage.orders[id].inner.delta.
case expressions
case matches a variant or option and yields a value. Every case must be handled. There is no wildcard.
storage is record {
limit : option(uint);
}
external view function limitOr(const fallback : uint) : uint is
case storage.limit of {
| Some(n) -> n
| None -> fallback
}A case can be a function's whole body, the with result after a block, or the initializer of a const. Payloads bind to names, and tuple payloads destructure: | Some((a, b)) -> a + b.
The first arm's | is optional, so a short match fits on one line: case p of { Open | Paused -> 1 | Closed -> 0 }. Every later arm starts with |.
One arm can cover several cases, | New | Pending -> 0. It binds a payload when each case binds the same names and carries the same payload type, | Queued(why) | Held(why) -> why. An arm can also be fail E, which fits any other arm's type:
type Phase is Open | Paused | Closed
error Shut;
storage is record {
phase : Phase;
fee : uint;
}
external view function fee() : uint raises Shut is
case storage.phase of {
| Open | Paused -> storage.fee
| Closed -> fail Shut
}In a stateful function, an arm can be a block of statements instead of a value. This is how dispatchers are written:
external function main(const action : Action) is
case action of {
| Deposit(amount) -> {
storage.total := storage.total + amount;
}
| Reset -> { storage.total := 0; }
}if expressions
if cond then a else b chooses a value. It goes anywhere a case can. else is required, both branches have the same type, and else if chains:
error Zero;
storage is record {
n : uint;
}
external pure function max(const a : uint; const b : uint) : uint is
if a > b then a else b
external pure function tier(const x : uint) : uint is {
const t : uint := if x < 10 then 1 else if x < 100 then 2 else 3;
} with t
external pure function nonzero(const x : uint) : uint raises Zero is
if x > 0 then x else fail ZeroIts branches are case arms, so a branch can be fail E, or a block. A block that ends with a value gives the branch that value, as a function body does:
external pure function scaled(const x : uint) : uint is
if x > 1000 then {
const k : uint := x / 1000;
} with k * k else xThere is no ternary operator.

