Aliasing Restrictions
Mutable records use reference semantics, so multiple variables can point to the same object. This page explains what aliasing patterns are supported, how the verifier handles aliased parameters, and what patterns are rejected.
What Aliasing Means
Two variables alias when they reference the same mutable record:
let c1 : Cell = { var x = 0 };
let c2 = c1; // c2 aliases c1
c2.x := 99;
assert c1.x == 99; // Both see the update
Aliasing is sound but requires the verifier to handle it correctly. The core challenge: when two parameters might alias, updates through one affect the other.
Conditional Postconditions
When a function takes multiple mutable record parameters of the same type, the verifier handles the ownership requirements conditionally. If two parameters may be the same object, the generated permissions and postconditions have to account for both aliased and distinct cases:
// Aliased parameters: calling with the same object twice
persistent actor {
type Cell = { var x : Int };
var cell : Cell = { var x = 0 };
// Postconditions handle both aliased and distinct cases
private func increment_both(c1 : Cell, c2 : Cell) : ()
ensures c1 == c2 ==> c1.x == old(c1.x) + 2;
ensures c1 != c2 ==> c1.x == old(c1.x) + 1;
ensures c1 != c2 ==> c2.x == old(c2.x) + 1;
{
c1.x := c1.x + 1;
c2.x := c2.x + 1;
};
// Call with same object - verifies correctly
public func test_aliased() : async ()
modifies cell
ensures cell.x == old(cell.x) + 2;
{
increment_both(cell, cell);
};
}
The function increment_both has two postcondition forms:
c1 == c2 ==> ...— what happens if both parameters point to the same objectc1 != c2 ==> ...— what happens if they point to different objects
This pattern handles self-aliased calls like increment_both(cell, cell) correctly.
Explicit Disjointness
When aliasing is not intended, require distinct parameters explicitly:
// Requiring distinct parameters with explicit precondition
persistent actor {
type Cell = { var x : Int };
// Function requires parameters to be distinct
private func swap(c1 : Cell, c2 : Cell) : ()
requires c1 != c2;
ensures c1.x == old(c2.x);
ensures c2.x == old(c1.x);
{
let tmp = c1.x;
c1.x := c2.x;
c2.x := tmp;
};
public func test_swap() : async () {
let a : Cell = { var x = 10 };
let b : Cell = { var x = 20 };
swap(a, b); // Fresh allocations are always distinct
assert a.x == 20;
assert b.x == 10;
};
}
The requires c1 != c2 precondition tells callers they cannot pass the same object twice. With this precondition, the verifier can use the simpler disjoint-ownership path. See requires for caller obligations.
Fresh Allocations Are Distinct
Freshly allocated records are guaranteed to not alias any existing reference:
// Fresh allocations are guaranteed distinct
persistent actor {
type Cell = { var x : Int };
var existing : Cell = { var x = 42 };
public func test_fresh_distinct() : async () modifies existing {
let fresh : Cell = { var x = 0 };
// Fresh allocations do not alias existing references
let ex0 = existing.x;
fresh.x := 99;
assert existing.x == ex0; // existing unchanged by fresh.x update
assert fresh.x == 99;
};
}
The verifier knows that each { var ... } expression creates a new, unique object. This guarantees:
- Fresh allocations are distinct from each other
- Fresh allocations are distinct from actor fields
- Updates to one do not affect the other
How the Verifier Handles Aliasing
The verifier uses partition-based alias reasoning:
- Group parameters by their mutable record type
- Generate all partitions of these parameters (aliased vs. distinct combinations)
- Compute effects for each partition based on which updates share targets
- Emit guarded facts that are valid for that partition
For a function with two Cell parameters, the verifier considers:
- Both same object:
c1 == c2 - Both distinct:
c1 != c2
For three parameters, it considers all groupings: all same, all distinct, pairs matching, etc.
Example: How Partitions Work
Consider:
private func update(c1 : Cell, c2 : Cell) : ()
{
c1.x := c1.x + 1;
c2.x := c2.x + 10;
};
The verifier generates:
| Condition | Effect on c1.x | Effect on c2.x |
|---|---|---|
c1 == c2 | old(c1.x) + 11 | (same as c1) |
c1 != c2 | old(c1.x) + 1 | old(c2.x) + 10 |
When aliased, both updates apply to the same object sequentially. When distinct, each object receives its own update.
Aliasing and Await
For aliasing behavior across await boundaries, see Local vs Shared State. The key points:
- Fresh allocations that don't escape remain stable across await
- Aliases to actor fields may not remain live across await; copy primitive data first or re-read through actor state after the await
- Escaping a fresh allocation to an actor field makes it shared
Restrictions
Cannot Pattern-Match Mutable Fields
Pattern matching on var fields is rejected because mutable fields are heap locations, not destructurable value fields:
// Cannot pattern-match mutable record fields
persistent actor {
type Cell = { var x : Int };
public func bad() : async () {
let cell : Cell = { var x = 10 };
// ERROR: cannot pattern match mutable field x
let { x } = cell;
};
}
The error is:
type error [M0120], cannot pattern match mutable field x
Access mutable fields directly instead:
let value = cell.x; // Direct field access
Cannot Use in Spec Collections
Mutable records cannot be elements of spec collections such as Set, Seq, Map, or Multiset:
// Mutable records cannot be used in spec collections
persistent actor {
type Cell = { var x : Int };
public func bad() : async () {
ghost {
// ERROR: Set element type must be immutable
let s : Set<Cell> = Set.empty();
};
};
}
The error is:
type error [M0242], Set element type must be immutable (field x is mutable; no var fields or mutable arrays)
This restriction exists because:
- Set membership must be stable (mutable elements break equality)
- Aliasing to collection elements creates unsound proof scenarios
- Collection operations (insert, remove, contains) assume immutable identity
Arrays of Mutable Records
Arrays of mutable records have a supported local lane, including cases where a mutable record owns a mutable array of mutable records:
type Cell = { var value : Nat };
type Box = { var items : [var Cell] };
let b : Box = { var items = [var { var value = 0 }] };
The limits are around unbounded aliasing and escape, not the existence of the array itself. Keep mutable-record arrays owned locally, add loop invariants when iterating, and avoid storing aliases in actor fields across awaits. When the shape becomes hard to prove, use a spec collection for the proof model and a runtime array for implementation state.
Aliasing Patterns
Supported: Self-Aliased Calls
Calling a function with the same object for multiple parameters:
add_to_both(cell, cell); // Same object twice
Works when postconditions handle the c1 == c2 case.
Supported: Known-Distinct Calls
Calling with objects guaranteed distinct:
let a : Cell = { var x = 0 };
let b : Cell = { var x = 0 };
swap(a, b); // Fresh allocations are distinct
Supported: Explicit Preconditions
Requiring disjointness when needed:
requires c1 != c2; // Caller must prove distinct
Unsupported: Unbounded Aliasing
The verifier cannot track aliasing across:
- Arbitrary collections of mutable records
- Recursive data structures with mutable nodes
- Callbacks with captured mutable references
For these patterns, expose a smaller verified interface, add explicit disjointness preconditions, or use ghost modeling. If you need a proof-only boundary while developing a larger proof, prefer abstract declarations; reserve trusted declarations for assumptions you are intentionally accepting.
Common Mistakes
Missing Conditional Postcondition
// WRONG: assumes parameters are distinct
private func add(c1 : Cell, c2 : Cell) : ()
ensures c1.x == old(c1.x) + 1;
ensures c2.x == old(c2.x) + 1; // Fails if c1 == c2
{
c1.x := c1.x + 1;
c2.x := c2.x + 1;
};
Fix: add guarded postconditions for the aliased case or require disjointness with requires c1 != c2.
Assuming Fresh After Escape
// WRONG: Fresh escapes to field
let fresh : Cell = { var x = 0 };
actorField := fresh; // fresh now aliases actor state
try { await pause(); } catch (_) {};
assert fresh.x == 0; // Rejected/fails: fresh is now shared
Fix: do not keep or rely on mutable references after storing them in actor fields across an await; copy primitive fields first or re-read through actor state after the await.
See Also
- Mutable Record Types - What makes a record mutable
- Ownership Predicates - How permissions are tracked
- Local vs Shared State - Aliasing at await boundaries
- Interference Model - What can change during await
Summary
- Multiple variables can alias the same mutable record
- The verifier uses conditional postconditions to handle both aliased and distinct cases
- Fresh allocations are guaranteed distinct from each other and existing references
- Use
requires c1 != c2to require disjointness when aliasing is not intended - Pattern matching on mutable fields is rejected
- Mutable records cannot be spec collection elements
- Aliasing behavior changes at await boundaries based on escape analysis
Related Topics
- Permission errors for missing ownership or field access failures
- Collection linearity restrictions for immutable collection elements
- Pattern exhaustiveness for supported pattern forms