mcpField
M
moonbit-practice

moonbit-practice

github.com/mizchi开发41660 次安装1 周前更新
代码仓库

关于 moonbit-practice

面向 AI 的 MoonBit 编码实践指南,提供语法、项目配置、测试和代码导航建议,并针对 MoonBit v0.10.12 的变化整理常见陷阱。

主要功能

  1. 01MoonBit 语法与类型指导
  2. 02使用 moon ide 导航代码
  3. 03项目配置与工作区建议
  4. 04测试与快照实践
  5. 05常见语法错误排查

适用场景

  1. 01生成符合当前语法的 MoonBit 代码
  2. 02在 MoonBit 项目中查找 API 定义
  3. 03配置模块、包和工作区
  4. 04编写测试并更新快照

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mizchi/skills --skill moonbit-practice

安装后在 Claude Code 中输入/moonbit-practice确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
assets/
ci-nix.yaml
ci.yaml
moon.mod
moon.pkg
publish-to-npm.yaml
reference/
configuration.md
ffi-native.md
ffi.md
ide.md
language.md
mbtx.md
nix.md
performance.md
refactor.md
skills-marketplace.md
stdlib.md
testing.md

详细介绍

MoonBit 实践指南

AI 生成 MoonBit 代码的最佳实践。 如果你不理解这里的内容,请使用 moonbit-docs skill 搜索官方文档。

已针对 MoonBit v0.10.12 验证(文档快照:2026-09)。MoonBit 仍处于 1.0 之前的阶段,小版本更新也会破坏语法 — 如果这里的某个构造被拒绝, 请先运行 moon fmt(它会自动迁移大多数弃用内容),然后检查 moon explain <error-code>。

指南

代码导航:优先使用 moon ide,而非 Read/Grep

在 MoonBit 项目中,优先使用 moon ide 命令,而非 Read 工具或 grep。

# ❌ Avoid: Reading files directly
Read src/parser.mbt

# ✅ Recommended: Find definitions from symbols
moon ide peek-def Parser::parse
moon ide doc 'Parser::*parse*'      # glob search over symbols

# ❌ Avoid: Searching with grep
grep -r "fn parse" .

# ✅ Recommended: Semantic search
moon ide find-references parse
moon ide outline src/parser.mbt

原因:

  • moon ide 提供语义搜索(可区分定义和调用位置)
  • grep 会匹配注释和字符串
  • moon ide doc 能快速显示 API

未安装 moon 时(沙盒、仅供审查的检出目录),本指南所依赖的验证步骤 — moon fmt、moon check、moon test -u、moon ide doc — 均无法使用。此时:

  • 将每个快照的 content="" 留空,之后由 moon test -u 填充。
  • reference/stdlib.md 是包概览,并非完整的签名索引。如果你需要的符号不在其中,且无法运行 moon ide doc,请优先使用本指南有说明的构造,而非凭模糊记忆选用其他构造,并标记为未经验证。
  • 在总结中明确说明哪些部分未经验证。

不要手动编写本应由工具生成的值。

其他规则
  • 实现前,使用 moon ide doc '<Type>' 探索 API。moon doc <SYMBOL> 已弃用——它会打印警告并指向 moon ide doc
  • 配置格式:使用 DSL moon.mod / moon.pkg。 JSON 格式 moon.mod.json / moon.pkg.json 已弃用,计划移除;moon fmt 会将 JSON 迁移为 DSL。示例:assets/moon.mod、assets/moon.pkg。编辑任一文件前,请查看 reference/configuration.md
  • 可执行包使用 pkgtype(kind: "executable"),而非 options("is-main": true)。pkgtype(kind: "foreign_library") 取代 options(link: true);普通库无需声明
  • 一个仓库包含多个模块时 → 使用工作区(moon.work),而非独立模块。moon work init <dirs> 会创建清单;成员共享同一构建上下文,并可在本地相互解析。moon.mod 中的路径依赖已弃用,建议改用 moon.work。请参阅 reference/configuration.md 中的“Workspace”
  • 发布过滤规则位于 .moonignore(若不存在则回退到 .gitignore),而非 moon.mod 中已弃用的 include / exclude 字段
  • 以下示例省略了 pub。 下游包或 JS/Wasm 宿主需要访问的任何内容都必须使用 pub(pub fn、pub struct、pub suberror、pub impl、pub extend);包内局部项则无需如此。
  • 测试文件:*_test.mbt 是黑盒测试(只能访问包的公开 API),*_wbtest.mbt 是白盒测试(也能访问私有项)。独立的 test "..." { } 代码块应放在这两类文件之一中;白盒测试也可内联到源文件中。黑盒测试通过包别名(@mypackage.f())访问所属包,无需额外导入;只有它使用的其他包才需要添加 import { ... } for "test" 条目
  • 可执行程序入口点:fn main { ... }——不带参数列表,也不带括号。异步程序使用 async fn main { ... }。使用 @env.args() 读取参数;该包需要声明 pkgtype(kind: "executable")
  • 可见性:pub 会公开名称;pub(all) 还会公开结构体的字段,或枚举 / suberror 的构造器,这是其他包进行模式匹配所需的。普通的 pub struct / pub suberror 对外部而言是只读的:只有在所属包内才能构造并进行模式匹配
  • 本技能中的参考文件:reference/language.md(类型、trait、模式匹配)、reference/testing.md(测试布局、快照、属性测试、基准测试)、reference/stdlib.md(核心 API)、reference/configuration.md(moon.mod / moon.pkg / 工作区)、reference/ide.md(moon ide)、reference/ffi.md 和 reference/ffi-native.md(FFI)、reference/performance.md、reference/refactor.md、reference/nix.md、reference/mbtx.md、reference/skills-marketplace.md。如有冲突,以 SKILL.md 为准。
  • 对于单文件 .mbtx 脚本(moon run script.mbtx、moonx script.mbtx、通过 moon run - 使用标准输入、通过 moon run -c '...' 使用内联源码、import { } 前置代码块),请查看 reference/mbtx.md
  • 在为 skills.mooncakes.io 定义或发布可执行程序前,请查看 reference/skills-marketplace.md;其中介绍了包内的 SKILL.md、Wasm 目标、moonx、宿主能力、策略和发布验证

常见陷阱

  • 变量/函数名不要使用大写字母 — 会导致编译错误
  • mut 仅用于重新赋值,不用于修改字段 — 向数组追加元素不需要它
  • 不需要 return — 最后一条表达式就是返回值
  • 方法需要 Type:: 前缀
  • 不支持 ++ -- — 使用 i = i + 1 或 i += 1
  • 传播错误不需要 try — 会自动处理(与 Swift 不同)
  • 没有 await 关键字 — 只需使用 async fn 声明
  • 优先使用范围 for,而不是 C 风格写法 — for i in 0..<n {...}
  • 函数式 for 循环的退出值使用 nobreak,而不是 else(else 已弃用)
  • 旧语法:function_name!(...) 和 function_name(...)? 已弃用
  • for { ... } 已弃用 — 无限循环请使用 for ;; { ... } 或 while true { ... }
  • 跨包调用 trait 方法时,. 语法需要 pub extend — 单独的 impl 永远无法让另一个包中的 value.f() 生效。请显式附加(pub extend Type with Trait::{ f });没有 pub 时,附加仅限于当前包;完全没有 extend 时,调用方必须写成 Trait::f(value)
  • trait/impl 方法需要 fn 关键字(v0.10.0) — moon fmt 会迁移旧代码;请参阅下文“迁移 trait 或 impl”
  • 使用 extend 附加 trait 方法(v0.10.4) — 隐式地将 impl 附加为方法的方式已弃用;请参阅下文“迁移 trait 或 impl”
  • try? 已移除(v0.10.0) — 对于 Result,使用 Ok(expr) catch { e => Err(e) };遇到错误时要触发 panic,请使用 try!
  • 结构体 fn new(..) 构造函数已移除(v0.10.0) — 请使用用户定义的构造函数 fn Type::Type(..);从 v0.10.4 起,任何类型都可以定义构造函数,不再仅限于结构体
  • immut/array 已移除(v0.10.9) — 在 v0.9 中已弃用。在 moon.pkg 中导入 "moonbitlang/core/immut/vector"(默认别名为 @immut/vector,即 moonbitlang/core/ 之后的路径),并使用 @immut/vector.Vector
  • 不可变容器上的 from_array 已弃用(v0.10.4) — 请使用以类型命名的构造函数,例如 @immut/vector.Vector([1, 2, 3])
  • 视图运算符不接受负索引(v0.10.4) — xs[-1:] 不再表示“从末尾开始”
  • moonbitlang/sys 已弃用(v0.10.9) — 请使用 moonbitlang/core/env:let argv = @env.args()(元素 0 是程序本身,因此 CLI 参数为 argv[1:])
  • @debug.to_repr(x) 已弃用(v0.10.9) — 请使用 @debug.Repr(x)
  • 不带 else 的 guard 必须能证明总是匹配(v0.10.9) — 否则会产生 E0087;预期触发 panic 时请写 guard!
  • Or 模式的 with 分支需要加括号(v0.10.9) — Some(x) | (None with x = 0)
  • 容器/自定义类型应使用 debug_inspect 取代 inspect(v0.9.2) — 容器类型(Array/Map/Set/Option/Result/元组)的 Show 已弃用;请参阅“快照测试”
  • 默认构建目标是 wasm(v0.10.9) — 请显式传入 --target js/--target native,或在 moon.mod 中设置 preferred_target

AI 常见语法错误

类型参数的位置
///| NG: fn identity[T] is old syntax
fn identity[T](val: T) -> T { val }

///| OK: Type parameter comes right after fn
fn[T] identity(val: T) -> T { val }
raise 语法
///|
/// NG: -> T!Error was removed
fn parse(s: String) -> Int!Error { ... }

///|
/// OK: Use raise keyword
fn parse(s: String) -> Int raise Error { ... }

Int raise 是 Int raise Error 的简写。 async fn 默认会隐式抛出错误;使用 noraise 可强制不抛出错误。

宏调用
///|
/// NG: ! suffix was removed
assert_true!(true)

///|
/// OK
assert_true(true)
迁移 trait 或 impl

同一段代码中总是会同时触发以下两项变更,因此请一次性完成:

  1. v0.10.0 — 在 trait 方法和 impl 中都添加 fn。
  2. v0.10.4 — 为调用方使用点语法(value.f())调用的每个 trait 方法添加 extend 声明;否则点调用会依赖已弃用的隐式附加方式。

moon fmt 只会执行步骤 1。步骤 2 需要手动完成。

///| NG: method without fn (deprecated, warns then removed)
trait I {
  f(Self) -> Unit
}
impl I for Int with f(_) {}

///| OK: fn keyword on both the trait method and the impl
trait I {
  fn f(Self) -> Unit
}
impl I for Int with fn f(_) {}

多态 trait 方法:方法自身的类型参数放在 fn 之后, impl 自身的类型参数仍放在 impl 之后。

trait Logger {
  fn[X : Show] write_object(Self, X) -> Unit
}
impl Logger for StringBuilder with fn write_object(self, x) {
  self.write_string(x.to_string())
}

impl[A] Poly for Array[A] with fn[X] f(self, x : X) { ... }
//   ^^^ impl type params                  ^^^ method type params

moon fmt 会自动重写旧的 trait/impl 语法。

使用 extend 附加 trait 方法(v0.10.4)

以前,编写 impl Trait for Type 会悄悄地将 trait 的所有方法附加到 Type,使其可以通过点调用。此做法现已弃用:上游 trait 新增默认方法后,可能会导致现有的 value.f() 调用产生歧义。请改为显式声明附加的方法。

///| NG (deprecated): relying on implicit attachment
impl Show for Point with fn output(self, logger) { ... }
let s = p.output(logger)   // implicit attachment, warns under warning 79

///| OK: attach the methods you want callable with dot syntax
impl Show for Point with fn output(self, logger) { ... }
pub extend Point with Show::{ output, to_string }
  • 通用形式:extend Type with Trait::{ method1, method2 }。pub 会使附加的方法成为公开方法;不加 pub 时,这些方法仅在包内可用。
  • 使用 trait 默认方法时,Self 会特化为 Type。
  • 也可以扩展 trait 对象:extend &Derived with Super::{ method }。
  • 不使用 extend 时,仍可通过 trait 调用:Trait::method(value, ...)。
  • 警告 implicit_impl_as_method(79)会标记此弃用模式;默认关闭。迁移库时请启用它(moon check --warn-list +79)。
结构体、构造函数和方法

已移除的特殊形式 fn new(..) 由普通的用户定义构造函数 fn Type::Type(..) 取代。自 v0.10.4 起,任何类型都可以使用此构造函数,而不仅限于结构体。方法通过 self : Self 接收接收者。

///| Struct with derives
struct Point {
  x : Int
  y : Int
} derive(Eq, Debug)

///| User-defined constructor (replaces removed `fn new`)
fn Point::Point(x : Int, y : Int) -> Point {
  { x, y }  // field-punning struct literal
}

///| Method: receiver `self : Self`, last expression is the return value
fn Point::manhattan(self : Self) -> Int {
  self.x.abs() + self.y.abs()
}

///| Call site: a declared constructor is called by the *type name*, not `Type::Type`
let p = Point(1, 2)

///| Constructors are ordinary functions, so they take labelled and optional
/// arguments and may raise:
fn Endpoint::Endpoint(host : String, port? : Int = 80) -> Endpoint {
  Host(host, port)
}
let e = Endpoint("example.com", port=443)

如果没有声明构造函数,仍可使用记录字面量 Point::{ x: 1, y: 2 }(或在类型已知时使用简写 { x: 1, y: 2 })来构造结构体。当构造过程需要校验、默认值或更简短的调用形式时,声明 fn Type::Type(..),然后在所有调用处优先使用 Point(1, 2)。

多行文本
let text =
  #|line 1
  #|line 2
注释和代码块分隔符

///| 是代码块分隔符。/// 注释会附加到后面的 ///| 代码块。

///|
/// This function is foo
fn foo() -> Unit { ... }

///|
/// This function is bar
fn bar() -> Unit { ... }

避免在文件开头连续使用 ///|,因为它们会创建不同的代码块。

快照测试

moon test -u 会自动填充 inspect(val) / debug_inspect(val) 中为空的 content=""。如何选择(v0.9.2+):

  • 基本类型 / 字符串 → inspect(此处使用 Show 没问题)
  • 容器类型(Array/Map/Set/Option/Result/元组)以及自定义类型 → debug_inspect,因为这些容器的 Show 已弃用,而 Debug 会提供结构化、带缩进的输出。Show 仅用于面向用户的格式化。
  • 深度嵌套的结构 → @json.inspect,其 JSON 输出经格式化后仍清晰易读。需要 derive(ToJson)。

请根据被检查表达式的静态类型进行选择,而不是运行时值:即使 Result[Int, _] 中保存的是 Ok(123),它仍是容器,此规则也会传递应用——要对自定义类型(包括嵌套在容器中的自定义类型)进行快照或断言,该类型必须 derive(Debug);若要通过 @debug.assert_eq 比较相等,还需要 derive(Eq)。

test "snapshot" {
  inspect("hello", content="")          // primitive/string -> inspect
  debug_inspect([1, 2, 3], content="")  // container -> debug_inspect
}

///| Custom type: derive Debug to snapshot, derive Eq to assert_eq
struct P {
  x : Int
} derive(Eq, Debug)

test "custom type" {
  debug_inspect(P::{ x: 1 }, content="")
  @debug.assert_eq(P::{ x: 1 }, P::{ x: 1 })
}

运行 moon test -u 后:

test "snapshot" {
  inspect("hello", content="hello")
  debug_inspect([1, 2, 3], content="[1, 2, 3]")
}

请填写 content="",并让 moon test -u 自动填充。切勿手写预期字符串——Debug 的呈现方式是结构化的,与 Show 不同,因此猜测的值会导致测试首次运行就失败。如果无法运行工具链,请将所有 content="" 留空并说明原因;空快照是诚实的待办事项,错误的快照则是虚假的断言。

以下是 Debug 生成的格式。这些示例是为了帮助你阅读已有快照,而不是编写新快照——编写工作仍应交给 -u:

被检查的值debug_inspect 的呈现结果
[1, 2, 3][1, 2, 3]
Point::{ x: 1, y: 2 }(结构体){ x: 1, y: 2 }
Shape::Rect(width=3, height=4)(带标签的枚举)Rect(width=3, height=4)
Host("example.com", 443)(位置参数枚举)Host("example.com", 443)
Err(InvalidEof)(Result + suberror)Err(InvalidEof)

较长的值会换行并缩进,这也是应该让 -u 来写入它们的另一个原因。

属性测试(v0.10.9)

QuickCheck 现在位于标准库 moonbitlang/core/quickcheck 中(别名为 @quickcheck)——请勿添加单独的 moonbitlang/quickcheck 依赖。有两个入口:

  • @quickcheck.check(property) — 失败时引发错误,成功时不输出内容。被测试的类型需要实现 Arbitrary + Shrink + Debug。
  • @quickcheck.report(property) — 返回结构化的 QuickCheckReport,而不是引发错误;只需要 Arbitrary + Shrink。
test "reverse is an involution" {
  @quickcheck.check((xs : Array[Int]) => xs.rev().rev() == xs)
}

derive(Arbitrary) 和 derive(Shrink) 会为自定义类型生成实例。可选的标签参数可调整运行:count?、max_size?、max_shrinks?、seed?(用于重现失败)、带有 discard_ratio? 的 filter?,以及通过 @quickcheck.{label, classify, collect} 获取统计信息的 observe?。

文档测试

可用于 .mbt.md 文件或 ///| 内联注释。

代码块行为
```mbt check由 LSP 检查
```mbt test作为 test {...} 执行
```moonbit仅显示(不执行)

示例(内联注释):


///|
/// Increment an integer by 1
/// ```mbt test
/// inspect(incr(41), content="42")
/// ```
pub fn incr(x : Int) -> Int {
  x + 1
}

发布前检查清单

发布前运行:

moon fmt   # Format code (also migrates deprecated syntax and JSON config)
moon info  # Generate type definition files

pkg.generated.mbti 由 moon info 自动生成。不要直接编辑。

探索内置类型方法

moon ide doc StringView   # StringView methods
moon ide doc Array        # Array methods
moon ide doc Map          # Map methods

快速参考

主题命令详情
测试moon testhttps://docs.moonbitlang.com/en/stable/language/tests
更新快照moon test -u同上
筛选测试moon test --filter 'glob'运行指定测试
基准测试moon benchhttps://docs.moonbitlang.com/en/stable/language/benchmarks
文档测试moon check / moon testhttps://docs.moonbitlang.com/en/stable/language/docs
格式化moon fmt将 moon.mod.json/moon.pkg.json 迁移为 DSL,并为 trait/impl 添加 fn 关键字。陷阱列表中的其他内容均不会处理——try?、fn new、is-main、extend 都需要手动处理
生成类型moon info-
解释错误代码moon explain E0087或使用 moon check --explain
文档参考moon ide doc <Type>moon doc <SYMBOL> 已弃用
搜索注册表moon search <keyword>使用 --json 输出机器可读内容
运行注册表工具moonx author/mod/cmd/tool替代已弃用的 moon runwasm
证明检查moon prove无需单独安装 Why3(v0.10.9)
工作区初始化moon work init参见 reference/configuration.md
添加工作区moon work use mod1 mod2将模块添加到工作区
API 使用分析moon ide analyze .显示公共 API 使用统计

moon ide 工具

用于代码导航,比 grep 更准确。详情参见 reference/ide.md。 子命令包括 peek-def、find-references、outline、doc、hover、 rename 和 analyze——没有 goto-definition。

# Show symbol definition (global search by symbol syntax)
moon ide peek-def Parser::read_u32_leb128

# Package outline
moon ide outline .

# Find references
moon ide find-references TranslationUnit

# Peek a definition from a source location (contextual, substring match)
moon ide peek-def Parser -loc src/parse.mbt:46:4

# Show type signature and docs at a location (unlike peek-def, shows inferred type + doc comments)
moon ide hover my_func -loc src/lib.mbt:10:4

# Rename symbol across the project
moon ide rename old_name new_name

# Analyze public API usage
moon ide analyze .              # Current package
moon ide analyze internal/*     # Glob pattern

函数式 for 循环

尽可能优先使用函数式 for 循环。这样可读性更好,也更容易推理。

// Functional for loop with state
for i = 0, sum = 0; i <= 10 {
  continue i + 1, sum + i  // Update state
} nobreak {
  sum  // Value at loop exit (nobreak, not else)
}

// Range for (recommended)
for i in 0..<n { ... }
for i, v in array { ... }  // index and value

// Range for carrying extra loop state (v0.10.0)
for x in xs; sum = 0; sum = sum + x { ... }

// Infinite loop (for { } is deprecated)
for ;; { ... }

循环带标签时,其中的 break / continue 也必须使用标签——在带标签循环内直接使用不带标签的语句已弃用。

带标签的块(v0.10.9)

块可以带标签;break label~ value 会携带该值退出块。可用于跳出嵌套控制流,而无需从函数返回。

fn absolute(n : Int) -> Int {
  result~: {
    if n < 0 {
      break result~ -n
    }
    n
  }
}

不带标签的 break 不能跳出带标签的块,而 continue 只能作用于循环。块和循环标签共用一个命名空间——重用外层标签会遮蔽它,并报告 E0036。

迭代器和列表推导式

[for .. => body] 会构建一个数组;在 => 前添加 if <guard> 可进行筛选。 结果类型取决于预期类型——默认是 Array,也可以是 FixedArray、ReadOnlyArray、String、Bytes 或 Json。

let evens = [for i in 0..<100 if i % 2 == 0 => i]
let labelled = [for i, x in ["a", "b", "c"] => "\{i}: \{x}"]
let text : String = [for x in 0..<3 => (x + 'a').unsafe_to_char()]

推导式中不允许使用 return / break / continue。

使用显式的 [| .. |] 字面量构建 Iter(v0.10.4)——依赖预期类型将 [x, ..xs] 转换为 Iter 已弃用,因为这会在不知不觉中改变求值顺序。

let prefix = [| 1, 2, 3 |]
let values = [| ..prefix, 4, 5 |]        // ..xs evaluated now, consumed lazily
let squares = [| for x in 1..<=3 => x * x |]  // fully lazy

对于带有状态的惰性或无限序列,直接构建 Iter:

let mut p1 = 1
let mut p2 = 0
let fibs : Iter[Int] = Iter::new(fn() {
  let next = p1
  p1 = p1 + p2
  p2 = next
  Some(next)
})
let first_six = fibs.take(6).collect()

迭代器只能单次遍历——被 each / fold / collect 消耗后,请向源请求一个新的迭代器。

模板写入 <+(v0.10.0)

buf <+ expr 会向 StringBuilder 追加内容——比 write_string 更简洁,并且适合搭配 $| 插值来构建标记/字符串。buf <? expr 会有条件地写入。

let buf = StringBuilder()
buf <+ "<ul>"
for item in items {
  buf <+ $|<li>\{item}</li>
}
buf <+ "</ul>"
buf.to_string()

Bytes 也支持插值:let b : Bytes = b"value=\{x}"(以 UTF-8 编码)。

正则表达式:=~、lexmatch、lexscan

根据需求,从三种结构中选择:

需求结构
进行一次布尔检查input =~ re"abc"
多个正则表达式分支返回不同的值lexmatch input with longest { ... }
对流进行分词并移动游标对 @lexbuf.{Lexbuf, AsyncLexbuf, StringScanner} 使用 lexscan
///| Boolean search (first-match, search-based — anchor with ^ / $ if needed)
let matched = s =~ re"abc"

///| Bind the match and the surrounding text
if s =~ (re"[0-9]+" as digits, before~, after~) { ... }

///| Multi-case dispatch over a String / StringView
let kind = lexmatch input with longest {
  (re"^[ \t\r\n]+") => Space
  (re"^[A-Za-z][A-Za-z0-9_]*" as t) => Ident(t)
  _ => Other
}

右侧必须是正则表达式常量表达式(字面量、const,或用 + / | / 括号组合的常量)——不接受运行时字符串。 旧版实验性的字符串片段 lexmatch 模式和 lexmatch? 已移除;lexscan 在 v0.10.9 中成为稳定功能。

字符串常量

const 支持字符串拼接和插值(v0.8.3+):

const Hello : String = "Hello"
const HelloWorld : String = Hello + " world"
const Message : String =
  $|========
  $|\{HelloWorld}
  $|========

错误处理

MoonBit 使用受检错误。详情请参阅 reference/ffi.md。

有三种声明形式——选择最精简且适用的一种:

///| No payload at all (the common case for a single failure mode).
/// This declares an error type `EmptyInput` with one payload-free constructor
/// of the same name — `raise EmptyInput` works directly.
suberror EmptyInput

///| `derive` attaches to the payload-free form too, with no braces in between
suberror NotFound derive(Eq, Debug)

///| One constructor carrying a payload — the constructor repeats the type name
suberror DivError { DivError(String) }

///| Several constructors, like an enum
suberror ParseError {
  InvalidEof
  InvalidChar(Char)
}

可见性规则与其他情况相同:pub suberror / pub(all) suberror / priv suberror。

对所有可能要快照或打印的错误类型派生 Debug。 下方 catch 形式返回的 Result 是一个容器,因此 debug_inspect 要求两侧都实现 Debug——包括错误:

suberror ParseError {
  InvalidEof
  InvalidChar(Char)
} derive(Eq, Debug)
///| Declare with raise, auto-propagates
fn parse(s: String) -> Int raise ParseError {
  if s.is_empty() { raise ParseError::InvalidEof }
  ...
}

///| Convert to Result — `try?` was removed (v0.10.0)
let result : Result[Int, ParseError] = Ok(parse(s)) catch { e => Err(e) }

///| Single expression with a fallback: omit `try`
let n = parse(s) catch { _ => 0 }

///| Panic instead of handling
let n = try! parse(s)

///| Full form: `noraise` branch handles the success value
let n = try parse(s) catch {
  ParseError::InvalidEof => -1
  _ => 0
} noraise {
  v => v
}

对该转换结果进行快照——组合情形:

test "parse failure" {
  let r : Result[Int, ParseError] = Ok(parse("")) catch { e => Err(e) }
  debug_inspect(r, content="")   // moon test -u fills in Err(InvalidEof)
}

如果为绑定标注具体的错误类型(Result[Int, ParseError] 而非 Result[Int, Error]),错误一侧就有 Debug 实例,快照也能正常工作。单独的 Error 是一种开放类型,无法为其附加 derive——请将签名收窄为你自己的 suberror,或匹配错误并对相应分支进行快照。

清理:defer 和 errdefer(v0.10.9)
///| defer: always runs on leaving the body (return / break / continue / raise)
fn read_all(path : String) -> String raise {
  let f = open(path)
  defer f.close()
  f.read_to_string()
}

///| errdefer: runs only when the body raises or is cancelled.
/// Use it when the resource is handed to the caller on success.
async fn connect(addr : Addr) -> Tcp {
  let sock = make_tcp_socket()
  errdefer sock.close()
  connect_tcp_socket(sock, addr)
  sock
}

连续的 defer 会按相反顺序运行。右侧不允许使用 return / break / continue;清理操作本身可以抛出错误(异步代码中也可以等待),此时其错误会替换原始错误。无法抛出错误的函数体中使用 errdefer 会报告 E0091;只进行清理并重新抛出的全捕获处理应改为 errdefer(E0092)。

Guard(v0.10.9)

guard 会进行穷尽性分析。可能失败的 guard 必须带有 else;如果编译器无法证明它总是成功,就会报告 E0087。当预期终止执行时,请使用 guard!——guard! 不接受 else。

///| Fallible guard: `else` supplies the value for the whole enclosing function
fn guarded_get(array : Array[Int], index : Int) -> Int? {
  guard index >= 0 && index < array.length() else { None }
  Some(array[index])
}

///| With a pattern, via `is`
fn plain_text(r : Resource) -> String raise Error {
  guard r is PlainText(text) else { fail("not plain text") }
  text
}

///| Provably total, or a deliberate panic: `guard!` takes no `else`
fn require_some(value : Int?) -> Int {
  guard! value is Some(result)
  result
}

穷尽条件中多余的 ! 或 else 现在会产生警告。

CI 和发布

第三方操作

优先使用官方 curl 安装程序(assets/ci.yaml)或搭配 moonbit-overlay 的 Nix(assets/ci-nix.yaml),而不是 hustcer/setup-moonbit@v1 等第三方操作。安装程序简短,且不会给操作供应链增加任何依赖;存在 flake.nix 时,Nix 方案完全可复现。

moon update 是必需的

运行器启动时注册表索引为空。任何依赖注册表托管的 MoonBit 依赖项的项目,在运行 moon update 之前,执行 moon check / moon test / moon build 都会失败,并提示 Failed to resolve registry dependency。在每个涉及 mooncakes 的工作流中,都应在安装 CLI 后立即运行 moon update。

发布构建依赖 MoonBit 的 npm 包

有些项目(例如 Vite 插件、语言工具)以 npm 包的形式发布,但会在 pnpm build 中运行 moon——例如,pnpm build:parser 会调用 moon -C tools/parser build --release --target js。在这种情况下,npm 发布工作流需要在 pnpm build 之前同时安装 MoonBit CLI 并运行 moon update,否则构建会在 CI 上失败。

请参阅 assets/publish-to-npm.yaml,了解一个由发布触发的精简工作流示例。它使用 OIDC Trusted Publishing(无需 NPM_TOKEN),并正确配置了 MoonBit。搭配发布自动化工具使用(请参阅 npm-release 技能,了解 release-please + OIDC 配置),以避免手动调用 npm publish。

发布到 mooncakes.io

包内容由 .moonignore 过滤(若不存在则使用 .gitignore); 默认排除以点开头的文件和 _build/。mooncakes.io 不接受仅大小写不同的 包名。发布前请验证制品:

moon publish --dry-run -v
unzip -l _build/publish/<author>-<module>-<version>.zip

资源

  • assets/moon.mod — DSL 格式的模块配置示例(替代 moon.mod.json)
  • assets/moon.pkg — DSL 格式的包配置示例(替代 moon.pkg.json)
  • assets/ci.yaml — GitHub Actions CI(curl 安装程序)
  • assets/ci-nix.yaml — 使用 Nix 的 GitHub Actions CI(moonbit-overlay)
  • assets/publish-to-npm.yaml — 由发布触发的 npm 发布示例,包含 MoonBit 构建和 OIDC Trusted Publishing
Nix 配置(moonbit-overlay)

moonbit-community/moonbit-overlay 提供了用于可复现 MoonBit 构建的 Nix flake overlay。

MoonBit 项目的精简 flake.nix:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    moonbit-overlay.url = "github:moonbit-community/moonbit-overlay";
    moon-registry = {
      url = "git+https://mooncakes.io/git/index";
      flake = false;
    };
  };

  outputs = { nixpkgs, moonbit-overlay, moon-registry, ... }:
    let
      system = "x86_64-linux"; # or aarch64-darwin, etc.
      pkgs = import nixpkgs {
        inherit system;
        overlays = [ moonbit-overlay.overlays.default ];
      };
      moonHome = pkgs.moonPlatform.bundleWithRegistry {
        cachedRegistry = pkgs.moonPlatform.buildCachedRegistry {
          moonModJson = ./moon.mod.json;
          registryIndexSrc = moon-registry;
        };
      };
    in {
      devShells.${system}.default = pkgs.mkShellNoCC {
        packages = [ moonHome pkgs.git ];
        env.MOON_HOME = "${moonHome}";
      };
    };
}

该 overlay 的主要 API:

  • pkgs.moonPlatform.buildMoonPackage - 将 MoonBit 包构建为 Nix 派生项
  • pkgs.moonPlatform.bundleWithRegistry - 创建包含缓存注册表的 MOON_HOME
  • pkgs.moonPlatform.buildCachedRegistry - 预先获取 mooncakes 注册表依赖项

该 overlay 的 moonModJson 参数仍需要 JSON 清单。如果你的模块已迁移到 moon.mod,请为 Nix 保留生成的 moon.mod.json,或在移除它之前检查该 overlay 是否支持 DSL。

评论

支持 Markdown