The pure Modifier
The pure modifier declares a function as side-effect free, enabling its use in verification specifications like contracts and invariants.
Basic Syntax
pure func double(x : Int) : Int {
x * 2
}
The pure keyword appears before func. Pure functions can use their parameters, immutable locals, fields of parameter values, and other pure or trusted-pure helpers. They cannot read actor/module state, declare mutable locals, modify state, await, assert, trap, or produce side effects.
What Pure Functions Mean
Pure functions are mathematical functions: given the same inputs, they always return the same output. The verifier uses this property to reason about specifications.
pure func max(a : Int, b : Int) : Int {
if (a > b) { a } else { b }
}
persistent actor {
var value : Int = 0;
public func clampedSet(x : Int) : async ()
modifies value
ensures value == max(0, x);
{
value := max(0, x);
}
}
Because max is pure, the verifier knows max(0, x) in the ensures clause equals max(0, x) in the body.
Declaring Pure Functions
In Modules
module Math {
public pure func abs(x : Int) : Int {
if (x < 0) { -x } else { x }
}
public pure func min(a : Int, b : Int) : Int {
if (a < b) { a } else { b }
}
public pure func clamp(x : Int, lo : Int, hi : Int) : Int {
max(lo, min(x, hi))
}
}
Pure functions can be private or public. Public pure functions can be called from other modules in contracts.
In Actors
persistent actor {
var balance : Int = 100;
pure func doubled(x : Int) : Int {
x * 2
}
public func addDoubled(amount : Int) : async Int
modifies balance
ensures result == old(balance) + doubled(amount);
{
balance := balance + doubled(amount);
balance
}
}
Pure functions in actors cannot access actor fields. They operate only on their parameters.
Pure Functions with Contracts
Pure functions can have requires and ensures clauses:
pure func safeSub(a : Int, b : Int) : Int
requires a >= b;
ensures result == a - b;
{
a - b
}
pure func safeDivide(a : Int, b : Int) : Int
requires b > 0;
ensures result * b <= a;
{
a / b
}
When you call a pure function, including from another contract clause, the verifier checks its preconditions at that call site.
Calling Pure Functions
Pure Calling Pure
Pure functions can call other pure functions:
pure func square(x : Int) : Int {
x * x
}
pure func sumOfSquares(a : Int, b : Int) : Int {
square(a) + square(b)
}
pure func quadruple(x : Int) : Int {
double(double(x)) // Nested pure calls
}
Pure in Regular Functions
Regular functions can call pure functions:
persistent actor {
var counter : Int = 0;
pure func doubled(x : Int) : Int { x * 2 }
public func addTwice(n : Int) : async Int
modifies counter
ensures counter == old(counter) + doubled(n);
{
counter += doubled(n);
counter
}
}
What Pure Functions Cannot Do
Pure functions are restricted to ensure referential transparency.
Cannot Access State
persistent actor {
var total : Int = 0;
// WRONG: accesses actor field
pure func bad() : Int {
total // ERROR: pure function cannot access field 'total'
}
// CORRECT: take state as parameter
pure func good(t : Int) : Int {
t
}
}
Cannot Modify State
module {
var counter : Nat = 0;
// WRONG: modifies module state
pure func tick() : Nat {
counter := counter + 1; // ERROR: pure or lemma functions may not write state
counter
}
}
Cannot Use Assertions or Traps
// WRONG: contains assert
pure func bad1(x : Nat) : Nat {
assert x > 0; // ERROR: pure or lemma functions may not contain assertions
x
}
// WRONG: contains trap
pure func bad2(x : Nat) : Nat {
trap (x == 0); // ERROR: pure or lemma functions may not contain assertions or traps
x
}
// WRONG: contains runtime:assert
pure func bad3(x : Nat) : Nat {
runtime:assert x > 0; // ERROR
x
}
Cannot Await
// WRONG: async operations not allowed
pure func bad() : async Int {
await someCall() // ERROR: pure or lemma functions may not await
}
Cannot Use Statement-Style Loops
// WRONG: statement loop in a pure body
pure func bad(x : Nat) : Nat {
while (false) {
};
x
}
Pure bodies used by the verifier must translate as specification expressions. Use conditionals, pattern matching, small algebraic helpers, lemmas, or trusted pure helpers instead of imperative loops.
Cannot Be Recursive
// WRONG: direct recursion
pure func factorial(n : Nat) : Nat {
if (n == 0) { 1 }
else { n * factorial(n - 1) } // ERROR: recursive pure functions not supported
}
// WRONG: mutual recursion
pure func isEven(n : Nat) : Bool {
if (n == 0) { true }
else { isOdd(n - 1) } // ERROR
}
pure func isOdd(n : Nat) : Bool {
if (n == 0) { false }
else { isEven(n - 1) }
}
Use a lemma or pure trusted func for recursive specifications.
Cannot Use old()
// WRONG: old() in pure function contract
pure func bad(x : Nat) : Nat
ensures result == old(x); // ERROR: old() not allowed in pure function contracts
{
x
}
Pure functions have no notion of "previous state" since they don't access state.
Cannot Index Arrays
// WRONG: array indexing
pure func bad(arr : [Nat], i : Nat) : Nat {
arr[i] // ERROR: pure function cannot index arrays
}
Pure functions can receive array parameters but cannot index into them.
Allowed in Pure Functions
| Expression | Example |
|---|---|
| Parameters | x, a + b |
| Arithmetic | x * 2, a / b |
| Comparisons | x > 0, a == b |
| Logical operators | a and b, not x |
| Conditionals | if (x > 0) { x } else { -x } |
| Local let bindings | let y = x + 1; y * 2 |
| Pure function calls | double(x), max(a, b), Runtime.moduleHash() |
| Record construction | { x = 1; y = 2 } |
| Record field access | point.x |
| Tuple construction | (a, b, c) |
| Variant construction | #red, #some(42) |
| Pattern matching | switch (v) { case (#a) 1; case (#b) 2 } |
Common Use Cases
Specification Helpers
persistent actor {
pure func tri2(n : Nat) : Nat {
n * (n + 1)
}
public func sumTo(n : Nat) : async Nat
ensures 2 * result == tri2(n);
{
var total : Nat = 0;
var i : Nat = 0;
while (i < n) {
loop:invariant i <= n;
loop:invariant 2 * total == tri2(i);
total += i + 1;
i += 1;
};
total
}
}
Named Predicates
pure func isValidAmount(amount : Nat, balance : Nat) : Bool {
amount > 0 and amount <= balance
}
persistent actor {
var balance : Nat = 100;
public func withdraw(amount : Nat) : async Nat
entry_requires isValidAmount(amount, balance);
requires isValidAmount(amount, balance);
modifies balance
ensures balance == old(balance) - amount;
{
balance -= amount;
balance
}
}
Invariant Helpers
persistent actor {
var minted : Nat = 0;
var burned : Nat = 0;
pure func withinSupply(minted : Nat, burned : Nat, cap : Nat) : Bool {
burned <= minted and minted - burned <= cap
}
invariant withinSupply(minted, burned, 1_000_000);
}
Record Computations
type Point = { x : Int; y : Int };
pure func origin() : Point {
{ x = 0; y = 0 }
}
pure func addPoints(p1 : Point, p2 : Point) : Point {
{ x = p1.x + p2.x; y = p1.y + p2.y }
}
pure func manhattan(p : Point) : Int {
let ax = if (p.x < 0) { -p.x } else { p.x };
let ay = if (p.y < 0) { -p.y } else { p.y };
ax + ay
}
Variant Pattern Matching
type Color = { #red; #green; #blue };
pure func colorCode(c : Color) : Int {
switch (c) {
case (#red) { 1 };
case (#green) { 2 };
case (#blue) { 3 };
}
}
pure func isRed(c : Color) : Bool {
switch (c) {
case (#red) { true };
case (_) { false };
}
}
Pure Functions vs Related Features
| Feature | Can Access State | Can Modify State | Use in Specs | Verified Body |
|---|---|---|---|---|
pure func | No | No | Yes | Yes |
lemma func | Via parameters | No | Proof steps | Yes |
pure trusted func | No | No | Yes | Trusted |
Regular func | Yes | Yes | No | Yes |
Error Messages
| Error | Cause |
|---|---|
| "pure function cannot access field" | Accessing actor/module field |
| "pure or lemma functions may not write state" | Assignment in pure function |
| "pure or lemma functions may not await" | Async operation |
| "pure or lemma functions may not contain assertions or traps" | assert, trap, or runtime:assert |
| "recursive pure functions are not supported" | Direct or mutual recursion |
| "old() is not allowed in pure function contracts" | Using old() in requires/ensures |
| "pure function cannot index arrays" | Array element access |
| "pure expression contains statementful source expression" | Statement loop in a pure body |
Common Mistakes
Accessing Actor State
persistent actor {
var count : Nat = 0;
// WRONG: reads actor field
pure func withCount(x : Nat) : Nat {
x + count // ERROR
}
// CORRECT: pass state as parameter
pure func addTo(x : Nat, c : Nat) : Nat {
x + c
}
}
Using Assertions for Validation
// WRONG: using assert in pure function
pure func safeSqrt(x : Nat) : Nat {
assert x >= 0; // ERROR
// ...
}
// CORRECT: use requires clause
pure func safeSqrt(x : Nat) : Nat
requires x >= 0;
{
// ...
}
Expecting Runtime Checks
// Pure functions have no runtime effect
pure func validate(x : Nat) : Bool {
x > 0
}
public func process(x : Nat) : async Nat {
// This does NOT check at runtime - pure calls are specification-only
if (validate(x)) { x } else { 0 } // OK but no runtime trap
}
Pure function calls in runtime code are compiled normally. They can branch and compute values, but they cannot contain runtime checks such as runtime:assert or trap.
Summary
puredeclares a function as side-effect free- Pure functions can use parameters, immutable locals, parameter fields, and pure or trusted-pure helpers
- Cannot access actor/module state, modify state, await, assert, trap, use statement loops in verifier-visible bodies, or recurse
- Cannot use
old()in contracts - Use pure functions for specification helpers, named predicates, and invariant computations
- Pure functions can have
requiresandensuresclauses - Callers and enclosing contracts must satisfy pure function preconditions
- For recursive specifications, use lemmas or
pure trusted funcinstead
Related Topics
- Pure functions in contracts for reusable predicates and expected-value helpers
- Pure function restrictions for complete error cases
- Loop invariants for using pure helpers in iterative proofs
- Imported helpers for
inline pureand contract-only source imports