Skip to main content

Variables and Bindings

Sector9 provides two ways to bind values to names: immutable let bindings and mutable var declarations. Choosing the right one affects both code clarity and verification.

Immutable Bindings with let​

The let keyword creates an immutable binding. Once assigned, the value cannot be changed:

let x : Nat = 42;
let name = "Alice"; // Type inferred as Text
let pair = (1, 2); // Type inferred as (Nat, Nat)

Attempting to reassign a let binding produces a type error:

let x = 10;
x := 20; // ERROR: expected mutable assignment target

Pattern Bindings​

let supports destructuring patterns for tuples:

let (a, b) = (10, 20);  // a = 10, b = 20
let (first, _, third) = (1, 2, 3); // Ignore middle with _

Pattern bindings must be irrefutable. Tuple and immutable-record destructuring are supported when the pattern cannot fail. Refutable patterns such as literals and options are rejected:

let (0, y) = pair;  // ERROR: refutable pattern not allowed
let (?x, y) = pair; // ERROR: option pattern can fail

let { x } = record; // OK when record has field x

Mutable Variables with var​

The var keyword creates a mutable binding that can be reassigned using :=:

var counter : Nat = 0;
counter := counter + 1; // Now counter = 1
counter += 1; // Shorthand: counter = 2

Mutable variables require simple identifiers. Unlike let, patterns are not allowed:

var (a, b) = (1, 2);  // ERROR: patterns not allowed with var

Local vs Actor-Level Variables​

Variables can be declared at two scopes:

Local Variables​

Declared inside function bodies, local variables exist only for that function call:

public func compute(n : Nat) : async Nat {
let doubled = n * 2; // Immutable local
var sum = 0; // Mutable local
sum := sum + doubled;
sum
}

Actor Fields​

Declared at actor level, these persist across function calls:

persistent actor {
var balance : Nat = 0; // Mutable actor field
let rate : Nat = 5; // Immutable actor field

public func deposit(amount : Nat) : async ()
modifies balance
{
balance += amount;
}
}

Actor fields marked with var require modifies clauses in functions that change them.

Mutable Records and Arrays​

Mutability also applies to record fields and arrays:

Mutable Record Fields​

Records can have mutable fields using var:

type Point = { var x : Int; var y : Int };

let p : Point = { var x = 0; var y = 0 };
p.x := 10; // OK: field is mutable
p.y := 20;

The binding p is immutable (cannot be reassigned), but its fields are mutable.

Mutable Arrays​

Arrays can be mutable or immutable:

let immutableArr = [1, 2, 3];     // Immutable array
let mutableArr = [var 1, 2, 3]; // Mutable array

mutableArr[0] := 99; // OK
immutableArr[0] := 99; // ERROR: immutable array

Assignment Operators​

Mutable variables support compound assignment:

OperatorMeaning
:=Simple assignment
+=Add and assign
-=Subtract and assign
*=Multiply and assign
/=Divide and assign
%=Modulo and assign
var n : Nat = 10;
n += 5; // n = 15
n -= 3; // n = 12
n *= 2; // n = 24

When to Use Each​

Prefer let When​

  • The value should not change after initialization
  • You want the verifier to track that a value is constant
  • You are destructuring tuples or capturing intermediate results

Use var When​

  • The value must change during execution (counters, accumulators)
  • You need actor-level persistent state
  • The algorithm requires updating values in loops

Verification Implications​

Immutable Bindings are Simpler to Verify​

The verifier can substitute let bindings directly:

let x = a + b;
assert x == a + b; // Trivially true

Mutable Variables Require State Tracking​

Changes to var bindings must be reasoned about explicitly:

var x = 0;
x := x + 1;
assert x == 1; // Verifier tracks the update
x := x + 1;
assert x == 2; // Verifier tracks again

Actor Fields Need Footprints​

Functions that modify actor-level var fields must declare them in modifies:

persistent actor {
var counter : Nat = 0;

public func increment() : async Nat
modifies counter
ensures counter == old(counter) + 1
{
counter += 1;
counter
}
}

Loop Invariants​

Mutable variables modified in loops often require explicit loop invariants:

var sum = 0;
var i = 0;
while (i < n) {
loop:invariant i >= 0 and i <= n;
loop:invariant sum == i * (i + 1) / 2;
i += 1;
sum += i;
}

Ghost Variables​

For verification-only state, use ghost var:

persistent actor {
var balance : Nat = 0;
ghost var operations : Nat = 0; // Verification only

invariant operations >= 0;

public func deposit(amount : Nat) : async ()
modifies balance, operations
{
balance += amount;
ghost { operations += 1; };
}
}

Ghost variables are erased from runtime code but available for proofs. See Ghost Variables for details.

Type Annotations​

Type annotations are optional when the type can be inferred:

let x = 42;           // Inferred as Nat
let y : Int = 42; // Explicitly Int
var z = 0; // Inferred as Nat
var w : Int64 = 0; // Explicitly Int64

For actor fields, explicit types are recommended for clarity:

persistent actor {
var counter : Nat = 0; // Clear intent
var flag : Bool = false; // Explicit type
}

Common Mistakes​

Forgetting var for Mutable Fields​

// WRONG - field is immutable
type Cell = { value : Int };
let c : Cell = { value = 0 };
c.value := 10; // ERROR

// CORRECT
type Cell = { var value : Int };
let c : Cell = { var value = 0 };
c.value := 10; // OK

Missing modifies Clause​

persistent actor {
var x : Nat = 0;

// WRONG - missing modifies
public func update() : async () {
x := 1; // ERROR: no write permission
}

// CORRECT
public func update() : async ()
modifies x
{
x := 1;
}
}

Using Complex Patterns with var​

// WRONG - var doesn't support patterns
var (a, b) = (1, 2);

// CORRECT - use separate declarations
var a = 1;
var b = 2;

Summary​

  • let creates immutable bindings that cannot be reassigned
  • var creates mutable bindings that can be updated with :=
  • let supports irrefutable tuple and record patterns; var requires simple identifiers
  • Actor-level var fields require modifies clauses in functions that write them
  • Record fields and arrays can independently be mutable with var
  • Prefer let for simpler verification; use var when mutation is needed
  • Use ghost var for verification-only state