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:
// 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:
// 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:
| Component | Purpose | Example |
|---|---|---|
| Fields | Mutable state with var | var count : Nat = 0 |
| Invariants | Method-boundary properties over state | invariant count >= 0 |
| Public functions | Shared entry points callable externally | public func get() : async Nat |
| Private functions | Internal helpers | func helper() : Nat |
Complete Example
// 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:
| Aspect | Module | Actor |
|---|---|---|
| State | Stateless | Mutable state with var |
| Functions | Synchronous local functions | Shared methods return async T; private helpers can be synchronous |
| Persistence | No canister storage | Ordinary fields persist across calls and upgrades; transient fields reset on upgrade |
| Verification | Library/type checking and contracts | Full contract verification |
| Use case | Libraries, helpers | Services, canisters |
Use modules for stateless logic and actors for stateful services:
// 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.
- Correct
- Without persistent
persistent actor {
var data : Nat = 0;
// ...
}
// Error M0220: this actor should be declared `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.countindicate 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
persistentkeyword is required for stateful actors - Modules handle stateless logic; actors handle stateful services
- Public actor members must be shared functions returning
asynctypes
Related Topics
- Actor fields for persistent and transient state
- Entry requires for exported runtime guards
- Actor invariants for method-boundary state rules
- Async functions for actor method suspension
- Modules for stateless library code