moon_zod/moonbit_syntax_pitfalls.md

14 KiB
Raw Blame History

MoonBit 语法避坑指南

基于 moon_zod 项目开发过程中踩过的真实错误汇总。


目录


1. 类型与变量

PascalCase 是禁区

变量必须用小写 snake_casePascalCase 是类型/枚举/构造器的保留命名规则。

// ❌ 编译错误unbound variable
let UserSchema = object({ ... })

// ✅ 正确
let user_schema = object({ ... })

全局变量需要类型注解

顶层的 let 绑定(非函数内部)经常需要显式写出类型:

// ❌ 类型推断失败
let schema = @moon_zod.object({ ... })

// ✅ 显式注解
let schema : @moon_zod.Schema = @moon_zod.object({ ... })

pub vs pub(all)

关键字 适用对象 说明
fn 函数 仅包内可见
pub fn 函数 外部包可见
pub(all) enum/struct 类型 外部包可见
// ❌ pub(all) 不能用于函数
pub(all) fn bench(n : Int) -> Bool { ... }

// ✅ 正确
pub fn bench(n : Int) -> Bool { ... }

没有三元运算符

MoonBit 没有 cond ? a : b,用 if-else 表达式替代:

let label = if n > 0 { "positive" } else { "non-positive" }

lazy 是保留关键字

MoonBit 已预留 lazy 关键字,不能用作函数名或变量名:

// ❌ 警告lazy is reserved for possible future use
pub fn lazy(f : () -> Schema) -> Schema { ... }

// ✅ 改用其他名称
pub fn recursive(f : () -> Schema) -> Schema { ... }

let rec 只支持函数

let rec 只能用于递归函数定义,不能用于递归

// ❌ 编译错误The value identifier tree is unbound
let rec tree = object({ "children": array(recursive(fn() { tree })).optional() })

// ✅ 用函数模式包装
fn tree_schema() -> Schema {
  recursive(fn() {
    object({
      "value": number(),
      "children": array(recursive(tree_schema)).optional(),
    })
  })
}

[] 创建空数组需要类型注解

let errors : Array[ValidationError] = []  // 需要类型
let tags = ["rust", "wasm", "ai"]          // 有初始值可推断

Result 是 Ok/Err,不是 Ok/Error

// ❌ Error 不是 MoonBit 的 Result 变体
let r : Result[Int, String] = Error("bad")

// ✅ 正确
let r : Result[Int, String] = Err("bad")

2. 函数与流程控制

fn main 中不能使用 Result-returning 函数

fn mainis-main 入口,不支持 Result 返回类型的函数(如 @json.parse())。必须通过 catch 语法处理:

// ❌ 错误Function with error is not allowed in `fn main`
let json = @json.parse(raw) match {
  Ok(v) => v
  Err(_) => return
}

// ✅ 正确:用 catch 处理
let json = @json.parse(raw) catch {
  _ => {
    println("Error: invalid JSON")
    return
  }
}

Match 臂内联时需 ; 分隔分支

当 match 写在一行时,分支之间需要 ; 分隔:

// ❌ 解析错误unexpected token `_`
match json { Number(v, ..) => v > 0.0 _ => false }

// ✅ 正确:换行或者用 ; 分隔
match json {
  Number(v, ..) => v > 0.0
  _ => false
}
match json { Number(v, ..) => v > 0.0; _ => false }

Match 分支返回值必须一致

所有 match arm 必须返回相同类型:

// ❌ 类型不一致
match mode {
  "moonzod" => bench(n)        // 返回 Bool
  "startup" => println("done") // 返回 Unit
}

// ✅ 用 let _ = 消化不需要的返回值
match mode {
  "moonzod" => { let _ = bench(n) }
  "startup" => println("done")
}

方法定义已弃用旧语法

新版已弃用 fn method(self : Type, ...),必须用 fn Type::method(self, ...)

// ❌ deprecated_syntax 警告
pub fn append_rule(self : Schema, check : ...) -> Schema { ... }

// ✅ 正确
pub fn Schema::append_rule(self, check : ...) -> Schema { ... }

3. 数据结构与集合

可变变量用 let mut

let mut i = 0
i = i + 1  // 重新绑定需要 mut

let mut 对 Array/Map 的误区

Array/Map 的内容修改不需要 mut,只有重新绑定变量才需要:

let errors : Array[ValidationError] = []

// ✅ push 不需要 mut修改内容
errors.push(ValidationError::{ ... })

// ❌ 重新赋值需要 mut
errors = []  // ← 需要 let mut errors

Map 取值返回 Option

match map.get("name") {
  Some(value) => // 存在
  None => // 不存在
}

Array pop() 返回 Option[T]

let top = stack.pop()    // Option[String]
let _ = stack.pop()      // 忽略 None

Map 方法返回 Iter不是 Array

Map.keys()Map.values() 返回 Iter 类型,不能直接链式调用 Array 方法:

// ❌ 类型错误Expr Type Mismatch, has type Iter[T], wanted Array[T]
let keys_json : Array[Json] = options.keys().map(fn(k) { Json::string(k) })

// ✅ 手动收集
let keys_json : Array[Json] = []
for k in options.keys() {
  keys_json.push(Json::string(k))
}

有载荷的 enum 变体用 ::{} 构造

// ❌ 编译错误
ValidationError("x", "bad", json)

// ✅ 正确
ValidationError::{ path: "x", message: "bad", got: json }

结构体更新用 .. 展开

{ ..schema, rules: schema.rules + [new_rule] }

For 循环两种写法

for i = 0; i < n; i = i + 1 { ... }     // C 风格
for element in array { ... }             // 迭代器风格
for i, element in array { ... }          // 带索引

4. Json 处理

构造器 vs 模式匹配

最容易踩的坑:构造用小写 Json::object(),模式匹配用大写 Object()

// ✅ 构造
let data = Json::object({ "name": Json::string("Alice") })

// ✅ 模式匹配
match data {
  Object(map) => map.get("name")
  _ => None
}

// ❌ 错误Cannot create values of the read-only type
let bad = Json::Object({ ... })

同理 Json::array()/Array()Json::string()/String()

Json 模式匹配注意 ..

match json {
  Number(v, ..) => // .. 必须,忽略额外字段
  Object(map) => ...
  Array(elements) => ...
}

字符串插值用 \{ }

// ❌ ${n} 被当作普通文本
println("n = ${n}")  // 输出: n = ${n}

// ✅ 正确
println("n = \{n}")

5. 模块与包

子包必须加 @ 模块前缀

子包(如 cmd/wasm/)引用上层模块时必须加前缀:

// ❌ 找不到 object
let schema = object({ ... })

// ✅ 正确
let schema = @moon_zod.object({ ... })

is-main 包不能被 import

moon.pkg 中标记 is-main: true 的包是独立可执行入口,不能被其他包 import。

MoonBit 核心包无需显式导入

moonbitlang/core/json 等核心包新版已自动处理,不需要在 moon.pkg 中声明。

// moon.pkg  核心包可省略
import {
  // "moonbitlang/core/json",   新版可省略
  "Betterlol/moon_zod",  // 外部包仍需声明
}

6. 测试

test 文件不能写 import

测试文件(test_*.mbt不能使用 import { ... } 语法。依赖需在 moon.pkg 中声明:

// ❌ test_xxx.mbt 中不能写 import
import { "Betterlol/moon_zod" }  // 编译错误

// ✅ 在 moon.pkg 中声明依赖

@debug.assert_eq 只有两个参数

// ❌ 错误given 3 positional arguments
@debug.assert_eq(a, b, "custom message")

// ✅ 正确
@debug.assert_eq(a, b)

moon test 只能发现根目录的 test_*.mbt

子目录(如 cmd/validate/)中的测试文件不会被自动发现。

没有 @os.execute() / @process

无法在单元测试中执行外部命令。CLI 功能需通过外部 shell 脚本验证。


7. Wasm 特定

只导出 _start

MoonBit --target wasm 编译后,只有 _startmemory 是导出项pub fn 不会成为独立 Wasm 导出函数。

// ❌ 不存在
wasm.exports.some_function()

// ✅ 只有 _start
wasm.exports._start()

解决方案CLI 参数分发模式。

fn main {
  let args = @env.args()
  let mode = args.get(1).unwrap_or("default")
  match mode {
    "bench" => { let _ = run_benchmark() }
    _ => println("usage")
  }
}

8. Trait 与泛型

Trait 定义语法

Trait 方法签名中的参数必须有名字,不能只写类型:

// ❌ 编译错误unexpected token id (uppercase start)
pub(open) trait Renderer {
  fn render_circle(Self, Double) -> String
}

// ✅ 正确:参数需要名字
pub(open) trait Renderer {
  fn render_circle(Self, radius: Double) -> String
  fn render_rect(Self, w: Double, h: Double) -> String
}

Trait 方法不能有方法体

Trait 定义中只能写签名,不能有 { } 方法体

// ❌ 编译错误unexpected token `{`
pub(open) trait Renderer {
  fn render(Self, Renderable) -> String { ... }
}

// ✅ 正确:只在 trait 里写签名
pub(open) trait Renderer {
  fn render_circle(Self, radius: Double) -> String
  fn render_rect(Self, w: Double, h: Double) -> String
}

默认方法实现用 impl Trait with

如果所有实现类型共享某个默认方法,写在独立的 impl Trait with fn中(注意:无 for Type

impl Renderer with fn render_inches(self, cm: Double) -> String {
  self.render_circle(cm / 2.54)  // 可调用 trait 中其他方法
}

Trait 实现必须用 with fn Type::method

pub struct SimpleRenderer {}

// ✅ 正确语法
pub impl Renderer for SimpleRenderer with fn render_circle(
  self: SimpleRenderer,
  radius: Double
) -> String {
  "circle:" + radius.to_string()
}

泛型函数 —— fn[...] 在函数名前

新版语法fn 关键字后立刻跟 [泛型参数],再跟函数名:

// ❌ 弃用语法 (deprecated_syntax 警告)
fn render[R : Renderer](renderer: R, st: Shape) -> String { ... }

// ✅ 正确
fn[R : Renderer] render(renderer: R, st: Shape) -> String { ... }

不支持带类型参数的 Trait

MoonBit 不支持 trait Foo[T],也不支持关联类型:

// ❌ 编译错误unexpected token `[`
pub(open) trait SchemaRenderer[T] {
  fn render_string(Self, s: Schema) -> T
}

// ✅ 正确做法:每个返回类型定义一个独立 trait
pub(open) trait StringRenderer {
  fn render_string(Self, s: Schema) -> String
}
pub(open) trait JsonRenderer {
  fn render_string(Self, s: Schema) -> Json
}

不能把 Trait 当参数类型

Trait 名不能直接作参数类型,必须用泛型约束:

// ❌ 编译错误Renderer is a trait, not a type
fn render(renderer: Renderer, shape: Shape) -> String { ... }

// ✅ 正确:用泛型约束
fn[R : Renderer] render(renderer: R, shape: Shape) -> String { ... }

结构体泛型参数不支持 Trait 约束

结构体定义中,类型参数不能用 : Trait 约束。约束只能加在方法上:

// ❌ 编译错误unexpected token `:`
pub struct Wrapper[R : Renderer] {
  inner: R
}

// ✅ 正确:结构体只写类型名
pub struct Wrapper[R] {
  inner: R
}
// 约束加在方法上
fn[R : Renderer] Wrapper::process(self: Wrapper[R], shape: Shape) -> String { ... }

Struct 不会自动生成构造函数

MoonBit 的 struct 必须显式声明构造函数,否则无法用 Type(args) 语法创建:

pub struct Point {
  x: Int
  y: String
}

// 必须写构造函数
pub fn Point::Point(x~ : Int, y~ : String) -> Point {
  { x, y }  // ~ 标记字段名即参数名
}

// 泛型结构体的构造函数
pub struct Wrapper[T] {
  inner: T
  prefix: String
}
pub[T] fn Wrapper::Wrapper(inner~ : T, prefix~ : String) -> Wrapper[T] {
  { inner, prefix }
}

构造函数的参数标记 ~ 表示字段名即参数名label punning调用时

let p = Point(x=10, y="hello")
let w = Wrapper(inner=some_val, prefix="tag")

空结构体的构造

空结构体需要用 Type::{} 语法在构造函数体中使用:

pub struct Empty {}
pub fn Empty::Empty() -> Empty {
  Empty::{}    // ← 不是 {}{} 会被解析为 Map 字面量
}

Match 臂中 { } 块可能引起解析歧义

在返回 Json 等需要推断类型的 match 臂中使用 { let ...; expr } 块,可能触发编译器关于表达式的歧义警告。如有歧义,可抽提为独立函数或简化 match 臂:

// ⚠️ 可能触发 "value cannot be implicitly ignored" 级联错误
DiscriminatedUnionType(_, options) => {
  let schemas : Array[@core.Schema] = []
  for _key, option in options { schemas.push(option) }
  renderer.render_union(schemas, schema)
}

// ✅ 简化为单一表达式
DiscriminatedUnionType(_, _) => Json::null()

字符串中 \{ 永远是插值

MoonBit 字符串插值用 \{expr}无法在插值字符串中包含字面量 {

// ❌ 编译错误:\{ radius: 被解析为插值开始
"Circle \{ radius: \{r} }"

// ✅ 正确:用 + 拼接
"Circle " + radius.to_string()

// ✅ 或用多行字符串 $|...|#
$|{ "radius": \{r} }|

附录:运行命令

moon test                          # 跑测试
moon build                         # 原生编译
moon build --target wasm --release # Wasm 编译
moon run cmd/validate -- --help    # 运行 CLI
moon info && moon fmt              # 更新接口 + 格式化
moon add moonbitlang/x             # 添加依赖