Skip to main content

Actor Declarations

Actors are the canister-facing services in Sector9. They encapsulate stable canister state, expose shared public methods, and define the boundaries where actor invariants, entry guards, and async interleaving matter.

Basic Syntax​

The core syntax for declaring an actor uses the persistent actor keywords:

persistent actor {
// fields and methods
}

The persistent keyword makes upgrade persistence explicit. In a persistent actor, ordinary actor fields default to stable storage and survive upgrades when their types are stable; use transient for caches or derived state that should reset. Upgrade compatibility is covered in state persistence.

Anonymous vs Named Actors​

Actors can be declared with or without a name.

Anonymous Actor​

An anonymous actor has no identifier:

actor-basic.sr9
// Basic persistent actor with state and a method
persistent actor {
var count : Nat = 0;

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

Named Actor​

A named actor includes an identifier after the keywords:

actor-named.sr9
// Named persistent actor
persistent actor Counter {
var count : Nat = 0;

invariant count <= 1_000_000;

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

public func increment() : async Nat
modifies count
entry_requires count < 1_000_000;
requires count < 1_000_000;
ensures result == old(count) + 1;
{
count += 1;
count
};
}

Names are optional but useful for documentation and debugging. The name appears in error messages and generated artifacts.

Actor Structure​

An actor body contains:

ComponentPurposeExample
FieldsMutable state with varvar count : Nat = 0
InvariantsMethod-boundary properties over stateinvariant count >= 0
Public functionsShared entry points callable externallypublic func get() : async Nat
Private functionsInternal helpersfunc helper() : Nat

Complete Example​

actor-with-invariant.sr9
// Actor with invariant ensuring balance never goes negative
persistent actor Bank {
var balance : Int = 0;

invariant balance >= 0;

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

public func withdraw(amount : Nat) : async Bool
modifies balance
ensures result == (old(balance) >= amount);
ensures result ==> balance == old(balance) - amount;
ensures (not result) ==> balance == old(balance);
{
if (balance >= amount) {
balance -= amount;
true
} else {
false
}
};
}

Actors vs Modules​

Actors and modules serve different purposes:

AspectModuleActor
StateStatelessMutable state with var
FunctionsSynchronous local functionsShared methods return async T; private helpers can be synchronous
PersistenceNo canister storageOrdinary fields persist across calls and upgrades; transient fields reset on upgrade
VerificationLibrary/type checking and contractsFull contract verification
Use caseLibraries, helpersServices, canisters

Use modules for stateless logic and actors for stateful services:

actor-with-module.sr9
// Actor using a helper module
module Math {
public func double(n : Nat) : Nat { n * 2 };
};

persistent actor Calculator {
var storedResult : Nat = 0;

public func doubleAndStore(n : Nat) : async Nat
modifies storedResult
ensures storedResult == n * 2;
{
storedResult := Math.double(n);
storedResult
};
}

The persistent Keyword​

Sector9 currently defaults to requiring the persistent keyword for stateful actors. This makes persistence explicit and prevents accidental data loss during upgrades.

persistent actor {
var data : Nat = 0;
// ...
}

Verification and $Self​

When Sector9 verifies an actor, it models the actor instance as $Self. This appears in:

  • Invariants: The verifier checks that $Inv($Self) holds at method boundaries
  • Permissions: Field access requires acc($Self.field, ...) permissions
  • Error messages: References like $Self.count indicate actor field access

You write contracts using field names directly. The $Self notation appears only in verifier output and error messages.

For public exported methods, use entry_requires for runtime entry guards and matching requires clauses for proof facts. In verification mode, a public actor method with nontrivial logical requires and no entry_requires is rejected.

Common Mistakes​

Non-Shared Constructor Parameters​

Actor classes cannot accept non-serializable parameters:

// Error M0034: shared constructor has non-shared parameter type
actor class Counter(initial : [var Int]) { }

Fix by using shared types (no var, no functions):

actor class Counter(initial : [Int]) { }

Public Fields Must Be Functions​

Actors expose functionality through functions, not direct field access:

// Error M0125: public actor field needs to be a manifest function
persistent actor {
public let value = 42; // Wrong
}

Fix by using a function:

persistent actor {
let value = 42;
public func getValue() : async Nat { value }; // Correct
}

Await Outside Async Context​

The await keyword only works inside async functions:

// Error M0038: misplaced await
persistent actor {
let x = try { await someAsyncCall() } catch (_) { 0 }; // Wrong - in actor body
}

Fix by placing await inside an async function:

persistent actor {
public func init() : async () {
let x = try { await someAsyncCall() } catch (_) { 0 }; // Correct
};
}

Summary​

  • Use persistent actor { } to declare canister actors with upgrade-persistent state
  • Named actors (persistent actor Name { }) improve debugging
  • Actors contain fields, invariants, and public/private functions
  • The persistent keyword is required for stateful actors
  • Modules handle stateless logic; actors handle stateful services
  • Public actor members must be shared functions returning async types