关于 motoko-base-to-core-migration
为 Motoko 项目提供从 mo:base 迁移到 mo:core 的分阶段操作指南,面向维护 canister 的开发者。内容涵盖 API 替换、持久化调整、构建验证与升级测试。
主要功能
- 01分六阶段迁移库与 API
- 02指导 Buffer、Map、Set 替换
- 03调整为持久化 actor
- 04检查稳定数据与升级兼容
- 05提供构建和迁移审计清单
适用场景
- 01将现有 Motoko canister 迁移至 mo:core
- 02核对容器 API 变更及可变语义
- 03测试升级后状态是否保留
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ research-ag/ motoko-skills --skill motoko-base-to-core-migration安装后在 Claude Code 中输入/motoko-base-to-core-migration确认已加载。
详细介绍
技能:将 Motoko mo:base 迁移到 mo:core
AI 快速检查清单(请勿跳过)
- 版本
- 确保 dfx 版本为 0.31+
- 确认仓库在开始迁移前可以编译
- 在 mops.toml 中添加 core = "2.2.0"
- 在 [toolchain] 和 [requirements] 部分将 mops.toml 中的 moc 更新为 "1.3.0"。
- 如果 mops.toml 中存在 wasmtime,将其升级到至少 "42.0.1"
- 第 5 阶段完成前,切勿移除 base 依赖
- dfx.json 标志(每个 canister)
- wasm_memory_persistence:保留
- 机械式重命名(第 1–2 阶段)
- 导入:mo:base/* → mo:core/*(有注明的例外)
- 仅导入类型:仅导入类型时优先使用 mo:core/Types(例如,Iter.Iter → Types.Iter,Result.Result<T,E> → Types.Result<T,E>)
- 风格:如果从 mo:core/Types 导入的类型不超过 2 个,优先使用具名类型导入(例如,
import { type Result; type Iter } "mo:core/Types";),并在代码中使用Result<T>/Iter<T>;如果导入的类型不少于 3 个,则导入整个模块(import Types "mo:core/Types";)。 - 方法:.vals() → .values()、Array.、Option.、Debug.trap → Runtime.trap 等
- 数据结构迁移(第 3 阶段)
- Buffer → List(可变,add() 返回 void)
- HashMap/TrieMap → Map(可变,add() 返回 void,Map.size(map))
- TrieSet → Set(可变,add() 返回 void)
- 消除所有“:= Map.add/List.add/Set.add”——这些操作现在会原地修改
- 持久化 Actor(第 4 阶段)
- actor → persistent actor(包括 actor class)
- 移除 mo:base(第 5 阶段)
- 仅在没有任何引用且所有 canister 都能成功构建后执行
- 稳定布局与升级(第 6 阶段)
- 保持稳定序列化数组兼容,或使用 with migration = 语法
- 使用 wasm_memory_persistence keep 测试本地升级
- 验证与签核
- 单独构建每个 canister
- 运行正则表达式审计,确保已消除关键错误
- 在适用情况下比较 DID 接口
- 执行本地升级测试
验收标准:
- grep 搜索 mo:base、Buffer.、HashMap.、TrieSet.、Debug.trap、.vals() 和“:= Map.add/List.add/Set.add”均返回零个结果(适当排除 .mops)
- 所有 canister 均能成功构建
- 使用 wasm_memory_persistence keep 进行升级测试,确保状态(堆)得以保留
- 检查公共方法的 API 兼容性
前置条件
mops.toml(依赖分阶段处理)
- 添加:core = "2.2.0"
- 在第 5 阶段完成前保留 base 依赖
dfx.json(每个 canister)
- type: "motoko"
- wasm_memory_persistence: "keep"
要求:
- dfx 0.31+
- moc 1.3.0
架构转变:增强型正交持久化(EOP)
旧方式(mo:base)
- stable var + preupgrade/postupgrade + 手动序列化到稳定内存
新方式(mo:core)
- persistent actor + transient var
- 堆会持久化;大多数结构无需序列化即可跨升级保留
persistent actor 内的规则:
- let x = ... → 默认稳定
- var x = ... → 默认稳定
- transient var x = ... → 不稳定(升级时重置)
- stable 关键字为隐式
- 仍可在需要时使用 preupgrade/postupgrade
6 个阶段
第 1 阶段:重命名导入(机械操作)
批量重命名:
- mo:base/X → mo:core/X(适用于大多数模块)
- 特例:
- mo:base/ExperimentalCycles → mo:core/Cycles
- mo:base/ExperimentalInternetComputer → mo:core/InternetComputer
- mo:base/List → mo:core/pure/List(不可变列表;不是 mo:core/List)
- mo:base/OrderedMap → mo:core/pure/Map
- mo:base/OrderedSet → mo:core/pure/Set
- mo:base/Deque → mo:core/pure/Queue
仅导入类型(mo:core/Types)
-
只需要类型而不需要其模块中的函数时,请导入
Types,而不是该模块。 -
最常见、需要明确修复的情况:
- 如果
import Iter "mo:core/Iter"仅用于类型Iter.Iter<T>,则替换为import Types "mo:core/Types",并将Iter.Iter<T>改为Types.Iter<T>。 - 如果
import Result "mo:core/Result"仅用于类型Result.Result<T,E>,则替换为import Types "mo:core/Types",并将Result.Result<T,E>改为Types.Result<T,E>。
- 如果
-
保持简单:除非明确发现其他仅导入类型的情况,否则只修复这两种模式。
-
审查(查找可能需要修复的位置):
grep -rn "Iter\.Iter<" . --include="*.mo" | grep -v \.mopsgrep -rn "Result\.Result<" . --include="*.mo" | grep -v \.mops
-
最小修复模式:
- 如果没有调用任何
Iter.*函数,请将import Iter "mo:core/Iter"改为import Types "mo:core/Types",并将类型改写为Types.Iter<...>。 - 对于
Result,如果只使用Result.Result<...>类型,也同样处理。
- 如果没有调用任何
-
少量类型的首选导入方式:
- 如果从
mo:core/Types中需要的类型不超过 2 个,请按名称将它们作为类型导入,然后在代码中直接使用,不加前缀。 - 示例:
// 之前(模块导入或带前缀的用法) import Types "mo:core/Types"; type R = Types.Result<Nat, Text>; type I = Types.Iter<Nat>; // 之后(≤2 个类型:按名称直接导入类型) import { type Result; type Iter } "mo:core/Types"; type R = Result<Nat, Text>; type I = Iter<Nat>; - 如果从
Types中导入 3 个或更多类型,请导入整个模块:import Types "mo:core/Types";,并使用Types.Result<...>、Types.Iter<...>等。
- 如果从
使用 sed 命令一次性替换所有出现的位置
已移除且没有直接替代项(需要手动修复):
- AssocList、Buffer、Hash、HashMap、Heap、IterType、None、Prelude、RBTree、Trie、TrieMap、TrieSet
审查:
- grep -rl 'mo:base' . --include="*.mo" | grep -v .mops
阶段 2:函数重命名(机械操作)
常见映射:
- Debug.trap(msg) → Runtime.trap(msg)(导入 Runtime "mo:core/Runtime")
- Prelude.unreachable() → Runtime.unreachable()(导入 Runtime "mo:core/Runtime")
- .vals() → .values()
- Array.append(a, b) → Array.concat(a, b)
- Array.init(size, val) → VarArray.repeat(val, size)(参数顺序相反)
- Array.freeze(arr) → Array.fromVarArray(arr)
- Array.thaw(arr) → Array.toVarArray(arr)
- Array.slice(a, s, e) → Array.range(a, s, e)
- Array.subArray(a, s, l) → Array.sliceToArray(a, s, s + l)
- Array.make(x) → Array.singleton(x)
- Array.mapFilter → Array.filterMap
- Array.chain → Array.flatMap
- Iter.range(a, b) → Nat.range(a, b + 1)(现在上界不包含在范围内)
- Iter.revRange(a, b) → Nat.rangeByInclusive(a, b, -1)
- Text.toLowercase → Text.toLower
- Text.toUppercase → Text.toUpper
- Text.translate → Text.flatMap
- Option.make → Option.some
- Option.iterate → Option.forEach
- Float.equal(a,b) → Float.equal(a,b,epsilon)
- Cycles.add(n); await call(args) → 使用“await (with cycles = n) call(args)”
审查:
- grep -rn ".vals()" . --include="*.mo" | grep -v .mops
阶段 3a:Buffer → List(可变,这是最容易踩的坑)
- mo:core 中的 List 是可变的;List.add(list, x) 返回 void,并会就地修改列表
- 切勿写:list := List.add(list, x)
- 访问器:
- List.at(list, i) → T(索引越界时触发陷阱)
- List.get(list, i) → ?T(安全)
- 转换:
- List.toArray(list), List.fromArray(arr)
- 对持久状态,优先使用稳定的 let list = List.empty()
阶段 3b:HashMap/TrieMap → Map(可变)
- Map 是可变的;Map.add(map, compare, k, v) 返回 void
- 切勿写:map := Map.add(map, ...)
- 点表示法 API 模式:
- Map.empty<K,V>()
- map.add(cmp, k, v)
- map.get(cmp, k) → ?V
- map.remove(cmp, k) → void
- map.delete(cmp, k) → Bool
- map.take(cmp, k) → ?V
- map.entries()、map.values()、map.size()
- “cmp” 是键比较函数,例如 Text.compare、Nat.compare
阶段 3c:TrieSet → Set(可变)
- Set.add(set, compare, x) 返回 void
- Set.contains(set, compare, x) → Bool
- Set.delete 返回 Bool;Set.remove 返回 void
- 优先使用稳定的 let set = Set.empty()
阶段 3d:点表示法
有关点表示法的详情和安全模式,请参阅 Skill“Motoko mo:core Code Improvements”——B 节(优先使用点表示法)和 C 节(确保为点表示法导入必要内容)。
简要规则:优先对 mo:core API 使用点表示法;没有对应方法时,保留模块级工厂/构造函数调用(例如,Blob.fromArray / Blob.fromVarArray)。在完成基本重命名后执行此步骤,以确保行为保持不变。
阶段 4:persistent actor 关键字
- actor → persistent actor
- actor class → persistent actor class
审查:
- grep -rn "^actor\b|^ actor\b" . --include="*.mo" | grep -v persistent
阶段 5:从依赖项中移除 mo:base
仅在满足以下条件后执行:
- 所有文件都能使用 mo:core 编译
- 自有代码中不再引用 mo:base
审查:
- grep -r 'mo:base' . --include="*.mo" | grep -v .mops
- 如果结果为零,则从 mops.toml 中移除 base = "..."
注意:
- 第三方 .mops 依赖项可能仍在使用 mo:base;跳过它们
阶段 6:稳定变量迁移(生产环境关键)
如果更改序列化策略或类型:
- 保持简单类型的稳定数组在不同版本间兼容;沿用相同的 var 名称/类型
- 对升级时会重建的内存中 Map/List/Set,使用 transient var
- 必要时使用 preupgrade/postupgrade 进行序列化/反序列化
- 对于不兼容的更改,可考虑使用 with migration = 语法将旧状态转换为新状态
7 个关键错误(及其修复方法)
- := Map.add / := List.add / := Set.add
- 症状:类型错误 [M0098],无法隐式实例化类型为……的函数
- 修复:移除 :=;这些操作会原地修改并返回 void
- Array.init → VarArray.repeat(参数顺序相反)
- 修复:VarArray.repeat(val, size)
- Cycles.add 已移除
- 修复:await (with cycles = n) call(args)
- 自动编辑导致代码错乱
- 症状:Map.empty/List.empty 附近出现多余的构造函数或类型片段
- 修复:将声明清理为简单的 Map.empty<K,V>() / List.empty()
- 将 .values() 用作模块函数
- 修复:使用 List.values(list)、Map.values(map),而不是 l.values() 或 m.values()
- 十六进制字面量推断为 Nat8
- 修复:显式添加类型注解,例如 0xB5 : Nat8
- Error.message 与 Runtime.message
- 修复:保留来自 mo:core/Error 的 Error.message(e);只有 Debug.trap → Runtime.trap
Agent 策略(扩展到 100+ 个文件)
适合自动处理:
- 阶段 1(导入重命名)
- 阶段 2(函数重命名)
- 工具函数中简单的 Buffer→List
需要谨慎/人工审查:
- actor 主文件中的阶段 3 Map 迁移
- preupgrade/postupgrade 文件(稳定布局至关重要)
- 任何 Cycles.add 用法
- 第三方 .mops 包
Agent 提示词模板:
- 将 [FILE] 从 mo:base 迁移到 mo:core。关键规则:
- Map.add/List.add/Set.add 返回 VOID——移除所有 := 赋值
- VarArray.repeat(val, size)——参数顺序与 Array.init(size, val) 相反
- List/Map/Set 会原地修改——持久化数据优先使用稳定的 let
- Error.message 仍为 Error.message——不是 Runtime.message
- 编译器提示 M0098 时,添加显式类型参数
- 写入前先给我查看差异。
构建与验证工作流
按 canister 构建(快速迭代):
- dfx build 2>&1 | head -30
迁移完整性(进入 Phase 5 前必须为零):
- grep -rn "mo:base" . --include="*.mo" | grep -v .mops | wc -l
- grep -rn "HashMap." . --include="*.mo" | grep -v .mops | wc -l
- grep -rn "Buffer." . --include="*.mo" | grep -v .mops | wc -l
- grep -rn "TrieSet." . --include="*.mo" | grep -v .mops | wc -l
- grep -rn "Debug.trap" . --include="*.mo" | grep -v .mops | wc -l
- grep -rn ".vals()" . --include="*.mo" | grep -v .mops | wc -l
- grep -rn ":= Map.add|:= List.add|:= Set.add" . --include="*.mo" | wc -l
API 兼容性:
- 将生成的 .did 文件与基线进行比较,和/或运行定向调用
本地升级测试(dfx)
构建两个 WASM:
- 旧版(基线)和新版(迁移后)
在本地安装并升级:
- 确保 .most 文件已就位,以便进行本地 EOP 测试
- 升级时使用 --wasm-memory-persistence keep
- 验证模块哈希已更改
- 在旧版中填充状态,升级到新版,验证状态得以保留且 API 可正常工作
主网部署顺序(金丝雀策略)
- 先部署低风险 canister;逐个验证后再继续
- 尽早部署有代表性的 canister,以尽早发现系统性问题
- 最后部署高价值/复杂的 canister
每次部署后:
- API 兼容性检查
- 特定领域验证(适用时验证加密/数学功能)
快速参考:mo:core API
Map
- Map.empty<K,V>() → Map<K,V>
- Map.add(m, cmp, k, v) → void
- Map.get(m, cmp, k) → ?V
- Map.remove(m, cmp, k) → void
- Map.delete(m, cmp, k) → Bool
- Map.take(m, cmp, k) → ?V
- Map.swap(m, cmp, k, v) → ?V
- Map.insert(m, cmp, k, v) → Bool
- Map.size(m) → Nat
- Map.entries(m) → Iter<(K,V)>
- Map.containsKey(m, cmp, k) → Bool
- Map.clear(m) → void
- Map.fromIter(iter, cmp) → Map<K,V>
List
- List.empty() → List
- List.add(l, x) → void
- List.addAll(l, iter) → void
- List.size(l) → Nat
- List.get(l, i) → ?T
- List.at(l, i) → T
- List.put(l, i, v) → void
- List.toArray(l) → [T]
- List.fromArray(arr) → List
- List.values(l) → Iter
- List.removeLast(l) → ?T
- List.clear(l) → void
- List.sort(l, compare) → void
Set
- Set.empty() → Set
- Set.add(s, cmp, x) → void
- Set.contains(s, cmp, x) → Bool
- Set.remove(s, cmp, x) → void
- Set.delete(s, cmp, x) → Bool
- Set.insert(s, cmp, x) → Bool
- Set.size(s) → Nat
- Set.values(s) → Iter
- Set.fromIter(iter, cmp) → Set
VarArray(可变数组)
- VarArray.repeat(val, size) → [var T]
- VarArray.tabulate(n, f) → [var T]
- 使用 Array.fromVarArray/Array.toVarArray 进行转换
经验总结
- 有疑问时直接阅读 mo:core 源码;公开文档可能滞后。
- 一次构建一个 canister,以便更快获得反馈。
- “:= void”是最常见的迁移错误;请明确搜索它。
- 第三方软件包通常需要在 .mops 中手动打补丁。
- EOP + 持久化 actor 通常无需序列化;但升级到结构发生变化的版本时,仍应使用 stable 数组。
- 本地升级需要 .most 文件和 --wasm-memory-persistence keep。
- 对于非交互式升级,请使用 -y/--yes,并在升级后验证模块哈希。
AI 操作手册(分步指南)
- 准备
- 创建分支,确认版本,添加 mo:core,设置 dfx 标志
- 阶段 1
- 系统性地重命名导入
- 为纯模块添加例外(不可变)
- 阶段 2
- 按映射列表重命名方法/函数
- 修复 Iter 范围和 Text 大小写变更
- 阶段 3
- Buffer → List;移除所有 :=,并使用 List.add
- HashMap/TrieMap → Map;在每次调用时添加 compare;移除 :=;改用 Map.size(map)
- TrieSet → Set;移除 :=;将成员检查更新为 Set.contains
- 阶段 4
- 将 actor/actor class 转换为 persistent actor
- 构建 + 审查
- 按 canister 逐个构建
- 使用 grep 审查 mo:base、Debug.trap、.vals() 和 := add 调用
- 修复第三方依赖
- 根据需要修补 .mops 包;重新运行构建
- 阶段 5
- 审查通过后,从 mops.toml 中移除 mo:base
- 阶段 6
- 确保状态布局连续,或通过迁移实现兼容
- 使用 wasm_memory_persistence keep 进行本地升级测试
- 签核
- 所有构建均通过,审查结果为零,升级验证通过,API 已检查