Skip to main content

Arrays

Arrays are indexed runtime collections with a fixed size at creation time. Sector9 supports both mutable arrays [var T] that allow element updates and immutable arrays [T] that cannot change after creation. For proof-only sequence models, use Seq instead.

Creating Arrays

Use square brackets to create array literals:

array-basic.sr9
// Basic array creation and access
persistent actor {
// Immutable array - cannot change elements after creation
let immutableArr : [Nat] = [1, 2, 3, 4, 5];

// Mutable array - can update elements
var mutableArr : [var Nat] = [var 10, 20, 30];
invariant mutableArr.size() == 3;

// Empty arrays
let emptyImmutable : [Nat] = [];
var emptyMutable : [var Nat] = [var];

public func basics() : async Nat
reads immutableArr, mutableArr
{
// Read from immutable array
let first = immutableArr[0]; // 1
let second = immutableArr[1]; // 2

// Read from mutable array
let mFirst = mutableArr[0]; // 10

// Get array size
let size = immutableArr.size(); // 5

assert size == 5;

first + mFirst // returns 11
};
}

Key points:

  • [T] is an immutable array of elements of type T
  • [var T] is a mutable array where elements can be updated
  • Literal syntax: [1, 2, 3] for immutable, [var 1, 2, 3] for mutable
  • Empty arrays: [] for immutable, [var] for mutable
  • All elements must have compatible types
  • Use arr[i] to read element at index i (index type is Nat)
  • Use arr.size() to get the array length

Mutability

Array mutability determines whether elements can be updated after creation:

array-mutation.sr9
// Updating mutable array elements
persistent actor {
var data : [var Nat] = [var 1, 2, 3];

public func updateElement(i : Nat, value : Nat) : async ()
modifies data
entry_requires i < data.size();
requires i < data.size();
ensures data[i] == value;
{
// Update using assignment
data[i] := value;
};

public func swap(i : Nat, j : Nat) : async ()
modifies data
entry_requires i < data.size() and j < data.size();
requires i < data.size();
requires j < data.size();
ensures data[i] == old(data[j]);
ensures data[j] == old(data[i]);
{
let temp = data[i];
data[i] := data[j];
data[j] := temp;
};
}

Mutability rules:

  • arr[i] := value updates element at index i (mutable arrays only)
  • Immutable arrays cannot be updated after creation
  • modifies arr grants permission to update any element
  • old(arr[i]) captures the element value at method entry

The mutability of an array is part of its type:

  • A mutable array [var Nat] cannot be assigned to an immutable [Nat]
  • The binding mutability (let vs var) is separate from array element mutability

Bounds Checking

Array access requires proving the index is within bounds. The verifier tracks array sizes and requires explicit bounds checks:

array-bounds.sr9
// Array bounds checking in verification
persistent actor {
var data : [var Nat] = [var 10, 20, 30];
invariant data.size() == 3;

// Correct: bounds check in precondition
public func safeGet(i : Nat) : async Nat
reads data
entry_requires i < data.size();
requires i < data.size();
ensures result == data[i];
{
data[i]
};

// Using size in contracts
public func getFirst() : async Nat
reads data
entry_requires data.size() > 0;
requires data.size() > 0;
ensures result == data[0];
{
data[0]
};

// Size preservation after updates
public func setFirst(value : Nat) : async ()
modifies data
ensures data.size() == old(data.size());
ensures data[0] == value;
{
data[0] := value;
};
}

Bounds patterns:

  • requires i < data.size() - precondition guards array access
  • invariant data.size() == N - fixed-size invariant for actor arrays
  • ensures data.size() == old(data.size()) - size preserved after update
  • Without a bounds guard, verification fails with a permission error because the verifier cannot prove the indexed heap location exists

Iterating with for-in

Use for-in loops with .values() to iterate over array elements:

array-iteration.sr9
// Iterating over arrays with for-in loops
persistent actor {
var data : [var Nat] = [var 1, 2, 3, 4, 5];
invariant data.size() == 5;

// Sum all elements using for-in
public func sum() : async Nat
reads data
ensures result >= 0;
{
var total : Nat = 0;
for (x in data.values()) {
loop:invariant total >= 0;
total += x;
};
total
};

// Count elements matching a condition
public func countPositive() : async Nat
reads data
{
var count : Nat = 0;
for (x in data.values()) {
loop:invariant count <= data.size();
if (x > 0) {
count += 1;
};
};
count
};
}

Loop rules:

  • Use arr.values() to iterate over elements
  • Add loop:invariant to specify properties preserved across iterations
  • The loop variable receives each element value in order

Quantifiers and Arrays

Use forall and exists to express properties over array indices:

array-quantifier.sr9
// Using quantifiers with arrays
persistent actor {
var data : [var Nat] = [var 1, 2, 3, 4, 5];
invariant data.size() == 5;
// All elements are positive
invariant forall<Nat>(pure func (i : Nat) : Bool =
(i < data.size()) ==> (data[i] >= 1)
);

// Assert all elements satisfy a property
public func allPositive() : async Bool
reads data
{
let n = data.size();
assert forall<Nat>(pure func (i : Nat) : Bool =
(i < n) ==> (data[i] >= 1)
);
true
};

// Check if any element matches
public func hasValue(target : Nat) : async Bool
reads data
{
var found = false;
for (x in data.values()) {
if (x == target) {
found := true;
};
};
found
};
}

Quantifier patterns:

  • forall<Nat>(pure func (i : Nat) : Bool = (i < n) ==> property(arr[i])) - all indices
  • exists<Nat>(pure func (i : Nat) : Bool = (i < n) and arr[i] == target) - some index
  • Always guard with i < arr.size() using implication (==>) for forall or conjunction (and) for exists
  • The quantified function must be pure

Footprints

Declare array access in function footprints:

public func readAll() : async Nat
reads data // Read permission for all elements
{ ... };

public func updateAll() : async ()
modifies data // Write permission for all elements
{ ... };

public func copyTo(dest : [var Nat]) : async ()
reads data // Read source
modifies dest // Write destination
{ ... };

Footprint rules:

  • reads arr grants read access to all elements of arr
  • modifies arr grants read and write access to all elements
  • Footprints apply to the entire array, not individual indices
  • Frame conditions: unlisted fields remain unchanged across calls

Verification Behavior

The verifier models arrays with indexed heap locations:

MotokoViper Translation
arr[i] read$loc(arr, i).$array$T
arr[i] := v write$loc(arr, i).$array$T := v
arr.size()$size(arr)
modifies arr$array_acc(arr, write)

Arrays generate bounds predicates for numeric element types (Nat, Int, fixed-width types). The verifier automatically assumes elements satisfy type bounds (for example, Nat elements are non-negative and fixed-width elements stay inside their checked ranges). See numeric bounds for the same proof shape outside arrays.

Verification Failure Example

Missing bounds checks cause verification failures:

array-missing-bounds_unsat.sr9
// Verification failure: missing bounds check
persistent actor {
var data : [var Nat] = [var 10, 20, 30];
invariant data.size() == 3;

// ERROR: No bounds check - verification fails
public func unsafeGet(i : Nat) : async Nat
reads data
// Missing: requires i < data.size()
ensures result == data[i];
{
data[i] // Verification fails: cannot prove i < size
};
}

Without requires i < data.size(), the verifier cannot prove the access is safe. The error appears as "insufficient permission to access" because unbounded indices may exceed the array size.

Restrictions

For-In Loop Iterators

arr.values() is special-cased in verification and automatically introduces array permissions and index bounds. Other iterator forms (like arr.keys() or arr.vals()) are supported if the iterator's next() method has a type contract, but they do not introduce array-specific invariants. Add your own invariants or use a while loop when you need bounds facts.

// Recommended: values() gives array-aware invariants
for (x in arr.values()) { ... }

// Supported: iterator form, but you must add your own bounds reasoning
for (k in arr.keys()) { ... }

To iterate with indices and explicit bounds, use a while loop:

var i : Nat = 0;
while (i < arr.size()) {
loop:invariant i <= arr.size();
let x = arr[i];
// use x and i
i += 1;
};

Pure and Spec Restrictions

Named pure functions cannot close over actor arrays or index mutable arrays from hidden state:

var data : [var Nat] = [var 1, 2, 3];

// ERROR M0317: pure function cannot index arrays
pure func getFirst() : Nat { data[0] };

Pure functions can use their parameters and call other pure functions. For quantified array properties, use a guarded anonymous pure predicate such as i < arr.size() ==> arr[i] >= 0; the verifier supplies the array permission from the surrounding contract or assertion.

Nested Arrays

Nested mutable arrays have limited support:

// Supported: nested immutable arrays
let matrix : [[Nat]] = [[1, 2], [3, 4]];

// Limited: nested mutable arrays may have aliasing issues
var grid : [var [var Nat]] = [var [var 1, 2], [var 3, 4]];

For complex multi-dimensional data, consider flattening:

var flat : [var Nat] = [var 0, 0, 0, 0]; // 2x2 matrix
let cols = 2;

pure func index(row : Nat, col : Nat) : Nat { row * cols + col };

public func set(row : Nat, col : Nat, v : Nat) : async ()
modifies flat
entry_requires index(row, col) < flat.size();
requires index(row, col) < flat.size();
{
flat[index(row, col)] := v;
};

Mutable Record Arrays

Arrays of mutable records are no longer a blanket unsupported case. Current verification support covers the owned/fresh lanes used by the core library and the ownership tests, including local mutable-record arrays and some opaque handle payloads. Deeply nested or escaping shapes still need explicit ownership and aliasing proofs, and some shapes remain outside the supported verifier lane.

type Cell = { var value : Nat };

// Supported in the fresh local lane.
var cells : [var Cell] = [var { var value = 0 }];
cells[0].value := 1;

When this gets hard to verify, prefer a flattened representation or keep the mutable records behind a small module API with explicit contracts.

Spec Collection Restrictions

Mutable arrays cannot be elements of spec collections (Seq, Set, Map, Multiset):

// ERROR: mutable arrays cannot be Seq elements
let s : Seq<[var Nat]> = Seq.empty();

Spec collections require immutable element types for equality stability.

Array vs Seq

FeatureArray ([T], [var T])Seq (Seq<T>)
PurposeRuntime data storageVerification reasoning
MutabilityMutable with [var T]Always immutable
Index typeNatInt
LocationHeap-allocatedGhost state only
Footprintsreads/modifies requiredNo footprints needed
OperationsIndex, update, iterateConcat, slice, contains

Use arrays for runtime data. Use Seq in ghost blocks and contracts for reasoning about sequences.

Error Codes

CodeMeaning
M0317Pure function cannot index arrays
M0242Mutable array in spec collection element

Summary

  • [T] is immutable; [var T] is mutable
  • Create with literals: [1, 2, 3] or [var 1, 2, 3]
  • Access elements with arr[i] where i : Nat
  • Get length with arr.size()
  • Update mutable arrays with arr[i] := value
  • Declare footprints: reads arr or modifies arr
  • Always guard access with bounds checks: requires i < arr.size()
  • Use for (x in arr.values()) to iterate
  • Use forall<Nat>(...) with i < size ==> guard for array properties
  • Named pure functions cannot close over actor arrays; guarded anonymous pure predicates are the usual way to write quantified array facts
  • Nested mutable arrays and mutable record arrays have limited support