Imported from caffeinelabs/skills (
skills/migrating-to-moc-2/SKILL.md). Install upstream withnpx skills add caffeinelabs/skills --skill migrating-to-moc-2. Copyright stays with the author.
Migrating to moc 2
moc 2 is a major release of the Motoko compiler. It changes four things:
- Persistence. Actors are persistent by default. Enhanced orthogonal persistence (EOP) is the only memory model and the incremental GC the only collector.
- Strictness. Warnings for code that traps or silently does not do what it says are now errors, and a pattern must fit the type it matches.
- Syntax. A few moc 1 forms no longer parse (
a ??b, a branch glued to its condition,flexible). Several lighter forms are new and optional. - Flags. Every flag of a removed feature is rejected as an unknown option.
Most projects need a handful of edits, and the compiler names each one. This skill maps every diagnostic to its fix. The upstream moc 1 → 2 migration guide has longer explanations, and the release notes list each change per release.
First: is any canister still on classical persistence?
moc 2 cannot build a canister on classical (32-bit) persistence, but it can upgrade one to EOP. A deployed canister is classical if it was built with --legacy-persistence, with a non-incremental GC flag (--copying-gc, --compacting-gc, --generational-gc), or with a moc older than 0.15, where classical was the default.
- Move it to EOP: compile that one upgrade with
--enhanced-orthogonal-persistenceand without--enhanced-migration. The move is irreversible; later upgrades need neither flag. Without the flag the upgrade traps ("Detected implicit upgrade from classical orthogonal persistence to enhanced orthogonal persistence"); with--enhanced-migrationit traps too ("Cannot upgrade from classical orthogonal persistence with --enhanced-migration"). - Keep it classical: stay on moc 1.
A canister that moved to EOP with a moc older than 1.5.0 can trap on a later upgrade with cannot upgrade from an actor using enhanced migration to an actor not using enhanced migration. Run that one upgrade with graph-copy stabilization; the upstream EOP migration path has the steps.
Every other canister already runs on EOP and needs no persistence work.
Checklist
- Build cleanly on the latest moc 1 first. The
.vals()(M0269) andpreupgrade/postupgrade(M0270) deprecations are identical in moc 2, so fix them before switching; on moc 2,mops check --fixrewrites.vals()to.values(). Fix every warning in the diagnostics table too — most become errors. - Bump the toolchain to the latest 2.x release:
mops toolchain use moc <version>(it rewrites[toolchain] mocinmops.toml), or wherever your build pins moc. - Delete the removed flags from
[moc].args,[build].args,[canisters.<name>].args,dfx.jsonand build scripts. - Run
mops checkand fix what it reports in this order, because earlier errors hide later ones:- syntax errors (
??, glued branches,flexible, record literals in block position); - errors that were warnings in moc 1;
- M0277, redundant
asyncon a function body; - M0142 and M0193 in libraries and actor classes;
- M0131, only if you built with
--legacy-actors.
- syntax errors (
- Check upgrade compatibility against the deployed version before upgrading a live canister:
mops check-stable, ormoc --stable-compatible old.most new.most. Removingpersistentandstablekeywords does not change the stable signature. - Optionally adopt the moc 2 forms — only once nothing builds this code with moc 1 anymore. See After the switch.
Removed flags
moc 2 exits with unknown option '<flag>' on each of these.
| Flag | Replacement |
|---|---|
--default-persistent-actors, --require-persistent-actors |
none needed: actors are persistent by default |
--legacy-actors |
mark every field that should reset on upgrade transient |
--legacy-persistence, --copying-gc, --compacting-gc, --generational-gc |
none: stay on moc 1 for classical canisters |
--incremental-gc, --rts-stack-pages, --skip-gc-deprecation-warning, --experimental-rtti |
none needed |
--experimental-stable-memory |
use Region |
--experimental-field-aliasing |
none: record update copies var fields |
--generate-view-queries |
write public query func getters. The generated __<field> queries leave the canister's Candid interface on upgrade, so update any client that called them |
--trap-on-call-error |
none: a failed call throws; handle it with try/catch |
--experimental-multi-value, --no-experimental-multi-value |
none needed |
-no-system-api |
-wasi-system-api to run outside the Internet Computer |
-ref-system-api |
none needed: it selected the default |
--print-source-on-error, -no-link, --profile, --profile-file, --profile-line-prefix, --profile-field |
none |
--enhanced-orthogonal-persistence stays: it is a no-op on EOP canisters, and only the one-time classical upgrade needs it. -A, -W and -E no longer accept M0135 and M0142.
Diagnostics
Each moc 1 warning that became an error can be downgraded again with -W <code> (e.g. -W=M0145,M0215 in [moc].args). That is a stopgap: the code it lets through traps or silently misbehaves. Pattern errors M0116, M0050 and M0115 cannot be downgraded.
| Code | In moc 1 | What it flags | Fix |
|---|---|---|---|
M0001 at ?? |
accepted | a ??b — ?? is whitespace-sensitive like < |
a ?? b |
M0001 at flexible |
accepted | flexible is no longer a keyword |
transient |
| M0273 | accepted | a block after ?? — { there now opens a record literal |
opt ?? do { ... } |
| M0275 | accepted | a branch glued to its condition (if (c)-1 else 1, if c[0] else [1]), or a bare branch after a compound condition (if f(x) a else b) |
space before the branch (if (c) -1 else 1), or braced branches |
| M0272 | M0001 | a record literal in block position — a case body, function body, or if/switch/while head |
nest it as the block's result: case null { { x = 0 } }; parenthesize a head: switch ({ x = 0 }) |
| M0274 | M0001 | a reserved keyword (query, implicit, …) as an identifier |
rename it |
| M0277 | accepted | = async { ... } or = ignore (async ...) as a function body |
drop the async; see Async function bodies |
| M0145 | warning | a pattern that does not cover every value, in switch, let, catch, for and parameters |
let ... else { ... }, or a case _ arm |
| M0222 | warning | ignore of an async* value, which never runs |
await* it |
| M0210 | warning | a parenthetical on an await* call, where it has no effect |
attach (with cycles = n) to an async call |
| M0212 | warning | an unknown parenthetical attribute ((with cycle = n)) |
fix the name: cycles |
| M0215 | warning | a field the expected type drops, e.g. the typo in { u with emial = e } |
fix the name, or drop the field |
| M0128 | warning | a function named like a system method (heartbeat, inspect, …) without system |
system func ..., or rename it |
| M0242 | warning | a public func without a return type, implicitly oneway |
: () if oneway is intended, else : async () |
| M0005 | warning | an import path whose letter case differs from the file name | match the file name |
| M0276 | warning M0062 | a comparison whose result is constant, at a type with one value (Any, {}, records sharing no field) |
compare the fields you mean: user.id == order.userId |
| M0278 | warning M0251 | a file in the --enhanced-migration directory with no public func migration, so it never ran |
rename the function to migration, or move the module out of the directory |
| M0074, M0081, M0101 | warning | an array, if or switch whose only common type is Any |
make the elements or branches agree, or annotate : Any if intended |
| M0166, M0167 | warning | an intersection that is None, a union that is Any |
write the intended type |
| M0116 | warning M0146 | a variant tag the scrutinee's type lacks — usually a typo | fix the tag; to match a wider type on purpose, annotate the scrutinee: switch (x : T) |
| M0050 | warning M0146 | a signed literal pattern (case (-1)) against Nat |
annotate switch (n : Int), or drop the case |
| M0115 | warning M0146 | an option pattern against Null |
fix the scrutinee's type |
| M0142 | warning | an imported file that is a bare list of declarations | wrap it in module { ... } and mark exports public |
| M0193 | warning M0135 | an actor class whose return type is not async |
actor class C() : async actor { ... } |
| M0131 | not reported: the field was transient | a function or object field that --legacy-actors left transient is now persistent |
mark it transient |
| M0057 | accepted | under moc --check a.mo b.mo, a file using another file's declaration without importing it |
move the shared code into a module and import it |
| M0072 | accepted | mo:base/ExperimentalStableMemory and the Prim.stableMemory* primitives are gone |
use Region |
New warnings, which only break a -Werror build:
- M0217 / M0218 — the
persistentkeyword andstablemodifier are redundant.mops check --fixdeletes them, so hold off while moc 1 must still build the code: there, a bareactorneeds--default-persistent-actors. - M0196 — an explicit
<system>on a primitive that no longer needs it (Prim.getSelfPrincipal,Prim.envVar,Prim.envVarNames,Prim.callerInfoSigner,Prim.callerInfoData,Prim.getCandidLimits,Prim.getCandidTypeLimits). Delete it;querymethods can now call these. - M0236 — a module call that could use dot notation (
Map.size(m)→m.size()), now on by default.mops check --fixapplies it;-A M0236silences it. - M0146 — now also reported for an alternative that is never matched in
let ... else.
Persistence
Actors are persistent by default
Every actor and actor class is persistent: its let and var fields keep their values across upgrades unless declared transient. Code written as persistent actor with stable fields compiles unchanged, with warnings M0217 and M0218.
If you built with --legacy-actors, unmarked fields used to be transient and are now persistent. Mark every field that should reset on upgrade transient; a field whose type cannot be persisted (a function, an object with methods) is error M0131 until you do. A field that turns from transient to persistent needs no migration: on the first upgrade it is new to the stable signature, so its initializer runs once.
preupgrade and postupgrade
They still work, with deprecation warning M0270. The usual reason for them is a structure that could not be persisted, such as mo:base/HashMap, copied into a stable array before an upgrade and rebuilt after it:
// moc 1, in the actor body
var entries : [(Text, Nat)] = [];
transient let users = HashMap.HashMap<Text, Nat>(16, Text.equal, Text.hash);
system func preupgrade() { entries := Iter.toArray(users.entries()) };
system func postupgrade() {
for ((k, v) in entries.vals()) { users.put(k, v) };
entries := [];
};
The mo:core collections are stable, so declare users as an ordinary field, let users : Map.Map<Text, Nat> = Map.empty();, delete both hooks and entries, and move the data once with a migration function on the actor:
// moc 2
import Map "mo:core/Map";
import Text "mo:core/Text";
(with migration = func(old : { var entries : [(Text, Nat)] }) : { users : Map.Map<Text, Nat> } {
{ users = Map.fromIter(old.entries.values()) } // `compare` is implicit, from the imported Text
})
actor { /* ... */ };
The upgrade that installs this version still runs the old preupgrade, so entries holds the data when the migration reads it. Warning M0207 on the dropped field is expected. Delete the (with migration = ...) clause once this version is deployed. With an enhanced migration chain (--enhanced-migration), put the same function in the next chain file as public func migration instead.
Syntax that no longer parses
?? needs spaces, and its right-hand side is an expression
let n = o ??0; // moc 2: syntax error
let n = o ?? 0; // both
let n = o ?? do { let k = f(); k + 1 }; // a block needs `do` (M0273 without it)
let r = o ?? ({ x = 1 }); // a record literal; parentheses work in both
Branches glued to the condition
A (, [ or prefix operator directly after a condition now continues the condition, so a bare branch needs a space before it. A condition that is more than a name or a parenthesized expression needs braced branches (M0275):
if (c)-1 else 1; // moc 2: `(c)-1` is the condition — M0275
if (c) -1 else 1; // branch `-1`
if (f(x)) a else b; // both
if (f(x)) { a } else { b } // both, and never ambiguous
Async function bodies
A function whose return type is async T has an asynchronous body whether it is written { ... } or = e. Spelling the async out is now M0277, and the fix-it removes it:
func f() : async Nat = async { 1 }; // M0277
func f() : async Nat { 1 }; // both
The same applies to = ignore (async ...) in a oneway shared function. Two moc 1 forms have no direct replacement: a parenthetical on the body (= (with cycles = n) async { ... }) moves to each call site, (with cycles = n) f(); and a local function can no longer return a future created by its enclosing function — await it there instead.
Record update copies var fields
{ base with ... } on a record with var fields was error M0179 in moc 1, or aliased the base's fields under --experimental-field-aliasing. moc 2 copies them into new cells, like the equivalent record literal:
let base = { var count = 0; name = "a" };
let copy = { base with name = "b" };
copy.count += 1; // base.count is still 0
Code that relied on aliasing compiles silently with different behavior. Keep the shared mutable state in one object that both records reference.
Libraries, modules and primitives
- Imported files must be modules (M0142). Wrap a bare list of declarations in
module { ... }and mark the exportspublic. - Actor classes return
async(M0193):actor class C() : async actor { ... }. mo:base/ExperimentalStableMemoryno longer type-checks. UseRegion(mo:core/Regionormo:base/Region). The rest ofbasestill compiles; new code should usecore.Prim.createActoris gone. Use actor classes, or the management canister'screate_canisterandinstall_code.- Canisters no longer export
__motoko_stable_var_info, a query that always trapped under EOP. - Implicit arguments and dot notation also search nested modules, so importing a facade that re-exports its package's modules is enough. Every call that resolved in moc 1 resolves the same way; a call moc 1 rejected may now resolve, or be ambiguous (M0224, M0231).
Tooling
moc --check a.mo b.mochecks each file on its own, seeing only its own imports.-c,--idland-rtake exactly one main file, and the REPL (-i) preloads at most one. Split programs with imports.-gemits only the DWARF line table.moc.js:Motoko.runtakes[]as its first argument (use imports instead of preloads), andgcFlagsaccepts only"force"and"scheduling".- No Intel-Mac binary. On an x86_64 Mac, build moc from source, use
moc.js, or stay on moc 1. Themotoko-base-library.tar.gzartifact is gone too; getbasefrom mops. mops benchcannot use--legacy-persistenceor a non-incremental--gcunder moc 2.
Code that must compile with both moc 1 and moc 2
A library, or a codebase built by both majors during a transition, has to stay in the syntax both accept. Write moc 1 code with these rules and moc 2 compiles it with at most the warnings above:
- Fix every warning moc 2 turns into an error (the diagnostics table) — moc 1 only warns, so a clean moc 1 build is the bar.
- Parenthesize
if,while,forandswitchheads:if (c),for (x in xs.values()),switch (e). - Write case patterns in parentheses and end every case with
;:case (#tag(p)) { ... };. Always parenthesize a variant payload. - Put a space between a parenthesized condition and a bare branch, or brace the branches.
- Space both sides of
??; write a block on its right asdo { ... }and a record literal as({ ... }). - Annotate a function passed where an
asyncfunction is expected:func(n : Nat) : async Nat { ... }. moc 1 cannot infer it. - Write a function body as a block, never
= async { ... }. - Never write
{ base with ... }whenbasehasvarfields: moc 1 rejects it, moc 2 copies them. - Never write
flexibleorstable. - The read-only primitives listed above warn either way: moc 1 asks for
<system>(M0195) and moc 2 calls it unneeded (M0196). Themo:corewrappers, such asRuntime.envVar<system>(name), compile cleanly on both. - For actor persistence, either build with
--default-persistent-actorson moc 1 only and write a bareactor, or writepersistent actorand allow M0217 on moc 2 (-A M0217).
After the switch
Once no moc 1 build remains, these moc 2 forms are available. All are optional.
- Delete
persistentandstable(M0217, M0218) —mops check --fixdoes it. The stable signature does not change. - Heads without parentheses:
if f(x) { a } else { b },while n > 0 { n -= 1 },for x in xs.values() { ... },switch p.x { ... }. The branches or body must then be blocks. - Lighter
switchcases: the;between cases is optional, and a pattern with a fixed extent needs no parentheses —case null,case -1,case ?v,case #tag(p), combined withor,andand: T. A variant payload keeps its own parentheses:case #tag(p), nevercase #tag p. - A function passed where an
asyncfunction type is expected takes its parameter and return types from it:run(func(n) { n + 1 }). do { ... }works as an operator operand:1 + do { ... },debug_show do { ... }.
