AI 写了 7300 行 MoonBit:编译器能验的,和验不了的

定稿。所有数字都来自真实项目、可用文中命令复现。 项目仓库: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 写出来照样编译通过,只有测试能抓:

  1. 前缀规则的 add 必须前置,后缀才后置。 第一版把 PFX 0 re . 也写成追加,create 变成了 createre。
  2. 条件必须对「未 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/词

两处都不是算法错,而是纯函数被放在了循环里、白付了成本:

  1. apply_conversion 在每个输入字符 的循环里重新解析一遍转换规则、重新分配一个数组------而那个规则表在循环里根本不变。en_US 有一条 ICONV,于是每检查一个词都要分配"词长 × 规则数"个数组。最讽刺的是,这个文件自己的注释白纸黑字写着"per-word cost is a linear scan without re-parsing the patterns" ------设计意图在实现里从未落地。
  2. check_word_group 为了判断"这一行是不是多个词",先分配一个数组、一个字符串缓冲、再把整词复制一份,然后才发现只有一个词。而绝大多数输入就是一行一个词。

修完之后,2.31 倍回来了。

这件事的关键不在于"AI 写了慢代码"。关键在于:这三样东西全都发现不了它。

  • moon check --deny-warn → 0 error, 0 warning
  • moon 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 负责打字,编译器负责判定真假,而性能只能自己盯着。

具体是这几条:

  1. 一次只让 AI 写一个函数 + 它的测试,不一次生成上百行
  2. 写完立刻 moon check;报错就把原始报错文本贴回去
  3. moon test 通过才算这块完成,否则不进下一块
  4. AI 说"已修复"时,一律自己重跑 moon check
  5. 语法不确定时查官方文档或 core 源码,不接受 AI 的语法
  6. 改一个没有测试的函数之前,先给它补测试。 我这轮改的 apply_conversion 实现了 ICONV/OCONV 却一行单元测试都没有 ;补了 10 个之后,我还故意把快速路径改坏,确认其中 5 个会失败------测试本身也要被测试
  7. 收口时重测,不沿用文档里的数字------因为那 0.34 µs 就是这么变成 0.75 的

这套流程的关键在于:MoonBit 的工具链反馈非常精确 。错误码、行号、moon explain 的解释都很到位------所以"AI 生成 → 编译器判定 → 修正"这个循环跑得很快。反馈质量决定 AI 能不能用。


结果

  • 176 个测试(native 180),wasm / wasm-gc / js / native 四个后端全部通过 ;moon check --deny-warn 0 warning
  • Hunspell 官方语料(154 个测试套件、848 个必须接受的判定): .good 841/848 = 99.2% | .wrong 611/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...

相关推荐
正在走向自律1 小时前
从只写接口到独立交付前后端,飞算JavaAI能给Java后端工程师多少溢价?
java·springboot·ai编程·全栈开发·java求职·飞算javaai·金九银十
全栈弄潮儿2 小时前
3 个能立刻复用的 AI 编程工作流
aigc·openai·ai编程
桃西西呀6 小时前
Laya 源码级原理拆解之二:序列打包与决策头
人工智能·llm·ai编程
盟接之桥7 小时前
线束数字化--先进先出为什么总是停留在纸面上?
大数据·网络·数据库·人工智能·制造·ai编程
Yunovian7 小时前
AI时代,针对模型与应用,浅谈一下各编程语言
开发语言·c++·人工智能·python·ai·rust·ai编程
用户4015823324628 小时前
Git 合并冲突怎么解决?用 AI 处理冲突的流程、Prompt 和 4 个易错点
ai编程
小虎AI生活8 小时前
官媒集体下场做 GEO:AI 眼里的“你”,得自己管(附 10 分钟自检实操)
aigc·ai编程
桃西西呀9 小时前
Laya 源码级原理拆解之一:整体架构与运行入口
人工智能·llm·ai编程
吴佳浩9 小时前
Agent 安全红线:越狱防御、间接注入与数据防泄漏实战
人工智能·agent·ai编程