Moxzi - Alpha 6 - actor*, faster loops, and more

0.1.0-alpha.6 (2026-09-22)

Three themes: actor* — many independently addressable, independently upgradable actors
inside one canister; a moc 1.16.1 baseline, which is where the language and runtime this
compiler tracks now sit; and cheaper indexed loops and message arguments — the index work
is on by default, the other two are opt-in. Embedders also get deadlines, cooperative slices
and checkpointed schedules.


Added: actor* — many addressable actors inside one canister

If you are building something with many independent entities — accounts, rooms, agents,
ledgers, inventories — Motoko has offered two shapes and neither fits. A class is cheap but
invisible from outside the canister: no address, nobody can call it. A canister per entity is
addressable and independently upgradable, but you pay a canister’s cost and operational weight
for every one.

actor* sits between them. An instance lives in one canister’s heap, and has its own
principal, its own Candid methods, and its own upgrade story.

public persistent actor* class Counter(creator : Principal, initial : Nat) = me {
  var n = initial;
  public func internal_add(amount : Nat) : async* Nat { n += amount; n };   // internal
  public shared(msg) func add(amount : Nat) : async Nat {                   // external
    if (msg.caller != creator) throw Error.reject("only the creator may add");
    await* internal_add(amount)
  };
  public query func read() : async Nat { n };
};
var counters : [Counter] = [];        // ordinary values, and they outlive an upgrade

The pieces:

  • Two kinds of method, and the distinction is the design. async* is internal: callable
    from anywhere in the container with await*, never a message, no commit point, absent from
    the canister’s interface. async and query : async are external: real methods with a real
    msg.caller. In one line, async* is inside and async is outside.

  • An address. Actor.id(c) is a principal you can hand out and store, and later turn back
    into a typed reference. A stale or retired one fails closed rather than quietly hitting
    whatever now occupies the slot.

  • Callable from anything that can call a canister. A public actor* class’s external
    methods appear in the container’s Candid interface as Class_method(id, args…), so dfx, an
    agent or another canister reaches a subactor by passing the principal it was handed. The
    compiler also emits a small client module, so a calling canister writes
    Counter(service, id).add(5) instead of spelling the façade by hand.

  • Upgrades. A persistent actor* class keeps its instances across an upgrade with their
    state and identity, and you can hold them directly in a stable variable. The principals you
    handed out still name the same instances afterwards, so external callers need not be told
    anything and you do not rebuild the population from a side table of identifiers. Adding a
    method to the class does not disturb any of it. Changing the class’s fields — including
    changing a stored variable’s TYPE — takes a migration function, as it would for an actor, and
    every live instance is migrated once:

    // v1 stored a scalar          v2 stores a record, plus a field the old rows never had
    var n : Nat                    var tally : { count : Nat; unit : Text }
                                   var bumps : Nat
    
    public (with migration = M.run) persistent actor* class Counter(…) = me { … };
    
    module {   // M, the class's own migration
      public func run(old : { n : Nat }) : { tally : { count : Nat; unit : Text }; bumps : Nat } {
        { tally = { count = old.n; unit = "migrated" }; bumps = 0 }
      };
    };
    

    A scalar becoming a record is the case that cannot be fudged: no reinterpretation of the saved
    bytes produces it, so the migration has to build it. The principals you handed out still name
    the same instances afterwards, and the migration runs exactly once. One thing it may NOT do is
    produce the = me binding — identity belongs to the registry, and a migration that returns it
    is refused. Instances can also carry system func preupgrade and postupgrade of their own.

  • Rights. Timers and cycles work by the same spelling an actor uses — Timer.setTimer,
    Timer.recurringTimer, Timer.cancelTimer, (with cycles = n) — with <system> on class
    heads and methods carrying the capability. A synchronous method without <system> is
    refused rather than silently granted.

What it costs. From the recorded cost ladder, 10,000 children, native: a call into a
subactor is 3,314 instructions and 48 bytes, against 276 instructions and no allocation
for a plain class method — about 12x. A live instance adds roughly 704 bytes of allocation
footprint; that figure comes from tiny independent actors sharing one execution domain and is
not a promise about the retained size of an actor holding real state. The honest advice: if all
you want is encapsulated state that rolls back with the message, use a class — it is an order
of magnitude cheaper and it is the right tool. Reach for actor* when you need an address you can hand out and validate
later, entities that outlive an upgrade with their own migration, or entities callable from
outside the canister. It does not suit a hot loop that touches every entity every tick.

Limits The feature is experimental and behind
--experimental-local-actors N, with persistence behind
--experimental-local-actor-persistence; moc cannot compile a program that uses it. A long
sequential await* chain over thousands of children needs --experimental-private-gc on every
target, the replica included, because that trampoline is also where the collector runs.
Suspended continuations do not migrate across an upgrade, and quiescence is required.
Public computation transfer, foreign invocation and async constructor effects remain forbidden.
The guide and the introduction are both
generated from accepted fixtures, so every line of code in them is code that ran.

Added: two opt-in code generation options

Both default OFF, and a build without them is byte-identical to one from before them.

  • --wasm-tail-call emits return_call_indirect where applying a continuation is the last
    thing a function does. A CPS-lowered await* chain then reuses one stack frame instead of
    one per link. On a sweep of 10,000 children whose method can suspend, the ordinary build
    exhausts the call stack on native, Pulley and the replica, and the tail-call build completes
    at 4,645 instructions per call against 5,385 for the private-GC trampoline. It does not
    replace the trampoline: that is also the collector’s empty-stack safe point.
  • --candid-fast-path skips parsing an incoming Candid argument’s type table when its bytes
    are byte-for-byte the ones this compiler emits, which is every call from a caller using our
    own .did. Parsing costs about 267 instructions per table entry on every message. Record
    arguments decode 15% cheaper; a caller whose type section differs fails the comparison, takes
    the ordinary path, and pays one memcmp. A hit also skips a heap allocation, so a program
    that asserts an exact rts_heap_size() delta will see a different number under this flag.

Added: for embedders — deadlines, cooperative slices, checkpointed schedules

These are in the moxzi-runtime crate, for programs that host Motoko actors rather than write
them. All opt-in; nothing changes for an embedding that does not ask.

  • watchdog gives a synchronous Wasm call a deadline. A scope covers nested calls and
    Stores created on the executing thread, and expiry is a local abort, never a commit point. It
    must not cross an async suspension or move between threads, and host blocking IO still needs
    its own timeout — an engine epoch interrupts Wasm, not arbitrary Rust.
  • cooperative is an experimental synchronous admission pump: an engine Pending yields to
    a host callback while the message transaction stays on the owning stack. It is deliberately
    not an externally pollable runtime and not a serializable continuation; the callback may read
    its own host state but may not re-enter guest execution, and reentry is rejected rather than
    allowed to corrupt the transaction.
  • Bounded timer admission and additive scheduler checkpoints let an embedding cap what a
    round may schedule and recover the schedule from a checkpoint, with timer admission preserved
    across the capsule. Checkpointed job cursors keep a long job resumable across restarts.

Changed: checked index optimization is on by default

The default checked index fast path removes redundant fixed-width-to-Nat conversions at
checked array/blob indices and retains raw snapshots for eligible immutable index
bindings. Non-allocating small-word compound array updates also reuse the checked
element address. Types and bounds checks are preserved. Use --no-index-fast-path
(or API indexFastPath = ?false) for reference code generation; omitted API options
now enable the optimization. --index-fast-path remains supported. See the
index performance guide for measured scope and tests.

Changed: moc 1.16.1 is the baseline

The compiler/RTS upgrade from 1.14.1 ports migration-history validation (M0268),
.vals() and upgrade-hook deprecations (M0269/M0270), vetKD cost estimation, and
mixin method documentation in Candid. The CLI now supports ordered -A, -W, and
-E warning controls and stable .most sidecars, including enhanced migration
whose Wasm no longer embeds the stable signature.

Correctness fixes cover wide Candid record skipping, oversized array allocation,
small-word exponentiation overflow, exact-end region block tables, and safe Candid
coercion recovery. Compiler and RTS now share the cached-GC flag and multi-value
barrier ABI. Stock, compact, and configurable off-chain RTS builds retain Moxzi’s
extensions and use pinned upstream sources with recorded build provenance.

Functional-parity follow-up fixes implicit-derivation depth (default 100, with a
per-job --implicit-derivation-depth option), deferred-lambda output inference, and
rejection of refutable import patterns, structural implicit ambiguity and candidate
selection, transitive-import instance leakage, structural record field order, and
distinct generated thunk identities, and committed implicit-instance selection.

The qualification guide documents reproducible tests,
API changes, and limits. Optional M0236 rewrite suggestions remain unimplemented;
Moxzi does not emit the unsafe autofix corrected upstream. Historical full-suite
ratchets are still labeled 1.14.1; the new pinned campaign records current evidence.
No public release or deployed compiler canister is changed by this work.

Fixed

Most of this release’s fixes are internal to actor* and land with the feature. These are the
ones that matter to a program that never uses it:

  • The GC did not know about upgraded static root arrays. A canister that grew its static
    roots across an upgrade could have them collected out from under it. They are registered now
    (s42-root-barrier carries the before-and-after).
  • Fuel-backed performance counters drifted across budget refills. They now report banked
    consumption plus fuel since the latest refill, so a counter read after a refill is not a
    smaller number than one taken before it.
  • A stack overflow was reported as something else. Application stack exhaustion is now
    identified as itself, rather than surfacing as an unrelated failure — which is what made the
    actor* depth limits measurable in the first place.
  • A missing explicitly selected compiler was ignored. --compiler <path> pointing at a file
    that is not there is an error; it used to fall back to the search path and build with a
    compiler you did not ask for.
  • An upgrade of a suspended actor container is refused rather than producing a canister whose
    in-flight work cannot be resumed.
  • Deeply recursive helper functions could exhaust the canister stack. A self tail call
    inside a named nested function — func go(...) declared within another function and
    calling itself in tail position — was not being turned into a loop, so it consumed a stack
    frame per iteration and trapped with a stack overflow at roughly 24,000 levels. Anonymous
    and top-level functions were optimized correctly, which is why the effect was invisible
    until a program leaned on mo:core’s pure/List accumulator loops at depth: the Ethereum
    Foundation’s BLS12-381 multi_inf execution-spec fixtures trapped under moxzi while moc
    ran them in constant space. Such functions now compile to a loop, as they do under moc.
    Generic self tail calls (func go<T>(...) calling itself at <T>) are still not optimized
    and remain a known gap.
  • and/or type operands were judged for forward references in the wrong place (M0168).
    A type T = A and { s : S } where S is declared further down the same module was rejected
    (“cannot compute intersection of types containing recursive or forward references”), even
    though the two records have disjoint fields and the intersection never needs to look inside
    S. moc raises only when combining actually reaches an unelaborated definition; we scanned
    the operands for one anywhere. This is what kept mo:jwt — and therefore evm.mo — from
    building. Fixing it surfaced the mirror-image bug: type A = B or C with B/C declared
    later was accepted silently, because the union form had no M0168 check at all. Both forms now
    match moc on the reference negative suite, including the cases it accepts, and both report
    against the right-hand operand the way moc does.

More at https://Moxzi.ai

https://x.com/afat/status/2102469869698773467

Help spread the word!

Greetings,

if i were interested in trying out an ‘inventory canister’ feature with this, would I be working from the rust-cdk?

I’ve been appreciating your feedback. So it’d be cool to get a feel for what you are working with/on

No…you’d use moxzi and mops and the icp-cli/dfx. You don’t need to touch rust.

moxzi.ai - curl the cli as mentioned on the home page.
mops.one - if you need package mangement

See https://docs.internetcomputer.org/getting-started/quickstart/

you just need to sprinkle in the moxzi stuff and add the subactor flag. Point your ai at the moxzi skills: https://moxzi.ai/skills/

that is good becase when i touch rust my hand get rusty that why i like motko it is good becase every buddy is actor if i want feel rusty there c for that all ready but that just a pin man