Skip to content
LogoLogo

Expressions and operators

Operators

OperatorMeaningOperands
+ - *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
-xnegate (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 comparisonnumeric types, wei, timestamp, and address and bytesN, which order as unsigned numbers, as in Solidity
and or notboolean logicbool
& | ^ ~bitwise and, or, xor, notintegers 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:

LevelOperatorsAssociativity
1**right: a ** b ** c is a ** (b ** c)
2prefix -, not, ~
3* / %left
4+ -left
5<< >>left
6&left
7^left
8|left
9= <> < <= > >=none: a < b < c is an error
10andleft
11orleft

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:

FailureResult
overflow or underflowPanic(0x11)
division or remainder by zeroPanic(0x12)
Int.min / -1Panic(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

ExpressionMeaning
r.fielda 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.lengththe 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 Zero

Its 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 x

There is no ternary operator.