Skip to main content

Ghost Variables (ghost var)

Ghost variables are verification-only state that exists during proof checking but is erased from runtime output. They let you track abstract properties and maintain invariants without affecting the compiled program.

Basic Syntax

Declare a ghost variable with ghost var:

persistent actor {
ghost var counter : Nat = 0;
ghost var flag : Bool = true;
ghost var value : Int = -42;
}

Ghost variables:

  • Must have an initial value
  • Are typed like regular variables
  • Can use any spec-compatible type (Nat, Int, Bool, Text, records, variants, options, spec collections)

Actor-Level Ghost Fields

Ghost variables declared at actor level become ghost fields. They persist across method calls (for verification purposes) but are erased from the compiled actor.

ghost-var-basic.sr9
// Basic ghost variable example

persistent actor {
// Runtime state
var counter : Nat = 0;

// Ghost state - exists only for verification
ghost var shadow : Nat = 0;

// Invariant relates ghost and runtime state
invariant counter == shadow;

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

public func get() : async Nat
reads counter
ensures result == counter;
{
counter
};
}

The shadow field tracks the counter's value for verification. The invariant counter == shadow proves they stay synchronized. At runtime, only counter exists.

Local Ghost Variables

Ghost variables can also be declared inside function bodies:

ghost-var-local.sr9
// Local ghost variables in function bodies

persistent actor {
public func compute(n : Nat) : async Nat
entry_requires n <= 10;
requires n <= 10;
ensures result == n + 1;
{
// Runtime local
var acc : Nat = 0;

// Ghost local - for proof tracking
ghost var proof_acc : Nat = 0;

acc := n;
ghost { proof_acc := n; };

acc += 1;
ghost { proof_acc := proof_acc + 1; };

// Assert ghost and runtime are synchronized
assert acc == proof_acc;

acc
};
}

Local ghost variables help track intermediate proof state within a function. They are scoped to the function body and erased from runtime.

Mutating Ghost Variables

Ghost variables can only be mutated inside ghost blocks:

ghost { counter := counter + 1; };

Ghost blocks contain statements that execute only during verification. See Ghost Blocks for details.

The modifies Clause

When a function modifies a ghost field, you must declare it in the modifies clause:

public func update() : async ()
modifies ghostCounter
ensures ghostCounter == 10;
{
ghost { ghostCounter := 10; };
}

This is the same requirement as runtime fields. The verifier tracks permissions for ghost fields.

Using Ghost Variables in Invariants

Ghost variables are particularly useful in invariants to express conservation laws and abstract properties:

persistent actor {
var balance : Nat = 0;
ghost var totalDeposits : Nat = 0;
ghost var totalWithdrawals : Nat = 0;

invariant balance + totalWithdrawals == totalDeposits;

public func deposit(amount : Nat) : async ()
reads totalWithdrawals
modifies balance, totalDeposits
entry_requires amount > 0;
requires amount > 0;
{
balance += amount;
ghost { totalDeposits := totalDeposits + amount; };
};

public func withdraw(amount : Nat) : async ()
reads totalDeposits
modifies balance, totalWithdrawals
entry_requires amount <= balance;
requires amount <= balance;
{
balance -= amount;
ghost { totalWithdrawals := totalWithdrawals + amount; };
};
}

The ghost variables prove that balance correctly reflects all deposits and withdrawals, even though only balance exists at runtime.

Using Ghost Variables with old()

Ghost fields work with old() in postconditions:

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

The old(ghostCounter) captures the ghost field's value at method entry.

Restrictions

Cannot Use in Runtime Code

Ghost variables cannot be read or written outside ghost blocks:

ghost-var-runtime_reject.sr9
// ERROR: Ghost variable used in runtime code

persistent actor {
public func bad(n : Nat) : async Nat {
ghost var g : Nat = 0;
g := n; // ERROR M0314: ghost variable cannot be used in runtime code
g
};
}

Error M0314: ghost variable cannot be used in runtime code

Local Ghost Variables Cannot Escape Into Contracts

Local ghost variables are scoped to the body or ghost block where they are declared, so they cannot appear in function contracts:

// WRONG - ghost variable not visible to callers
public func bad(n : Nat) : async Nat {
ghost var g : Nat = 10;
requires n > g; // ERROR M0316
n
}

Error M0316: ghost variable cannot be used in contract

Actor and module ghost fields can appear in verifier-only contracts such as requires, ensures, and modifies. They cannot appear in entry_requires, because entry guards are emitted as runtime checks.

Ghost Blocks Cannot Mutate Runtime Fields

Ghost code is erased at runtime, so it cannot modify runtime state:

ghost-var-field-mutate_reject.sr9
// ERROR: Ghost block cannot mutate runtime fields

persistent actor {
var balance : Nat = 0;

public func bad() : async ()
modifies balance
{
ghost { balance := 1; }; // ERROR: ghost code cannot update runtime fields
};
}

This would be unsound: the proof depends on balance being 1, but runtime code never sets it.

Cannot Mutate Arrays or Mutable Records in Ghost Blocks

Similarly, ghost blocks cannot mutate array elements or mutable record fields:

// WRONG
public func bad() : async () modifies data {
ghost { data[0] := 42; }; // ERROR: cannot mutate array in ghost
}

Ghost vs Runtime: Key Differences

AspectRuntime VariableGhost Variable
Declarationvar xghost var x
Runtime code accessAllowedError M0314
Ghost block accessAllowedAllowed
In requires/ensuresAllowed (actor fields)Allowed for actor/module ghost fields; local ghosts are out of scope
Compiled outputIncludedErased
Mutated from ghostError (erased code cannot change runtime state)Allowed

When to Use Ghost Variables

Use ghost variables when you need to:

  1. Track abstract state - Model high-level properties that don't exist at runtime
  2. Prove conservation laws - Track totals, sums, or counts across operations
  3. Shadow runtime state - Maintain a parallel ghost model for complex proofs
  4. Express invariants - State properties that relate multiple operations

Ghost variables add no runtime overhead since they are completely erased.

Common Patterns

Conservation Pattern

Track what goes in and out:

ghost var totalIn : Nat = 0;
ghost var totalOut : Nat = 0;
invariant balance + totalOut == totalIn;

History Pattern

Track sequence of events:

ghost var history : Seq<Event> = Seq.empty<Event>();
invariant Seq.len(history) >= 0;

Permission Pattern

Track who has access:

ghost var permissions : Set<Nat> = Set.empty<Nat>();
invariant Set.contains(0, permissions);

Summary

  • ghost var declares verification-only variables erased from runtime
  • Ghost variables are mutated only inside ghost { } blocks
  • Declare ghost field modifications with modifies in contracts
  • Actor/module ghost fields can appear in verifier-only contracts; local ghost variables cannot escape their scope
  • Ghost blocks cannot mutate runtime fields (would be unsound)
  • Use ghost variables for conservation laws, abstract state, and complex invariants
  • No runtime overhead: ghost state is completely erased during compilation