mcpField
M
moonbit-agent-guide

moonbit-agent-guide

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

关于 moonbit-agent-guide

面向 Agent 和开发者的 MoonBit 编码指南,涵盖项目结构、语言约定、测试和命令行工作流。指南强调使用 IDE 语义导航,并以 MoonBit 的 `.mbtx` 格式编写 Agent 自动化脚本。

主要功能

  1. 01说明 MoonBit 模块与包布局
  2. 02使用 `moon ide` 导航符号
  3. 03遵循语言与代码组织规则
  4. 04用 `.mbtx` 编写自动化
  5. 05验证、测试并格式化更改

适用场景

  1. 01熟悉不熟悉的 MoonBit 项目
  2. 02借助语义导航修改 API
  3. 03创建并验证 MoonBit 自动化脚本

安装

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

$npx skills add https://github.com/moonbitlang/moonbit-agent-guide --skill moonbit-agent-guide

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

包含的文件

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

.gitignore
ide.md
LICENSE
moon.mod
moon.pkg
README.md
references/
advanced-moonbit-build.md
moon.pkg
moonbit-language-fundamentals.mbt.md

详细介绍

MoonBit 任务清单

  • 应用迁移建议前,检查 moon version --all 和项目的依赖版本。本指南已使用 moon/moonc 2026-10-16(moonc v0.10.16)验证;下文有关异步取消的建议适用于 moonbitlang/async 0.22 及更高版本。
  • 编辑前,找到所在的 moon.mod 和相关的 moon.pkg 文件。
  • 使用 moon ide doc 探索 API;使用 outline、peek-def、find-references、hover 和 rename 进行语义导航与重构。
  • Agent 编写的自动化代码只能使用 .mbtx;不要将自动化逻辑放在 shell 脚本或其他脚本语言中。
  • 将更改限制在正确的包内。文件不定义命名空间;使用 ///| 分隔顶层条目。
  • 为 bug 修复添加回归测试。为新增的公共 API 添加黑盒测试和文档字符串示例。
  • 使用 moon check 和范围最小的相关 moon test 进行验证;仅在有意更改快照时使用 moon test --update。使用 moon explain --diagnostic 列出警告,并使用 --warn-list 启用选定的警告(例如,+unnecessary_annotation)。
  • 交付前运行 moon fmt,并检查 moon check 生成的 pkg.generated.mbti 更改,尤其是在公共 API 应保持稳定时。
  • 对于可执行文档,使用 --deny-warn 检查并测试每个 .mbt.md,不要一概抑制警告。示例也要格式化:兼容性语法可被接受,并不代表它就是规范语法。可以直接检查和测试独立 Markdown;如果 moon fmt 要求模块,则在临时模块中格式化副本,再将结果同步回来。

MoonBit 项目布局

MoonBit 使用 .mbt 作为源代码文件扩展名,使用 .mbti 作为接口文件扩展名。在 MoonBit 项目的顶层有一个 moon.mod 文件,用于指定项目的元数据。项目可以包含多个包,每个包都有自己的 moon.pkg。子目录中也可能包含 moon.mod 文件,表示该子目录可以使用一组不同的依赖。旧项目中可能仍包含 moon.mod.json;应将其视为旧版模块元数据格式,并将迁移或更新指南改为使用 moon.mod,而不是创建新的 moon.mod.json 文件。

布局示例

my_module
├── moon.mod                  # Module metadata; source option can specify the source directory
├── moon.pkg             # Package metadata (each directory is a package like Golang)
├── README.mbt.md             # Markdown with tested code blocks (`test "..." { ... }`)
├── README.md -> README.mbt.md
├── cmd                       # Command line directory
│   └── main
│       ├── main.mbt
│       └── moon.pkg     # executable package with `pkgtype(kind: "executable")`
├── liba/                     # Library packages
│   └── moon.pkg         # Referenced by other packages as `@username/my_module/liba`
│   └── libb/                 # Library packages
│       └── moon.pkg     # Referenced by other packages as `@username/my_module/liba/libb`
├── user_pkg.mbt              # Root packages, referenced by other packages as `@username/my_module`
├── user_pkg_wbtest.mbt       # White-box tests (only needed for testing internal private members, similar to Golang's package mypackage)
└── user_pkg_test.mbt         # Black-box tests
└── ...                       # More package files, symbols visible to current package (like Golang)
  • 模块:由项目根目录中的 moon.mod 文件标识。 MoonBit 模块类似于 Go 模块;它是由子目录中的多个包组成的集合,通常对应一个代码仓库或项目。 模块边界对依赖管理和导入路径很重要。

  • 包:由每个目录中的 moon.pkg 文件标识。 所有 moon 子命令仍会在模块所在目录(即存放 moon.mod 的目录)中执行,而不是在当前包目录中执行。 MoonBit 包是实际的编译单元(类似于 Go 包)。 同一包中的所有源文件会合并为一个单元,因此整个包共享所有定义。 moon.mod 文件中的 name 与包源代码目录的相对路径共同定义包名,而不是文件名。 导入引用的是模块路径与包路径,而绝不会引用文件名。

  • 文件: .mbt 文件只是包内的一段源代码。 文件名不会创建模块、包或命名空间。 可以自由拆分、合并或移动同一包中的声明。 无论位于哪个文件,包中的任何声明都可以引用该包中的其他声明。

必须遵循的编码/布局规则:

  1. 优先使用多个小型、内聚的文件,而不是一个大型文件。

    • 将相关类型和函数归入职责明确的文件(例如 http_client.mbt、router.mbt)。
    • 如果文件变得过大或职责不清,请新建文件并将相关声明移过去。
  2. 你可以自由地在同一包内的文件之间移动声明。

    • 每个代码块由 ///| 分隔。在文件之间移动函数/结构体/特征不会改变语义,前提是名称和公开状态保持不变。代码块的顺序也无关紧要。
    • 在包内拆分或合并文件进行重构是安全的。
  3. 文件名仅用于组织。

    • 不要假定文件名定义了模块,也不要在类型路径中使用文件名。
    • 文件名应描述功能或职责,而不是严格照搬类型名称。
  4. 添加新代码时:

    • 优先将代码添加到与该功能相符的现有文件中。
    • 如果没有合适的文件,请在同一包下创建一个名称具有描述性的文件。
    • 避免创建庞大的 impl、misc 或 util 文件。
  5. 测试:

    • 将测试放在相应包中的专用测试文件(例如 *_test.mbt)内。 对于一个包(除 *_test.mbt 文件外),*.mbt.md 文件也是黑盒测试文件,同时也是 Markdown 文件。 由三个反引号分隔的代码块 mbt check 会被视为测试用例,同时用于文档和测试。 你可以使用包含 mbt check 代码示例的 README.mbt.md 文件。你也可以将 README.mbt.md 符号链接到 README.md,以便更好地与 GitHub 集成。
    • 使用多个小型测试文件完全没问题,也很值得鼓励。
  6. 接口文件(pkg.generated.mbti) pkg.generated.mbti 文件是编译器为每个包的公共 API 生成的摘要。 它们以正式、简洁的形式概述所有导出的类型、函数和特征,不包含实现细节。 moon check 默认会生成这些文件,它们对代码审查很有用。如果某次提交没有更改公共 API,pkg.generated.mbti 文件就不会变化,因此建议在完成后将 pkg.generated.mbti 纳入版本控制。 不要直接修改 pkg.generated.mbti,包括仅清理空白字符;请使用 moon check 重新生成,并审查其差异,以此确认公共 API 的变化。

    有关 IDE 导航和符号查找命令,请参阅下方专门介绍 moon ide 的章节。

应避免的常见问题

  • 变量/函数名不要使用大写字母——否则会导致编译错误
  • 可变记录字段别忘了加上 mut——字段默认不可变(注意,通常无需为数组加上 mut,除非要将数组整体重新赋值给变量。例如,简单的 push 操作不需要 mut)
  • 不要忽略错误处理——要么显式处理错误,要么在调用方声明 raise,让已检查的错误向上传播
  • 不必要时不要使用 return——最后一个表达式就是返回值
  • 不要创建缺少 Type:: 前缀的方法——方法需要显式的类型前缀
  • Trait 语法:在 trait 声明中使用 fn,并使用 impl Trait for Type with fn method(...)。若要通过点语法公开 trait 实现,请显式使用 extend Type with Trait::{method};参见 references/moonbit-language-fundamentals.mbt.md 中的 methods 示例。
  • 别忘了处理数组越界情况——使用 get() 安全访问
  • 调用其他包中的函数时,别忘了加上 @package 前缀
  • 不要使用 ++ 或 --(不支持)——请使用 i = i + 1 或 i += 1
  • 不要为错误传播显式添加 try——在 raise 函数中,直接调用会引发错误的函数;使用 catch 在本地处理错误,仅在有意中止时使用 try!
  • 旧版语法:旧代码可能会使用 function_name!(...) 或 function_name(...)?——这些语法已弃用;传播错误时请使用普通调用。
  • 不要为 main 写空参数列表——使用 fn main { ... } 或 fn main raise { ... },不要使用 fn main() { ... } 或 fn main() raise ... { ... }
  • 不要使用记录式枚举或错误构造器字段——带标签的构造器字段应写为 label~ : Type,例如 InvalidNumber(input~ : String),而不是 InvalidNumber(input: String)
  • 优先使用范围 for 循环,而不是 C 风格循环——在 MoonBit 中,for i in 0..<(n-1) {...} 和 for j in 0..=6 {...} 更符合惯用写法
  • 无限循环不要写成 for { ... }——请改用 for ;; { ... }
  • 不要为了调试而 derive(Show)——请派生 Debug,并使用 debug_inspect() 输出测试/诊断信息(插值组合值时使用 \{Repr(value)})。仅在需要专用显示格式(JSON、XML、领域文本)时手动实现 impl Show
  • 不要调用 @json.inspect()——请使用前导模块中的 json_inspect(value, ...),不要加包前缀
  • JavaScript FFI:在 JavaScript 数组边界处使用 FixedArray[T]。可调整大小的 Array[T] 的 JavaScript 表示形式不是稳定的 FFI 契约。
  • Async——MoonBit 没有 await 关键字;不要添加该关键字。异步函数默认会引发错误,因此不要添加 raise;只有在异步函数体不能引发错误时才添加 noraise。 调用其他异步函数的函数和测试即为异步函数和测试。 要将函数或测试标记为异步,只需添加 async 前缀(例如 [pub] async fn ...、async test ...)。

moon 基础知识

脚本模式(.mbtx)

仅将 .mbtx 用于由 Agent 编写的自动化任务。将循环、条件判断、解析、转换和进程编排放在 MoonBit 中,而不是 shell 脚本或其他脚本语言中。外层 shell 只应启动脚本或运行直接、单一用途的命令。

.mbtx 文件由可选的 import { ... } 块和普通 MoonBit 代码组成,其中包括 main 函数。直接运行:

moon run path/to/script.mbtx [args...]

根据脚本实际使用的效果选择 main:

  • fn main { ... } 是同步的,不会传播错误。
  • fn main raise { ... } 是同步的,可能会传播受检错误。
  • async fn main { ... } 适用于调用异步 API 的脚本。导入 "moonbitlang/async";不要添加 raise 或 await。

这三种方式都适用于默认的 Wasm 目标。纯同步脚本不要标记为 async;MoonBit 会报告 unused_async。

自动化任务运行命令时,导入 "moonbitlang/async/shell" 并使用 @shell.Cmd 或 @shell.Pipeline。它们会将可执行文件与参数分开,并且绝不会调用 shell,因此 |、$() 和 * 等字符都会按原样传递。使用普通的 MoonBit 控制流,而不是 &&、shell 循环或命令替换。

脚本模式默认使用 Wasm。提供 --wasm-policy policy.json 可启用默认拒绝的 MoonBit 主机 API 控制;仅授予所需的文件系统、环境、网络或进程访问权限。对于进程自动化,优先使用范围较窄、指定确切程序和参数前缀的 process.allow 规则,而不是 process.spawn: true。该策略允许启动子进程,但不会对其自身进行沙盒隔离,因此如果子进程不受信任,主机也必须对其加以限制。

最简脚本(hello.mbtx):

fn main {
  println("Hello from MoonBit script mode")
}

命令行自动化(sum_args.mbtx):

import {
  "moonbitlang/core/env",
  "moonbitlang/core/string",
}

fn main raise {
  let args = @env.args()
  for arg in args[1:]; total = 0 {
    continue total + @string.parse_int(arg)
  } nobreak {
    println(total)
  }
}

运行 moon run sum_args.mbtx 10 20 12 会输出 42。

可直接在脚本头部导入包(format_json.mbtx):

import {
  "moonbitlang/core/json",
}

fn main raise {
  let source =
    #|{
    #|  "project": "moonbit",
    #|  "enabled": true
    #|}
  let value = @json.parse(source)
  println(value.stringify(indent=2))
}

异步自动化适用于默认的 Wasm 目标。例如,运行 moon run async_job.mbtx,以执行:

import {
  "moonbitlang/async",
}

async fn main {
  @async.sleep(10)
  println("async automation complete")
}

必备命令

  • moon new my_project - 创建新项目
  • moon run cmd/main - 运行主包
  • moon run - < hello.mbtx - 从标准输入运行脚本代码(适合快速试验)
  • moon run -e "code snippet" - 从命令行参数运行代码(适合单行代码)
    moon run -e 'fn main { println("Hello, MoonBit!") }'
    
  • moon build - 构建项目 (moon run 和 moon build 均支持 --target;moon build 还支持 --diagnostic-limit <N>)
  • moon check - 不构建项目,执行类型检查,并默认生成 pkg.generated.mbti 文件。请定期运行,速度很快。 查看生成的文件,了解是否有任何公共接口发生变化。 (moon check 还支持 --target 和 --diagnostic-limit <N>)
  • moon check --target all - 检查所有后端的类型 使用已保存的 .mbtx 脚本处理结构化诊断,而不是使用 shell 管道或其他脚本语言。例如,将以下内容保存为 filter_diagnostics.mbtx;@shell.Cmd::each_line 会直接运行 moon check、 流式输出其 JSON 结果,并返回其退出状态:

import { "moonbitlang/async", "moonbitlang/async/shell", "moonbitlang/core/json", }

async fn main { let seen : Map[String, Unit] = Map([]) let exit_code = @shell.Cmd( "moon", ["check", "--target", "all", "--output-json"], ).each_line(line => { try @json.parse(line) catch { _ => () } noraise { { "level": "warning", "path": String(path), .. } => if !seen.contains(path) { seen[path] = () println(path) } _ => () } }) if exit_code != 0 { fail("moon check exited with code {exit_code}") } }

使用允许该命令前缀的策略运行它:
```json
{
  "process": {
    "allow": [
      {
        "program": "moon",
        "args_prefix": ["check", "--target", "all", "--output-json"]
      }
    ]
  }
}
moon run --wasm-policy moon-check-policy.json filter_diagnostics.mbtx
  • moon explain - 显示编译器诊断和语言主题的内置文档。
    • moon explain --diagnostic 列出警告助记符和 ID。
    • moon explain --diagnostic 31 解释警告 31(unused_optional_argument)。
    • moon explain --diagnostic unused_optional_argument 通过助记符解释同一警告。
    • moon explain --attribute 列出支持的属性,例如 #deprecated、#alias、#cfg、#coverage.skip 和 #warnings。
    • moon explain --attribute deprecated 解释 #deprecated 属性及其支持的形式。
  • moon add package - 添加依赖项
  • moon remove package - 移除依赖项
  • moon fmt - 格式化代码 - 应定期运行 - 请注意,文件可能会被重写 注意,也可以使用 moon -C dir check 在指定目录中运行命令。
热点路径分析(moon run --profile)

moon run --profile --target native --release cmd/<main> 会在采样分析器下运行原生发布版本,并打印按自身耗时和包含耗时排序的表格,以及“归因于 MoonBit 调用方的运行时叶节点开销”部分(该部分会将分配、引用计数和字符串相等性开销映射回你的函数),同时生成可在 Instruments 中打开的 profile.json 和 .trace。在 macOS 上需要 Xcode 的 xcrun xctrace,因此请先安装完整的 Xcode(而不只是命令行工具)。单次解析或计算的耗时太短,无法进行有效采样,因此应让经过性能分析的 main 运行一个循环,针对有代表性的样例执行热点路径数百次;这个循环测试框架是临时的,绝不应提交。

查看自身耗时,了解哪个函数消耗了 CPU 周期;查看包含耗时,了解哪个调用子树占主导,然后进行紧凑的循环:分析性能、修复排名第一的问题,再次分析。务必在相信性能差异前重新建立基线——采样计时会随机器负载波动,因此应交错构建并基准测试分支和 main,而不是拿它们与早前会话中的数字比较。

测试命令
  • moon test - 运行所有测试 (moon test 也支持 --target)
  • moon test --update - 更新快照
  • moon test -v - 输出详细信息及测试名称
  • moon test [dirname|filename] - 测试指定目录或文件
  • moon coverage analyze - 分析覆盖率
  • moon test [dirname|filename] --filter 'glob' - 运行符合筛选条件的测试
    moon test float/float_test.mbt --filter "Float::*"
    moon test float -F "Float::*" // shortcut syntax
    

README.mbt.md 生成指南

  • 在软件包目录中输出 README.mbt.md。 *.mbt.md 文件和文档注释内容会特殊处理 mbt check。 mbt check 代码块会直接作为代码纳入,并由 moon check 和 moon test 运行。如果不想检查代码片段,建议显式使用 mbt nocheck。 如果只引用软件包中的类型,应使用 mbt nocheck,这样只会进行语法高亮。 将 README.mbt.md 符号链接到 README.md,以适配要求使用 README.md 的系统。

测试指南

建议使用快照测试,因为行为发生变化时更新起来很方便。

  • 快照测试:编写 inspect(value) / debug_inspect(value) / json_inspect(value),然后运行 moon test --update(或 moon test -u)来填充 content=。

    • 对实现了 Show 的值(基本类型或手动实现 Show 的类型)使用 inspect()。
    • 对派生了 Debug 的任何类型使用 debug_inspect()——这是自定义数据类型的默认选择。
    • 对复杂的嵌套结构使用 json_inspect()(使用 ToJson trait,生成更易读的输出)。
    • 如果函数的整个返回值不大,建议检查整个返回值,这样测试会更简单。根据需要为 YourType 派生 Debug 和/或 ToJson(或实现 Show)。
  • 更新工作流:更改会影响输出的代码后,运行 moon test --update 重新生成快照,然后检查测试文件中的差异(content= 参数会自动更新)。

  • 验证顺序:遵循 MoonBit Task Checklist。

  • 默认使用黑盒测试:只通过 @package.fn 调用公共 API。仅当私有成员很重要时才使用白盒测试。

  • 分组:将相关检查合并到一个 test "..." { ... } 块中,以提高速度和清晰度。

  • Panic:为测试命名时使用前缀 test "panic ..."{...};如果调用会返回值,则用 ignore(...) 包装以消除警告。

  • 错误:预期成功时,直接调用会抛出错误的函数。如果调用意外抛出错误,测试会以实际错误失败。预期失败时,使用 try ... catch { ... } noraise { ... },在 catch 中检查错误,并在 noraise 中显式使测试失败。 预期失败的默认写法:try f() catch { err => inspect(err) } noraise { _ => fail("expected to fail") }。

文档注释测试

建议为公共 API 编写文档注释测试。

///|
/// Return the sum of an `Array`.
///
/// # Example
/// ```mbt check
/// test {
///   inspect(sum_array([1, 2, 3, 4, 5, 6]), content="21")
/// }
/// ```
pub fn sum_array(xs : Array[Int32]) -> Int32 {
  xs.fold(init=0, (a, b) => a + b)
}

文档注释中的 MoonBit 代码会自动进行类型检查和测试(使用 moon test --update)。在文档注释中,mbt check 只能包含 test 或 async test。

规范驱动开发

  • 规范可以写在只读的 spec.mbt 文件中(这是惯用名称,但不是强制要求),并将存根代码标记为声明:

此处使用 mbt nocheck 展示了这个有意不完整的契约;它没有实现,不会作为本指南的一部分进行检查或运行。在项目中,提供实现之前,moon check 会报告未实现的声明。

///|
declare pub type Yaml

///|
declare pub fn Yaml::to_string(y : Yaml) -> String raise

///|
declare pub impl Eq for Yaml

///|
declare pub fn parse_yaml(s : String) -> Yaml raise
  • 添加 spec_easy_test.mbt、spec_difficult_test.mbt 等文件来测试规范函数;所有内容都会经过类型检查(moon check)。

  • 得益于我们的软件包组织方式,AI 或用户可以在不同文件中实现 declare 函数。

  • 运行 moon test 检查一切是否正确。

  • declare 支持函数、方法和类型。

  • pub type Yaml 这一行是有意设置的不透明占位符;其表示形式由实现者决定。

  • 请注意,规范文件也可以包含普通代码,而不只是声明。

moon ide [doc|peek-def|outline|find-references|hover|rename|analyze] 用于代码导航和重构

对于项目本地的符号和导航,请使用:

  • 使用 moon ide doc <query> 查找 MoonBit 中可用的 API、函数、类型和方法。探索可用 API 时,始终优先使用 moon ide doc,它比 grep_search 或任何基于正则表达式的搜索工具更强大、更准确。
  • 使用 moon ide outline . 扫描一个包,
  • 使用 moon ide find-references <symbol> 查找用法,以及
  • 使用 moon ide peek-def 获取内联定义上下文并定位顶层符号。
  • 使用 moon ide hover sym --loc filename:line:col 获取特定位置的类型信息。
  • 使用 moon ide rename <symbol> <new_name> [--loc filename:line:col] 预览语义重命名。添加 --apply 可重写文件。符号名称有歧义时,优先使用 --loc。
  • 使用 moon ide analyze [path] 在规划安全重构时检查包或模块的公共 API 用法。 这些工具可以节省 token,且比 grep 更精准(grep 会同时显示定义和调用位置中的结果,包括注释)。
用于 API 探索的 moon ide doc

moon ide doc 使用专为符号查找设计的查询语法:

  • 空查询:moon ide doc ''

    • 在模块中:显示当前模块中所有可用的包,包括依赖项和 moonbitlang/core
    • 在包中:显示当前包中的所有符号
    • 在包外:显示所有可用的包
  • 函数/值查找:moon ide doc "[@pkg.]value_or_function_name"

  • 类型查找:moon ide doc "[@pkg.]Type_name"(内置类型不需要包前缀)

  • 方法/字段查找:moon ide doc "[@pkg.]Type_name::method_or_field_name"

  • 包探索:moon ide doc "@pkg"

    • 显示包 pkg 并列出其所有导出符号
    • 示例:moon ide doc "@json"——探索整个 @json 包
    • 示例:moon ide doc "@encoding/utf8"——探索嵌套包
  • 多个查询:moon ide doc "query1" "query2" ...

    • 在一次调用中运行多个查询并合并结果
    • 示例:moon ide doc "String" "Array" "@json",一次探索多个类型和一个包
  • 通配匹配:使用 * 通配符进行部分匹配,例如 moon ide doc "String::*rev*" 可查找名称中包含 "rev" 的所有 String 方法

moon ide doc 示例
# search for String methods in standard library:
$ moon ide doc "String"

type String

  pub fn String::add(String, String) -> String
  # ... more methods omitted ...

$ moon ide doc "@buffer" # list all symbols in package buffer:
moonbitlang/core/buffer

fn from_array(ArrayView[Byte]) -> Buffer
# ... omitted ...

$ moon ide doc "@buffer.new" # list the specific function in a package:
package "moonbitlang/core/buffer"

pub fn new(size_hint? : Int32) -> Buffer
  Creates ... omitted ...


$ moon ide doc "String::*rev*"  # globbing
package "moonbitlang/core/string"

pub fn String::rev(String) -> String
  Returns ... omitted ...
  # ... more

pub fn String::rev_find(String, StringView) -> Int32?
  Returns ... omitted ...

最佳实践:将本节视为命令参考;验证要求定义在 MoonBit Task Checklist 中。

moon ide rename sym new_name [--loc filename:line:col] 示例

当用户询问:“可以将函数 compute_sum 重命名为 calculate_sum 吗?”

$ moon ide rename compute_sum calculate_sum --loc math_utils.mbt:2

*** Begin Patch
*** Update File: cmd/main/main.mbt
@@
 ///|
 fn main {
-  println(@math_utils.compute_sum(1, 2))
+  println(@math_utils.calculate_sum(1, 2))
 }
*** Update File: math_utils.mbt
@@
 ///|
-pub fn compute_sum(a: Int32, b: Int32) -> Int32 {
+pub fn calculate_sum(a: Int32, b: Int32) -> Int32 {
   a + b
 }
*** Update File: math_utils_test.mbt
@@
 ///|
 test {
-  inspect(@math_utils.compute_sum(1, 2))
+  inspect(@math_utils.calculate_sum(1, 2))
 }
*** End Patch
moon ide hover sym --loc filename:line:col 示例

当用户询问:“filter 的签名和文档字符串是什么?在 hover.mbt 第 14 行。”

$ moon ide hover  filter --loc hover.mbt:14
test {
  let a: Array[Int32] = [1]
  inspect(a.filter((x) => {x > 1}))
            ^^^^^^
            ```moonbit
            fn[T] Array::filter(self : Array[T], f : (T) -> Bool raise?) -> Array[T] raise?
            ```
            ---
            
             Creates a new array containing all elements from the input array that satisfy
             ... omitted ...
}
moon ide peek-def sym [--loc filename:line:col] 示例

当用户询问:“可以检查一下 Parser::read_u32_leb128 的实现是否正确吗?” 你可以运行 moon ide peek-def Parser::read_u32_leb128 来获取定义上下文 (这比 grep 更好,因为它会基于语义搜索整个项目):

L45:|///|
L46:|fn Parser::read_u32_leb128(self : Parser) -> UInt32 raise ParseError {
L47:|  ...
...:| }

现在,如果你想查看 Parser 结构体的定义,可以运行:

$ moon ide peek-def Parser --loc src/parse.mbt:46:4
Definition found at file src/parse.mbt
  | ///|
2 | priv struct Parser {
  |             ^^^^^^
  |   bytes : Bytes
  |   mut pos : Int32
  | }
  |

对于 --loc 参数,行号必须准确;列号可以是近似值,因为位置参数 Parser 有助于定位。

如果“sym”是顶层符号,则可以省略位置:

$ moon ide peek-def String::rev
Found 1 symbols matching 'String::rev':

`pub fn String::rev` in package moonbitlang/core/builtin at /Users/usrname/.moon/lib/core/builtin/string_methods.mbt:1039-1044
1039 | ///|
     | /// Returns a new string with the characters in reverse order. It respects
     | /// Unicode characters and surrogate pairs but not grapheme clusters.
     | pub fn String::rev(self : String) -> String {
     |   self[:].rev()
     | }
用于包符号的 moon ide outline [dir|file] 和 moon ide find-references <sym>

使用 moon ide outline 扫描包或文件中的顶层符号,并查找其用法,无需使用 grep。

  • moon ide outline dir 概览当前包目录(按文件显示标题)
  • moon ide outline parser.mbt 概览单个文件 当你需要快速了解一个包,或在使用 peek-def 前找到合适的文件时,这很有用。
  • moon ide find-references TranslationUnit 查找当前模块中对某个符号的所有引用
$ moon ide outline .
spec.mbt:
 L003 | pub(all) enum CStandard {
        ...
 L013 | pub(all) struct Position {
        ...
$ moon ide find-references TranslationUnit

包管理

发现模块和依赖项
moon search 'html markdown' --limit 5
moon view moonbitlang/async --versions
moon tree --json
moon tree --package --target native --json

search 和 view 查询注册表,可在项目之外使用。tree 检查当前项目解析后的依赖项;--package 显示包导入项,而不仅仅是模块依赖项。添加或升级前,先查明版本,而不是照搬旧示例中的版本。

添加依赖项
moon add moonbitlang/x        # Add latest version
moon add moonbitlang/[email protected]  # Add specific version
更新依赖项
moon update                   # Update package index
浏览第三方源代码(moon fetch)

moon fetch <author>/<module>[@<version>] 会将包的源代码下载到 .repos/<author>/<module>/<version>/,以便离线阅读(例如示例、内部实现、生成的 .mbti)。它不会将包添加到 moon.mod 中——要添加包,请使用 moon add。将 .repos/ 添加到 .gitignore。

moon fetch moonbitlang/[email protected]   # browse source/examples without taking a dependency
常见的模块配置(moon.mod)
name = "username/hello"
version = "0.1.0"
readme = "README.mbt.md"
repository = ""
license = "Apache-2.0"
keywords = []
description = "..."

import {
  "moonbitlang/[email protected]",
}

options(
  // source: "src", // Optional; default is "."
  "preferred-target": "native",
)

使用 moon add moonbitlang/[email protected] 和 moon remove moonbitlang/x 管理 import 块,而不是手动编辑依赖版本。

常见的包配置(moon.pkg)

简洁版 moon.pkg

import {
  "username/hello/liba",
  "moonbitlang/x/encoding" @libb,
}
import {
  "username/hello/test_helpers",
} for "test"
import {
  "username/hello/internal_test_helpers",
} for "wbtest"
pkgtype(kind: "executable")

如果整个包仅支持选定的后端,可在顶层使用 supported_targets = "native" 或其他目标集表达式。

supported_targets = "native"
pkgtype(kind: "executable")

每个目录对应一个包;没有 moon.pkg 文件的目录不会被识别为包。

每个包只能声明一个 pkgtype:library(默认值)、用于 main 入口点的 executable,或供外部宿主使用导出的 foreign_library。这些选项互斥。pkgtype(kind: "executable") 替代旧版 options("is-main": true);pkgtype(kind: "foreign_library") 替代旧版 options(link: true)。特定后端的 options(link: { ... }) 仍用于配置链接器详情;参见 references/advanced-moonbit-build.md。

包导入(用于 moon.pkg)
  • 导入格式:"module_name/package_path"
  • 用法:使用 @alias.function() 调用导入的函数
  • 默认别名:路径的最后一部分(例如,username/hello/liba 的别名为 liba)
  • 包引用:在测试文件中使用 @packagename 引用 被测试的包

包别名规则:

  • 导入 "username/hello/liba" → 使用 @liba.function()(默认别名是路径的最后一段)
  • 使用自定义别名导入 import { "moonbitlang/x/encoding" @enc} → 使用 @enc.function() (注意:如果路径的最后一段与别名相同,则无需自定义别名。)
  • 在 _test.mbt 中,显式使用被测试包的别名(@packagename.fn);不带限定的访问依赖兼容性导入,并会触发 test_unqualified_package。_wbtest.mbt 与包共享作用域,可直接访问私有定义。

示例:

///|
/// In main.mbt after importing "username/hello/liba" in `moon.pkg`
fn main {
  println(@liba.hello()) // Calls hello() from liba package
}
使用标准库(moonbitlang/core)

moonbitlang/core 模块始终可用,无需将其添加到 moon.mod 依赖项中。普通的 core 包仍需在 moon.pkg 中显式导入,以使用 @utf8、@json 或 @strconv 等包别名;如果编译器报告缺少 core 包或隐式使用 core 包,请添加类似 "moonbitlang/core/encoding/utf8" 的导入项。

创建包

要在 . 下添加新包 fib:

  1. 创建目录:./fib/

  2. 添加 ./fib/moon.pkg

  3. 添加包含代码的 .mbt 文件

  4. 在依赖它的包中导入:

      import {
        "username/hello/fib",
      }
    

有关 conditional compilation、link configuration、warning control 和 pre-build commands 等更高级的主题,请参见 references/advanced-moonbit-build.md。

异步 IO

异步编程使用编译器支持以及 moonbitlang/async 运行时。导入运行时后,async fn main 可在默认 Wasm 目标和原生目标上运行。各个主机 I/O API 仍可能因目标而异;请使用 moon ide doc "@async" 并在预期目标上验证。

面向用户的子包包括 @async(任务、计时器、取消)、 @async/aqueue、@async/fs、@async/shell(不依赖 shell 的进程编排)、 @async/stdio 和 @async/websocket。 每个子包都必须在 moon.pkg 或 .mbtx 导入块中单独导入。

  1. 使用 moon add moonbitlang/async 添加依赖。
  2. 在可执行文件的 moon.pkg 中声明包类型并导入所需内容:
    import {
      "moonbitlang/async",
      "moonbitlang/async/stdio",
    }
    pkgtype(kind: "executable")
    
  3. 定义 async fn main(而非 fn main)。通过 with_task_group 启动并发任务,以实现结构化并发:
    ///|
    async fn main {
      @async.with_task_group(group => {
        group.spawn_bg(() => {
          @async.sleep(50)
          @stdio.stdout.write("A\n")
        })
        group.spawn_bg(() => {
          @async.sleep(20)
          @stdio.stdout.write("B\n")
        })
      })
    }
    
  • 异步函数默认具有可引发效应。请写成 async fn main { ... } 或 async fn f(...) { ... },而不是 async fn main raise { ... }。
  • 仅当异步函数体必须不引发错误时,才使用 async fn f(...) noraise { ... }。noraise 异步函数不能调用可能失败的 API,除非在本地处理其错误。

with_task_group 的结构化并发约定:

  • with_task_group 返回时,组内启动的所有任务都已终止。任务仍必须负责清理其获取的资源。
  • 如果任何已启动的任务失败(且启动时未指定 allow_failure=true),整个组都会失败:组内所有其他任务都会被取消,错误会从 with_task_group 传播出去。
  • 被取消的任务不属于普通失败,也不会触发同组任务取消。在 async 0.22+ 中,取消是编译器信号,而非 suberror。
取消和清理(async 0.22+)

catch 用于处理错误,而非取消。使用 defer 确保在每种退出情况下都进行清理,使用 errdefer 在发生错误或取消时进行清理。仅用于清理的全捕获处理方式应迁移为上述写法。

noraise 并不意味着不可取消:@async.sleep 是 noraise,但仍可被取消。nocancel 是一种独立的效应;当清理必须在取消时仍完成,请使用 @async.protect_from_cancel。TaskGroup::add_defer 要求回调具有 nocancel 属性。

仅当需要显式处理取消时,才使用 @async.handle_cancellation。取消时它会返回 None,但不会重置任务的已取消状态。@async.is_cancellation_error 已弃用,且始终返回 false;with_cancellation_handler 已由 handle_cancellation 取代。请参阅 async 0.22 迁移说明。

spawn_bg / spawn 的闭包语法:

  • ✅ () => { ... } — 符合惯用写法;异步属性会根据上下文推断。
  • ✅ async fn() { ... } — 显式标注;等价于箭头形式。
  • ⚠️ fn() { ... } — 会触发 Warning [0027] deprecated_syntax:“this fn is asynchronous but not annotated with async”。请勿使用。
  • ❌ async () => ...、fn() async { ... }、fn(args) async { ... } — 均会导致解析错误。async 只能放在 fn 前,不能放在箭头 lambda 前或参数列表后。
异步测试

对于调用异步函数的测试,请使用 async test。包含该测试的包必须为测试模式导入 moonbitlang/async;测试中使用的任何异步子包也要在同一个 for "test" 代码块中导入。

import {
  "moonbitlang/async",
  "moonbitlang/async/stdio",
} for "test"
///|
async test "sleep completes" {
  @async.sleep(1)
  inspect("done", content="done")
}
  • 没有 await 关键字(类似于可能引发错误的函数)。在 async test 中,直接正常调用异步函数。
  • async test 默认也具有异步引发错误的效果;不要添加 raise。
  • 异步测试默认并行运行。除非每个测试都能隔离所用资源,否则请避免共享端口、文件、环境变量和全局可变状态。
  • 在目标平台上运行 moon test;仅当被测 API 要求原生执行时,才添加 --target native。检查测试名称或异步调度行为时,使用 moon test -v。
  • 在 README.mbt.md 和文档字符串中,mbt check 代码块可以包含 async test 代码块;请确保相应测试模式的包已导入 moonbitlang/async。

MoonBit 语言导览

核心事实

  • 表达式导向:if、match 和循环都会返回值;最后一个表达式就是返回值。
  • 默认使用引用:数组/映射/结构体通过引用进行修改;需要修改基本类型时,请使用 Ref[T]。
  • 代码块:使用 ///| 分隔顶层项。按代码块逐块生成代码。 如果希望代码块内部(由花括号括起)保留空行,请在空行后添加一行注释(可以带或不带注释文本)。
  • 可见性:fn 默认是私有的;pub 会在允许的范围内开放读取和构造;pub(all) 允许外部构造。
  • 命名约定:值/函数使用 lower_snake;类型/枚举使用 UpperCamel;枚举变体以 UpperCamel 开头。
  • 包:代码文件中没有 import;通过 @alias.fn 调用。在 moon.pkg 中配置导入。
  • 占位符:在 MoonBit 代码中,... 是表示未完成实现的有效占位符。
  • 全局值:默认不可变,通常需要类型注解。
  • 垃圾回收:MoonBit 使用 GC;没有生命周期注解,也没有所有权系统。 与 Rust 不同,但与 F# 类似,只有在需要重新赋值变量时才需要 let mut,修改结构体字段或数组/映射元素时则不需要。

MoonBit 错误处理(已检查错误)

MoonBit 使用已检查的错误引发函数,而不是未经检查的异常。所有错误都是 Error 的子类型;你可以使用 suberror 声明自己的错误类型。

已检查的错误会记录在函数签名中,而不是在每个调用点标记。可能引发错误的函数会声明 raise 或 raise SomeError。如果调用方只想向上传递该错误,也要声明兼容的 raise,并正常调用可能引发错误的函数。

  • 在 raise 函数中进行普通调用:错误会自动向上传递。
  • fn main raise { ... } 可用于需要传递错误的同步命令行探测和小型示例。对于异步入口点,请使用 async fn main { ... };异步函数默认可以引发错误。
  • 在 suberror 构造器中,带标签的负载使用 label~ : Type;调用和模式匹配时使用 label=value。
  • 使用 expr catch { ... } 或 try { ... } catch { ... } 显式处理。
  • 使用 try! expr:如果引发错误,则终止执行。

不要添加用于传递错误的 Swift 风格 try。新代码不要使用旧式 function_name!(...) 或 function_name(...)? 语法。

///|
/// Declare error types with 'suberror'
suberror ValueError {
  ValueError(String)
}

///|
/// Tuple struct to hold position info
struct Position(Int32, Int32) derive(ToJson, Debug, Eq)

///|
/// ParseError is subtype of Error
pub(all) suberror ParseError {
  InvalidChar(pos~ : Position, Char) // pos is labeled
  InvalidEof(pos~ : Position)
  InvalidNumber(pos~ : Position, String)
  InvalidIdentEscape(pos~ : Position)
} derive(Eq, ToJson, Debug)

///|
/// Functions declare what they can throw
fn parse_int(s : String, position~ : Position) -> Int32 raise ParseError {
  // 'raise' throws an error
  if s is "" {
    raise ParseError::InvalidEof(pos=position)
  }
  @string.parse_int(s) catch {
    _ => raise ParseError::InvalidNumber(pos=position, s)
  }
}

///|
/// Declare a specific error type when callers should handle it precisely
fn div(x : Int32, y : Int32) -> Int32 raise ValueError {
  if y is 0 {
    raise ValueError::ValueError("Division by zero")
  }
  x / y
}

///|
test "expected success calls directly" {
  inspect(div(6, 3), content="2")
}

///|
test "expected failure handles the raised error" {
  try div(1, 0) catch {
    ValueError::ValueError(message) =>
      inspect(message, content="Division by zero")
  } noraise {
    _ => fail("expected to fail")
  }
}

// Three ways to handle errors:

///|
/// Propagate automatically
fn use_parse(s : String, position~ : Position) -> Int32 raise ParseError {
  // This plain call is the correct propagation syntax.
  // `try! parse_int(...)` would abort instead of propagating.
  let x = parse_int(s, position~) // label punning, equivalent to position=position
  // Error auto-propagates by default.
  // Unlike Swift, you do not need to mark `try` for functions that can raise
  // errors; the compiler infers it automatically. This keeps error handling
  // explicit but concise.
  x * 2
}

///|
/// Use try! to abort if it raises, no raise in the signature 
fn use_parse2(position~ : Position) -> Int32 {
  let x = try! parse_int("123", position~) // label punning
  x * 2
}

///|
/// Handle with try-catch
fn handle_parse(s : String, position~ : Position) -> Int32 {
  parse_int(s, position~) catch {
    ParseError::InvalidEof(pos=_) => {
      println("Parse failed: InvalidEof")
      -1 // Default value
    }
    _ => 2
  }
}

///|
test "error propagation and handling" {
  let position = Position(0, 0)
  inspect(use_parse("21", position~), content="42")
  inspect(use_parse2(position~), content="246")
  inspect(handle_parse("", position~), content="-1")
  inspect(handle_parse("invalid", position~), content="2")
}

重要:调用可能引发错误的函数时,如果你只想向上传递错误,则无需添加任何标记;编译器会推断。异步函数默认可以引发错误,无需显式声明。不要为了传递错误而给异步函数添加 raise;只有当异步函数必须拒绝未处理的错误时,才添加 noraise。

使用 SubErrorType::_ as err 匹配某种错误类型的所有构造器,并将 Error 值缩窄为该子类型:

///|
fn is_parse_error(error : Error) -> Bool {
  match error {
    ParseError::_ as err => {
      let _ : ParseError = err
      true
    }
    _ => false
  }
}

///|
test "matching an error subtype" {
  assert_true(is_parse_error(ParseError::InvalidEof(pos=Position(0, 0))))
  assert_false(is_parse_error(ValueError::ValueError("other")))
}

Integer、Char 和重载字面量

MoonBit 支持 Byte、Int16、Int32、UInt16、UInt32、Int64、UInt64 等类型。 在新示例中使用 Int32 和 UInt32。2026-10-16 版格式化程序会将仍受支持的别名 Int 和 UInt 规范化为这些名称。 已知类型时,字面量可以重载:

///|
test "integer and char literal overloading disambiguation via type in the current context" {
  let (int, uint, uint16, int64, byte) : (Int32, UInt32, UInt16, Int64, Byte) = (
    1, 1, 1, 1, 1,
  )
  // The literal `1` is overloaded based on the expected type in the current context.
  // compile time error if the literal cannot be represented in the target type, 
  // e.g. let a7 : Byte = 256 // ❌ won't compile, 256 exceeds Byte max value 255
  assert_eq(int, uint16.to_int())
  assert_eq(uint, 1U)
  assert_eq(int64, 1L)
  assert_eq(byte, b'\x01')
  let (a1, a2, a3) : (Int32, Char, UInt16) = ('b', 'b', 'b')
  // char literal overloading, `a1` will be the unicode value of 'b', 
  // compile time error when the literal cannot be represented in the target type 
  // e.g, let a6 : UInt16 = '𐍈' // ❌ won't compile, '𐍈' is U+10348, which exceeds UInt16 max value 0xffff  
  let a4 : Byte = b'b' // Byte literal
  assert_eq(a1, 98)
  assert_eq(a2, 'b')
  assert_eq(a3.to_int(), a1)
  assert_eq(a4, b'b')
}

Bytes(不可变)

///|
test "bytes literals" {
  let b0 : Bytes = b"abcd"
  let b1 : Bytes = [0xff, 0x00, 0x01] // Array literal overloading
  assert_eq(b1.length(), 3)
  guard b0 is [b'a', ..] && b0[1] is b'b' else {
    // Bytes can be pattern matched as BytesView and indexed
    fail("unexpected bytes content")
  }
}

Array(可调整大小)

///|
test "array literals overloading: disambiguation via type in the current context" {
  let (a0, a1, a2, a3) : (
    Array[Int32],
    FixedArray[Int32],
    ReadOnlyArray[Int32],
    ArrayView[Int32],
  ) = ([1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3])
  // The literal `[1, 2, 3]` is overloaded based on the expected type in the current context.
  // Defaults to Array[_]
  assert_eq(a0.length(), 3)
  assert_eq(a1.length(), 3)
  assert_eq(a2.length(), 3)
  assert_eq(a3.length(), 3)
}

String(不可变 UTF-16)

s[i] 返回一个码元(UInt16),s.get_char(i) 返回 Char?。 由于 MoonBit 支持字符字面量重载,你可以编写如下代码片段:

///|
test "string indexing and utf8 encode/decode" {
  let s = "hello world"
  let b0 : UInt16 = s[0]
  guard b0 is ('\n' | 'h' | 'b' | 'a'..<='z') && s is [.. "hello", .. rest] else {
    fail("unexpected string content")
  }
  guard rest is " world" else { fail("unexpected string suffix") }

  // In check mode (expression with explicit type), ('\n' : UInt16) is valid.

  // Using get_char for Option handling
  let b1 : Char? = s.get_char(0)
  assert_true(b1 is Some('a'..<='z'))

  // ⚠️ Important: Variables won't work with direct indexing
  let eq_char : Char = '='
  assert_false(s.get_char(0) == Some(eq_char))
  // s[0] == eq_char // ❌ Won't compile - eq_char is not a literal, lhs is UInt16 while rhs is Char
  // Use: s[0] == '=' or s.get_char(0) == Some(eq_char)
  // Requires `"moonbitlang/core/encoding/utf8"` in `moon.pkg`.
  let bytes = @utf8.encode("中文")
  assert_true(bytes is [0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87])
  let s2 : String = @utf8.decode(bytes) // decode utf8 bytes back to String
  assert_true(s2 is "中文")
  for c in "中文" {
    let _ : Char = c // unicode safe iteration
    println("char: \{c}") // iterate over chars
  }
}
字符串插值 && StringBuilder

MoonBit 使用 \{} 进行字符串插值;对于自定义类型,它们需要实现 trait Show。

///|
test "string interpolation basics" {
  let name : String = "Moon"
  let config = { "cache": 123 }
  let version = 1.0
  println("Hello \{name} v\{version}") // "Hello Moon v1"
  // ✅ Quoted map keys are allowed inside interpolation expressions.
  println("  - Checking if 'cache' section exists: \{config["cache"]}")
  let sb = StringBuilder()
  sb <+ "[\{[ for x in [1, 2, 3] => "\{x}" ].join(",")}]"
  inspect(sb, content="[1,2,3]")
  let x = 42
  let streamed = StringBuilder()
  streamed <+ "hello \{x}"
  inspect(streamed, content="hello 42")
}

\{} 内的表达式必须是单行表达式。 支持嵌套插值和字符串字面量,但 \{} 内不能换行。

用于流式插值的 <+ 和 <? 宏

字符串插值可以通过 <+ 直接流式写入 Logger/StringBuilder 风格的写入器,也可以通过 <? 有条件地写入可选写入器:

writer <+ "hello \{x}"
writer <+ {"key1": value, "key2": value2}
lhs <? "hello \{x}"
lhs <? {"key1": value, "key2": value2}

这会展开为对写入器的调用:

writer.write_string("hello ")
writer.write(x)
writer.write_object_begin()
writer.write_object_field("key1", value)
writer.write_object_field("key2", value2)
writer.write_object_end()
if lhs is Some(l) { l <+ "hello \{x}" }

字面量字符串片段使用 write_string;插值表达式使用 write。 对于 <?,None 不执行写入;Some(writer) 则对包装的写入器应用相同的 <+ 展开。 <+ 和 <? 的右侧必须是模板字符串/多行模板字符串或映射对象字面量,不能是任意表达式。 展开采用宏风格:它取决于 writer 类型如何为模板字符串实现 write_string 和 write,以及如何为映射对象字面量实现 write_object_begin、write_object_field 和 write_object_end。HTMLBuilder 或 JSONBuilder 等类型可以使用相同语法支持插值和流式写入,但语义不同。 由于 MoonBit 允许在外部类型上定义局部方法,包可以通过添加这些局部写入器方法,让现有写入器类型适配此语法。

多行字符串
///|
test "multi-line string literals" {
  let multi_line_string : String =
    #|Hello "world"
    #|World
    #|
  let multi_line_string_with_interp : String =
    $|Line 1 ""
    $|Line 2 \{1+2}
    $|
  // no escape in `#|`,
  // only escape '\{..}` in `$|`
  assert_eq(multi_line_string, "Hello \"world\"\nWorld\n")
  assert_eq(multi_line_string_with_interp, "Line 1 \"\"\nLine 2 3\n")
}

Map(可变,保留插入顺序)

///|
test "map literals and common operations" {
  // Map literal syntax
  let map : Map[String, Int32] = { "a": 1, "b": 2, "c": 3 }
  let empty : Map[String, Int32] = Map([]) // Empty map
  // From array of pairs
  let from_pairs : Map[String, Int32] = Map::from_array([("x", 1), ("y", 2)])
  assert_true(empty.is_empty())
  inspect(from_pairs["x"], content="1")

  // Set/update value
  map["new-key"] = 3
  map["a"] = 10 // Updates existing key

  // Get value - returns Option[T]
  guard map is { "new-key": 3, "missing"? : None, .. } else {
    fail("unexpected map contents")
  }

  // Direct access (panics if key missing)
  let value : Int32 = map["a"] // value = 10
  inspect(value, content="10")

  // Iteration preserves insertion order
  for k, v in map {
    println("\{k}: \{v}") // Prints: a: 10, b: 2, c: 3, new-key: 3
  }

  // Other common operations
  map.remove("b")
  guard map is { "a": 10, "c": 3, "new-key": 3, .. } && map.length() == 3 else {
    // "b" is gone, only 3 elements left
    fail("unexpected map contents after removal")
  }
}

视图类型

核心概念:视图类型(StringView、BytesView、ArrayView[T])可在不复制底层数据的情况下,只读访问一个范围。接受视图的参数也可以通过隐式转换接受相应的 String、Bytes 或数组。反向转换则需要显式物化视图。

数组和字节切片也会钳制范围:[10, 20, 30][1:100] 包含 20, 30,而起始位置超过末尾的范围为空。若无效范围应触发 panic,请使用 exact_view;若应返回 None,请使用 get_view。

  • String → StringView:使用 s[:]、s[start:end]、s[start:] 或 s[:end]
  • Bytes → BytesView:使用 b[:] 或 b[start:end] 等
  • Array[T]、FixedArray[T]、ReadOnlyArray[T] → ArrayView[T]:使用 a[:] 或 a[start:end] 等

字符串边界:偏移量按 UTF-16 码元计数。根据对无效边界的处理方式选择操作:

  • s[a:b](clamped_view)会钳制超出范围的偏移量,并将边界向内调整,以避免拆分代理对。它不会抛出异常或触发 panic;反向范围会生成空视图。
  • s.exact_view(start=a, end=b) 要求边界精确且有效,否则触发 panic。
  • s.get_view(start=a, end=b) 在边界无效时返回 None。
///|
test "string view boundaries and ownership" {
  let s = "ab😀cd"
  inspect(s[0:3], content="ab")
  inspect(s[3:6], content="cd")
  inspect(s[0:100], content="ab😀cd")
  inspect(s[4:2], content="")
  assert_true(s.get_view(end=3) is None)
  let owned : String = s.exact_view(start=2, end=4).to_owned()
  inspect(owned, content="😀")
}

何时使用视图:

  • 使用剩余模式([first, .. rest])进行模式匹配
  • 将切片传递给函数,避免分配开销
  • 避免不必要地复制大型序列

使用 .to_owned() 可将 StringView、BytesView 或 ArrayView[T] 物化为 String、Bytes 或 Array[T]。旧的视图转换方法 .to_string()、.to_bytes() 和 .to_array() 已弃用。(moon ide doc StringView)

用户定义类型(enum、struct)

///|
enum Tree[T] {
  Leaf(T) // Unlike Rust, no comma here
  Node(left~ : Tree[T], T, right~ : Tree[T]) // enum can use labels
} derive(Debug, ToJson) // derive traits for Tree

///|
pub fn Tree::sum(tree : Tree[Int32]) -> Int32 {
  match tree {
    Leaf(x) => x
    // we don't need to write Tree::Leaf, when `tree` has a known type
    Node(left~, x, right~) => left.sum() + x + right.sum() // method invoked in dot notation
  }
}

///|
struct Point {
  x : Int32
  y : Int32
} derive(Debug, ToJson) // derive traits for Point

///|
pub fn Point::Point(x~ : Int32, y~ : Int32) -> Point {
  { x, y, }
}

///|
test "user defined types: enum and struct" {
  let tree = Node(left=Leaf(1), 2, right=Leaf(3))
  inspect(tree.sum(), content="6")
  json_inspect(Point(x=10, y=20), content={ "x": 10, "y": 20 })
  debug_inspect(
    Point(x=10, y=20),
    content=(
      #|{ x: 10, y: 20 }
    ),
  )
}

函数式 for 循环

///|
pub(all) enum SearchIndex {
  Found(Int32)
  InsertionPoint(Int32)
} derive(Debug, Eq)

///|
pub fn binary_search(arr : ArrayView[Int32], value : Int32) -> SearchIndex {
  let len = arr.length()
  // functional for loop:
  // initial state ; [predicate] ; [post-update] {
  // loop body with `continue` to update state
  //} nobreak { // exit block
  // }
  // predicate and post-update are optional
  for i = 0, j = len; i < j; {
    // post-update is omitted, we use `continue` to update state
    let h = i + (j - i) / 2
    if arr[h] < value {
      continue h + 1, j // functional update of loop state
    } else {
      continue i, h // functional update of loop state
    }
  } nobreak { // exit of for loop
    if i < len && arr[i] == value {
      Found(i)
    } else {
      InsertionPoint(i)
    }
  }
}

///|
test "functional for loop control flow" {
  let arr : Array[Int32] = [1, 3, 5, 7, 9]
  debug_inspect(binary_search(arr, 5), content="Found(2)") // Array to ArrayView implicit conversion when passing as arguments
  debug_inspect(binary_search(arr, 6), content="InsertionPoint(3)")
  // for iteration is supported too
  for i, v in arr {
    println("\{i}: \{v}") // `i` is index, `v` is value
  }
}

强烈建议 尽可能使用函数式 for 循环,而不是命令式循环,因为前者更易于阅读和推理。

标签参数和可选参数

良好示例:使用带标签的参数和可选参数

使用 label~ : T 表示必需的标签参数,使用 label~ : T = default 表示带默认值的标签参数,使用 label? : T 表示省略时以 T? 表示的参数。格式化工具会将旧式的带默认值写法 label? : T = default 改写为 label~ : T = default。

///|
fn g(
  positional : Int32,
  required~ : Int32,
  optional? : Int32, // no default => Option
  optional_with_default~ : Int32 = 42, // default => plain Int32
) -> String {
  // These are the inferred types inside the function body.
  let _ : Int32 = positional
  let _ : Int32 = required
  let _ : Int32? = optional
  let _ : Int32 = optional_with_default
  // `Repr` renders Option without relying on its deprecated `Show` implementation.
  "\{positional},\{required},\{Repr(optional)},\{optional_with_default}"
}

///|
test {
  inspect(g(1, required=2), content="1,2,None,42")
  inspect(g(1, required=2, optional=3), content="1,2,Some(3),42")
  inspect(g(1, required=4, optional_with_default=100), content="1,4,None,100")
}

错误用法:arg : Type? 不是可选参数。 调用方仍必须传入它(以 None/Some(...) 的形式)。

///|
fn with_config(a : Int32?, b : Int32?, c : Int32) -> String {
  "\{Repr(a)},\{Repr(b)},\{c}"
}

///|
test {
  inspect(with_config(None, None, 1), content="None,None,1")
  inspect(with_config(Some(5), Some(5), 1), content="Some(5),Some(5),1")
}

反模式:arg? : Type?(没有默认值 ⇒ 双重 Option)。 如果你想要带默认值的可选参数,请写 b~ : Int32 = 1,而不是 b~ : Int32? = Some(1)。

///|
fn f_misuse(a? : Int32?, b~ : Int32 = 1) -> Unit {
  let _ : Int32?? = a // rarely intended
  let _ : Int32 = b
}
// How to fix: declare `(a? : Int32, b~ : Int32 = 1)` directly.

///|
fn f_correct(a? : Int32, b~ : Int32 = 1) -> Unit {
  let _ : Int32? = a
  let _ : Int32 = b
}

///|
test {
  f_misuse(b=3)
  f_misuse(a=Some(5), b=2) // works but confusing
  f_correct(b=2)
  f_correct(a=5)
}

错误示例:arg : APIOptions(应改用带标签的可选参数)

///|
/// Do not use struct to group options.
struct APIOptions {
  width : Int32?
  height : Int32?
}

///|
fn not_idiomatic(opts : APIOptions, arg : Int32) -> Int32 {
  opts.width.unwrap_or(arg) * opts.height.unwrap_or(arg)
}

///|
test {
  // Hard to use in call site
  inspect(not_idiomatic({ width: Some(5), height: None, }, 10), content="50")
  inspect(not_idiomatic({ width: None, height: None, }, 10), content="100")
}

MoonBit 包组织指南

一个包应拥有用户预期会使用其构造器、字段、模式匹配和方法的公共具体类型。所有者可以是 facade 包本身,也可以是 facade 有意重新导出的非 internal 公共包。

公共类型的所有权比实现位置更重要。如果用户将某个类型视为 @foo.X,那么 X 应定义在 foo 包中,或定义在由 foo 重新导出的公共包中;尤其当用户会调用 X::method、构造记录、匹配枚举构造器,或依赖生成的 .mbti 文档时更是如此。

当从非 internal 公共包重新导出类型时,MoonBit 可以为方法查找隐式加载该类型的所有者包。例如,如果 @foo 从公共包 @bar 重新导出 type X,外部用户可以将值写作 @foo.X,同时仍能调用由 @bar.X 所有的方法。

使用 internal/* 包来提供实现支持:

  • 扫描器
  • 子语法解析器
  • 转义/编码辅助函数
  • 校验辅助函数
  • 底层算法
  • 私有辅助结果类型

不要将公共具体 API 类型放在 internal/* 中,并指望 facade 通过重新导出恢复完整 API。外部用户无法像对待 internal 包中的类型那样,隐式加载其方法所有者,因此即使 x 的类型是 facade 重新导出的类型,x.method() 也可能失败。这也会让构造器、生成的接口和隐私边界更难以推理。

正确使用 using

使用 pub using 提升 facade 的易用性,而不是用于确定类型所有权。

良好用法:

// root package

///|
pub using @parser {parse, parse_fragment}

///|
pub using @dom {type Node, type NodeKind, to_markdown}

///|
pub using @serializer {type HtmlContext}

当 @parser、@dom 和 @serializer 是已经拥有这些 API 的公共包时,这样做是合适的。

从 internal 包良好地重新导出值:

///|
pub using @impl {decode_entities}

如果导出函数的签名没有暴露 internal 类型,并且你有意将该值作为公共 API,那么这样做是可以接受的。

有风险的用法:

///|
pub using @internal_impl {type X}

避免对公共具体类型这样做。如果 X 是公共类型,请在 facade 包或非 internal 公共包中定义它。如果 X 确实是 internal 类型,就不要将其作为公共具体类型暴露。

在需要以下操作时,请使用显式包装器,而不是 pub using:

  • 将 internal 辅助结果转换为公共类型
  • 强制采用公共默认值
  • 隐藏 internal 辅助类型
  • 明确公共 API 的所有权
  • 让 .mbti 更易于审查

实用规则

如果公共函数返回 X,并且用户应当能够检查、构造、进行模式匹配或调用 X 的方法,那么 X 应属于用户所使用的 facade 包,或属于由 facade 重新导出的非 internal 公共包。

如果某个辅助包只为另一个包计算数据,它可以放在 internal/* 下,但其中的类型应保持 internal,或仅作为不通过公共 facade 暴露的简单辅助结果类型。

良好的包边界如下:

foo/
  types.mbt        // public Foo, FooMode, FooResult
  api.mbt          // public functions and Foo::methods
  private_impl.mbt // private implementation files in same package

internal/foo/
  scanner.mbt
  escaping.mbt
  validation.mbt

评论

支持 Markdown