mcpField
M
motoko-base-to-core-migration

motoko-base-to-core-migration

github.com/research-ag开发050 次安装10 周前更新
代码仓库

关于 motoko-base-to-core-migration

为 Motoko 项目提供从 mo:base 迁移到 mo:core 的分阶段操作指南,面向维护 canister 的开发者。内容涵盖 API 替换、持久化调整、构建验证与升级测试。

主要功能

  1. 01分六阶段迁移库与 API
  2. 02指导 Buffer、Map、Set 替换
  3. 03调整为持久化 actor
  4. 04检查稳定数据与升级兼容
  5. 05提供构建和迁移审计清单

适用场景

  1. 01将现有 Motoko canister 迁移至 mo:core
  2. 02核对容器 API 变更及可变语义
  3. 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 快速检查清单(请勿跳过)

  1. 版本
  • 确保 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 依赖
  1. dfx.json 标志(每个 canister)
  • wasm_memory_persistence:保留
  1. 机械式重命名(第 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 等
  1. 数据结构迁移(第 3 阶段)
  • Buffer → List(可变,add() 返回 void)
  • HashMap/TrieMap → Map(可变,add() 返回 void,Map.size(map))
  • TrieSet → Set(可变,add() 返回 void)
  • 消除所有“:= Map.add/List.add/Set.add”——这些操作现在会原地修改
  1. 持久化 Actor(第 4 阶段)
  • actor → persistent actor(包括 actor class)
  1. 移除 mo:base(第 5 阶段)
  • 仅在没有任何引用且所有 canister 都能成功构建后执行
  1. 稳定布局与升级(第 6 阶段)
  • 保持稳定序列化数组兼容,或使用 with migration = 语法
  • 使用 wasm_memory_persistence keep 测试本地升级
  1. 验证与签核
  • 单独构建每个 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 \.mops
    • grep -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 个关键错误(及其修复方法)

  1. := Map.add / := List.add / := Set.add
  • 症状:类型错误 [M0098],无法隐式实例化类型为……的函数
  • 修复:移除 :=;这些操作会原地修改并返回 void
  1. Array.init → VarArray.repeat(参数顺序相反)
  • 修复:VarArray.repeat(val, size)
  1. Cycles.add 已移除
  • 修复:await (with cycles = n) call(args)
  1. 自动编辑导致代码错乱
  • 症状:Map.empty/List.empty 附近出现多余的构造函数或类型片段
  • 修复:将声明清理为简单的 Map.empty<K,V>() / List.empty()
  1. 将 .values() 用作模块函数
  • 修复:使用 List.values(list)、Map.values(map),而不是 l.values() 或 m.values()
  1. 十六进制字面量推断为 Nat8
  • 修复:显式添加类型注解,例如 0xB5 : Nat8
  1. 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。关键规则:
    1. Map.add/List.add/Set.add 返回 VOID——移除所有 := 赋值
    2. VarArray.repeat(val, size)——参数顺序与 Array.init(size, val) 相反
    3. List/Map/Set 会原地修改——持久化数据优先使用稳定的 let
    4. Error.message 仍为 Error.message——不是 Runtime.message
    5. 编译器提示 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 进行转换

经验总结

  1. 有疑问时直接阅读 mo:core 源码;公开文档可能滞后。
  2. 一次构建一个 canister,以便更快获得反馈。
  3. “:= void”是最常见的迁移错误;请明确搜索它。
  4. 第三方软件包通常需要在 .mops 中手动打补丁。
  5. EOP + 持久化 actor 通常无需序列化;但升级到结构发生变化的版本时,仍应使用 stable 数组。
  6. 本地升级需要 .most 文件和 --wasm-memory-persistence keep。
  7. 对于非交互式升级,请使用 -y/--yes,并在升级后验证模块哈希。

AI 操作手册(分步指南)

  1. 准备
  • 创建分支,确认版本,添加 mo:core,设置 dfx 标志
  1. 阶段 1
  • 系统性地重命名导入
  • 为纯模块添加例外(不可变)
  1. 阶段 2
  • 按映射列表重命名方法/函数
  • 修复 Iter 范围和 Text 大小写变更
  1. 阶段 3
  • Buffer → List;移除所有 :=,并使用 List.add
  • HashMap/TrieMap → Map;在每次调用时添加 compare;移除 :=;改用 Map.size(map)
  • TrieSet → Set;移除 :=;将成员检查更新为 Set.contains
  1. 阶段 4
  • 将 actor/actor class 转换为 persistent actor
  1. 构建 + 审查
  • 按 canister 逐个构建
  • 使用 grep 审查 mo:base、Debug.trap、.vals() 和 := add 调用
  1. 修复第三方依赖
  • 根据需要修补 .mops 包;重新运行构建
  1. 阶段 5
  • 审查通过后,从 mops.toml 中移除 mo:base
  1. 阶段 6
  • 确保状态布局连续,或通过迁移实现兼容
  • 使用 wasm_memory_persistence keep 进行本地升级测试
  1. 签核
  • 所有构建均通过,审查结果为零,升级验证通过,API 已检查

评论

支持 Markdown