定稿。所有数字都来自真实项目、可用文中命令复现。 项目仓库:github.com/Careylq/spe... | 完整坑清单:仓库内
docs/MOONBIT_GOTCHAS.md(43 条)
一个具体的瞬间
我最开始让 AI 写 MoonBit 时,它给了我这样一段代码:
arduino
import { "moonbitlang/core/string" }
看起来毫无问题------但 MoonBit 里没有这个语法。那次我的编译输出是 24 个错误,全部源于同一处写法。
这不是模型的错。这是"用 AI 写一个小众语言"的默认状态。
这不是感觉,是有数据的
IEEE TSE 录用的论文(arXiv 2606.16827)把 MoonBit 归类为 "no-resource language" ------LLM 几乎没有见过这门语言的训练数据。实测 McEval-Hard 零样本 pass@1:
| 语言类型 | 零样本 pass@1 |
|---|---|
| 高资源语言 | 59--89% |
| 低资源语言 | 27--84% |
| MoonBit / Gleam | 0--1% |
论文还提到,即使用 1,370 万 token 的 MoonBit 语料继续预训练,也只能提到约 15%。
MoonBit 官方自己的博客也承认过这件事。他们发布 Pilot(官方代码智能体)时写:
"由于缺乏 MoonBit 专用训练数据,模型第一次并没有产出正确输出;通过自动调用工具链拿到精确反馈,它才修复并优化了代码。"
换句话说:在 MoonBit 里,AI 能不能用,取决于你有没有把编译器接进回路。
43 个坑,分五类
我在一个真实项目里把这件事验证了一遍:用 MoonBit 从零实现一个 Hunspell 兼容的拼写检查库 (.aff/.dic 格式解析 + 词缀形态学 + 拼写判定 + 建议生成)。全程 AI 辅助,最终产出 7,350 行实现 + 3,323 行测试。
过程中撞上了 43 个坑,全部由编译器、warning 或实际运行验证过。挑最典型的:
A. AI 最常写错、编译器直接拒绝的
| 坑 | 真相 | 代价 |
|---|---|---|
fn f(self : T, ...) 当自由函数写 |
已废弃(0027),编译器把它注册成 T::f,同文件里调用 f(...) 就报"未绑定值" |
一处写法 → 24 个报错 |
unused_mut 是 error,不是 warning |
往 Array 里 push 不需要 字段是 mut |
一个"顺手加的 mut"直接编译失败 |
& 的优先级低于 == |
必须写 (b & 0xC0) == 0x80 |
逻辑悄悄错 |
pub struct 的私有字段类型也必须是 pub |
否则跨包报 4046 | 加法式改动引发结构性报错 |
第一条特别典型:AI 按 Rust/Python 的直觉写 self,而 MoonBit 有自己的一套规则。它写得"很合理",但编译器不接受。
B. 已废弃的写法(AI 训练数据里的旧语法)
inspect → @debug.debug_inspect · not(x) → !x · StringView::to_string() → to_owned() · x.is_none() 已废弃 · substring 已废弃 → 用切片 · #| 多行字符串不能直接当调用参数
这类坑的特征是:AI 用的语法在某个旧版本里是对的。 它的知识过期了,而它自己不知道。
C. MoonBit 与主流语言不一样的地方(会静默出错)
最值得记的一条:
String是 UTF-16 code unit 序列。length()数的是 code unit ,char_length()才是码点 ;get_char(i)按 code unit 索引(切在代理对中间返回None); 只有to_array()按码点给出Char。做 Unicode 正确的字符匹配必须用
to_array()/char_length()------ 否则遇到 emoji 或非 BMP 汉字就会错。
这一类不会报错,只会算错。所以我们给每个这类函数都配了测试。
D. 工程配置类(容易卡住半天)
moon.pkg的测试包要单独 写import { ... } for "test"/for "wbtest"- blackbox 测试里要写
@包名.前缀 moon fmt会改写moon.mod(插空行),可能弄脏你不想动的文件- 用
moon explain --diagnostic <码>查清哪些 warning 实际是 error (例:unused_mut= error id 15)
E. 编译器根本不管的语义 bug
这两条不是语法错误------AI 写出来照样编译通过,只有测试能抓:
- 前缀规则的
add必须前置,后缀才后置。 第一版把PFX 0 re .也写成追加,create变成了createre。 - 条件必须对「未 strip 的 stem」匹配。
SFX y ied [^aeiou]y若先 strip 掉y,剩下的impl永远不可能 匹配以y结尾的模式。
编译器保证语法和类型,测试才保证语义。
但还有第三层:编译器和测试都验不了的
上面 A--E 五类,都有一个共同的解药------跑一遍 。语法错跑 moon check,语义错跑 moon test。
真正让我改观的是最后一件事:有一大类 bug,moon check 干净、测试全绿、符合率一字不差,但项目已经偷偷慢了 2.3 倍。
收口时我顺手把 README 里记的性能数字重测了一遍------结果对不上:文档写的是直接命中 0.34 µs/词 ,实测是 0.75 µs/词。
排查的第一反应是怀疑算法。我读了一遍判定路径,排除了最可疑的那个循环(|| 短路,直接命中时根本不会执行到)。然后用了最笨也最可靠的办法:同一台机器、同一条命令、对真实 en_US 词典自带的 49,568 个词计时,再用 git stash 回退到改动前,A/B 对照:
| 版本 | 直接命中 | 未命中 |
|---|---|---|
| 改动前 | 0.746 µs/词 | 9.81 µs/词 |
| 只修第一处 | 0.403 µs/词 | 9.54 µs/词 |
| 再修第二处 | 0.323 µs/词 | 9.28 µs/词 |
两处都不是算法错,而是纯函数被放在了循环里、白付了成本:
apply_conversion在每个输入字符 的循环里重新解析一遍转换规则、重新分配一个数组------而那个规则表在循环里根本不变。en_US 有一条ICONV,于是每检查一个词都要分配"词长 × 规则数"个数组。最讽刺的是,这个文件自己的注释白纸黑字写着"per-word cost is a linear scan without re-parsing the patterns" ------设计意图在实现里从未落地。check_word_group为了判断"这一行是不是多个词",先分配一个数组、一个字符串缓冲、再把整词复制一份,然后才发现只有一个词。而绝大多数输入就是一行一个词。
修完之后,2.31 倍回来了。
这件事的关键不在于"AI 写了慢代码"。关键在于:这三样东西全都发现不了它。
moon check --deny-warn→ 0 error, 0 warningmoon test --target all→ 176 个测试全绿- Hunspell 官方语料符合率 → 一个数都没变
因为它们全都在验"对不对",没有一个在验"贵不贵"。
还有一件事:度量本身也会骗你
同一轮收口里,我还发现自己引以为据很久的一个数字是错的。
符合率套件里有个 .good 文件(记录"必须被接受"的词),其中 16 行本身含空格 (drink eat 这种)。而我的测试脚本用 paste 把"输入流"和"判定流"按位置 粘起来配对------含空格的行一错位,后面全歪,于是这 16 行无论引擎答对答错都被记成失败。
改成"每个非空输入行恰好一个判定、直接计数"之后:
| 口径 | .good 通过率 |
|---|---|
| 按位置配对(我用了很久的) | 825/848 = 97.3% |
| 按判定计数(正确口径) | 841/848 = 99.2% |
差值 16,正好是那 16 行。引擎一个字都没改。
也就是说:我一直在用低估了 1.9 个百分点的数字向别人汇报自己的项目,而且每次"重跑确认未回归"都在重复同一个错误。
这两件事合起来是我这轮最大的收获:
"跑出来的数字"比"想出来的结论"可靠------但"跑的方法"本身也需要被怀疑。
所以我改成了什么工作流
一句话:AI 负责打字,编译器负责判定真假,而性能只能自己盯着。
具体是这几条:
- 一次只让 AI 写一个函数 + 它的测试,不一次生成上百行
- 写完立刻
moon check;报错就把原始报错文本贴回去 moon test通过才算这块完成,否则不进下一块- AI 说"已修复"时,一律自己重跑
moon check - 语法不确定时查官方文档或 core 源码,不接受 AI 的语法
- 改一个没有测试的函数之前,先给它补测试。 我这轮改的
apply_conversion实现了ICONV/OCONV却一行单元测试都没有 ;补了 10 个之后,我还故意把快速路径改坏,确认其中 5 个会失败------测试本身也要被测试 - 收口时重测,不沿用文档里的数字------因为那 0.34 µs 就是这么变成 0.75 的
这套流程的关键在于:MoonBit 的工具链反馈非常精确 。错误码、行号、moon explain 的解释都很到位------所以"AI 生成 → 编译器判定 → 修正"这个循环跑得很快。反馈质量决定 AI 能不能用。
结果
- 176 个测试(native 180),wasm / wasm-gc / js / native 四个后端全部通过 ;
moon check --deny-warn0 warning - Hunspell 官方语料(154 个测试套件、848 个必须接受的判定):
.good841/848 = 99.2% |.wrong611/613 = 99.7% | 建议质量 141/173 = 81.5% - 在真实的 LibreOffice
en_US词典(49,568 词条 )上:49,565 个按预期接受, 被拒的 3 个带ONLYINCOMPOUND、独立出现本就该拒 - 与 C++ Hunspell 1.7.3 交叉验证 :这 49,568 个词条上两个实现判定完全一致 ; 另在 235,976 个真实词(
/usr/share/dict/words)上做差分测试,分歧 199 个 = 0.084% - 性能(原生二进制对原生二进制,同词典同词表):直接命中 0.38 µs/词 vs Hunspell 0.36 µs/词(持平) ; 未命中路径慢 6.44 倍,差距精确定位在那一处
- wasm 产物 142.2 KiB,无 C++ 运行时
我的判断
低资源语言用 AI,不是"能不能用"的问题,而是"工作流必须怎么设计"的问题。
在 Python 或 TypeScript 里,你可以让 AI 写一大段然后跑起来看看。在 MoonBit 里,你必须假设它写的每一行都可能是错的------然后让编译器告诉你哪一行是错的。
但更重要的是下一句:编译器只能告诉你"哪一行是错的",它不会告诉你"哪一行是对的但很贵",也不会告诉你"你的测试在测一件错的事"。
所以完整的答案不是"把编译器接进回路",而是: 把编译器接进回路,再把度量接进回路,最后把"怀疑自己的度量"也接进回路。
一旦接受这个前提,剩下的就是工程问题:把反馈回路做短,把每一步都变成可验证的。
那 43 个坑我全部记在了项目的 docs/MOONBIT_GOTCHAS.md 里。如果你也在用 AI 写 MoonBit,那份清单可以直接拿去避坑。
项目地址:github.com/Careylq/spe... | 包:mooncakes.io/docs/Careyl...