状态:设计稿(方案 A 已定——内部事实 = 原生 MoonBit 包;对外出口 = 生成 JSON;
json.mbt既有约定不动)。 修订(2026-10-10 审阅批):吸收病 11 收编现状(§3.5 重写)、sig()改全覆盖补专臂签名对账边(§3.2/§3.5)、宏真源改「预定义/头文件」两分口径(§3.1/§3.6)、rules.json.mbt计数勘正 13→12(§1.4)、多文件注入路径已通缺口收窄(§4.4)、遮蔽清单已有显式登记(§3.2/§6.3)。 性质:引擎内部结构设计,不改 C 语义(全域纪律 #4「以 Clang 为标准」不变)。 前置证据:07-质量与裁定/kimicc外部参考调查报告20260926.md§3/§4.3、01-定位与路线/MoonBit迁移总计划.md§10.5 G-6、07-质量与裁定/20261007_jsonmbt真相源迁移.md。 入库纪律:本文已同步docs/README.md索引;改动本文数字须跑go run ./scripts/facts --strict check。
1. 背景与问题(实测)
1.1 现状:同一件事有 10 个声明点
| # | 落点 | 装什么 | 消费方 |
|---|---|---|---|
| 1 | scripts/moonbit/libc_src/include/*.h(14 个) |
C 声明(surface) | → stubs_gen.mbt → 预处理 |
| 2 | scripts/moonbit/libc_src/src/*.c(3 个) |
bytecode 实现 | 预编译 |
| 3 | moonbit/libc/libc.mbt |
LibcSig 表 + builtin_all 放行集 |
typeck sig_of |
| 4 | moonbit/libc/bytecode_sig.mbt |
名 → 类型 | typeck check_user_func 第四级 |
| 5 | moonbit/bytecode/host_func_id_gen.mbt |
host id 常量 + 名→id + PURE | codegen 发射 |
| 6 | moonbit/bytecode/libc_index.mbt |
固定索引名单 | codegen 预注册 |
| 7 | moonbit/bytecode/route.mbt |
改判集 + 遮蔽清单 | codegen |
| 8 | moonbit/typeck/builtin.mbt |
check_builtin_* 26 方法 = 表驱动总入口 check_builtin_table + 25 具体专臂(病 11 第一层已收编 15 个纯骨架臂走 @libc.sig_of 表驱动、memset 死臂已删;libc 表 57 条 param_kinds 已填值,第二层逐臂收编进行中) |
typeck 分派 |
| 9 | moonbit/lexer/internal/pp/builtins.mbt |
宏表 | 预处理 |
| 10 | moonbit/host/*.mbt |
handler 实现 | VM 分发 |
另有预编译产物 moonbit/vm/libc_data_gen.mbt(由 #2 预编译而来)。
1.2 三条钉死的病灶
- 现有闸只查"名字集合",不查签名。
scripts/moonbit/libc_single_source/main.go判据是builtin_all == (host ∪ bytecode) − excluded;全文 grepsignature|签名|return_type|param零命中。⇒ N3/N4 那类签名漂移(printf返回void→int、strcpy返回char*vsvoid)在当前闸下结构上不可见,当时靠人读 C 标准抓出。注入实证(2026-10-10 审阅):把bytecode_sig.mbt的strcpy返回类型改成v_ty()后该闸仍 PASS。7780284d 已补param_kinds字符集静态锚(P/I/D,表加载层即红)——但这只锁表内自洽,跨表/专臂的签名一致性仍无闸。 - host 路由的真源是一份冻结的 Rust 快照。
scripts/moonbit/gen_host_route/host_func_id.snapshot.rs是host_func_id_gen.mbt的输入——native/已于 2026-10-05 删除,这张表仍在消费 Rust 冻结区的副本。 - "单源"想做但做不成。
moonbit/libc/libc.mbt头注原话:「计划原意是让下游两表从本表派生……但 libc 在 L5、codegen/bytecode 在 L6——后者可 import 前者、反之不可,故本表无法派生下游两表。单源只能实现为对账」。病 11 两层收编已大面积推进(第一层 15 臂表驱动 + memset 死臂删除、param_kinds已填 57 条,2026-10-09/10 落),但收编改变的是 typeck 臂 ↔ 表 的关系——表仍在 L5,派生 L6 两表仍不可行,死结本身未动。
1.3 生态对照(实测)
| 参照 | 做法 | 对本设计的含义 |
|---|---|---|
kimicc(moonbitlang/kimicc,bobzhang) |
ctype = 零依赖包(≈3500 行),builtin 事实用成对纯函数 _info(name,arg_count)->(symbol,forwarded)? + _arg_type(symbol,index)->Type(cfront/ctype/builtin.mbt:796-1576),三端共享同一包 |
正面模板:事实下沉到包,靠 import 单源 |
moonbitlang/core |
40+ 个小包,零代码生成器;公开面由编译器生成 .mbti |
生态常态:不建生成器/manifest,包边界即单源 |
| chibicc | 手写最小头 include/,编译器内建另算,不追求单源 |
说明"头 + 内建"两分是常态,但 chibicc 没有"多表须一致"的负担 |
| LLVM TableGen | 一份 machine description → 多后端生成多表(clang 亦用于 diagnostics/attributes) | 不同场景:适用于"一源→多语言/多产物",本设计内部场景不需要 |
1.4 结论
唯一异常是 libc 面。 scripts/**/rules.json.mbt(12 张,2026-10-10 实测;icon_catalog 用的是 rules.json JSON 形态,不在此面)与 golden_digest.json.mbt(5 张)的消费者是 Go 闸 或机器写回流程,属跨语言/外部,.json.mbt 载体正确;diagnostics_data/ledger/known 属词汇表,同样正确。只有 libc 面——消费者全在引擎内部(MoonBit)——却手抄成 10 处。这才是"加一个函数要动好几处"的根因。
根因旁注(生态视角):这套"生成器 + 规则 JSON + 对账闸"机器的原始职责是证明与 Rust oracle 逐字节等价。脱钩完成后,该职责对"真值锚已改为 C 标准/头文件存根"的面(见
libc.mbt批四注记)已经消失——留下的主要是惯性。
2. 决策(方案 A)
2.1 判据:消费者是谁
| 消费者 | 载体 | 理由 |
|---|---|---|
| 引擎内部(MoonBit 代码) | 原生包(纯函数 + 类型化数据) | 直接 import,零序列化、零漂移 |
| 引擎外部(其他语言 / Go 闸 / 进程) | .json.mbt 真源 + .json 产物 |
跨语言必须有线格式 |
2.2 三条边界(本次决策的完整口径)
- 内部事实 → 原生 MoonBit 包。不引入 JSON / manifest / CSV。
- 对外出口 → 从原生真源生成 JSON,不手写。本仓已有范式:
gateway/serve_static.mbt的memory_model直接由@bytecode.MEM_SIZE等原生常量导出。 json.mbt既有约定不动:12 张rules.json.mbt+ 5 张golden_digest.json.mbt+ 词汇面全部保留,jsonmbt build(真源→json)与jsonmbt import --fill(json→真源)双向机制不变。
3. 头文件设计
3.1 三层真相分离
每个标准库实体背三个可分离的事实,现在被糊在 10 处:
| 事实 | 定义 | 目标归属 |
|---|---|---|
| surface | 程序员看见的名字 / C 签名 / 宏 / typedef | 函数面 = .h 文件(原生 C,唯一真源);对象宏 = 两分:预定义宏(INT_MAX/EOF/RAND_MAX/stdout 等 23 个,真源 pp/builtins.mbt,limits.h 等存根是空壳占位)+ 头文件宏(DBL_/FLT_/errno 值等,真源 .h 由 pp 解析);两侧宏名零交集(2026-10-10 对拍实测) |
| binding | 实现在哪:Bytecode | Host | HostRerouted | Special |
事实包 stdlib(原生 MoonBit) |
| behavior | 实现本身 | libc_src/src/*.c(bytecode)/ host/*.mbt(host)/ 专臂(special) |
3.2 事实包接口草案(kimicc ctype 式)
新包 moonbit/stdlib(module 全名 vitro/engine/stdlib),零依赖(只依赖 vitro/engine/ast):
///| 头文件清单(数据驱动,取代 gen_stubs 的硬编码 stubNames)
pub fn headers() -> Array[String]
///| 名字 → 事实条目(None = 不在标准库面)
pub fn lookup(name : String) -> Entry?
///| 签名(返回类型 + 参数位骨架)——**全覆盖**:专臂也返回**期望签名**
///|(期望值源自 .h 机械提取,即 surface 真源);专臂实现的实际返回
///| 类型由闸与它对拍——surface ↔ Special 的对账边由此闭合,
///| N3(printf 返回 void→int)一类专臂签名漂移不再无闸。
pub fn sig(name : String) -> Sig?
///| 绑定层(typeck/codegen 路由判据的唯一来源)。
///| 语义 = **实际执行路径**;两表交集中被固定索引段遮蔽、按名不可达的
///| host handler 死面不由本枚举表达——沿用 route.mbt 既有显式清单
///| `shadowed_host_names()`(死面可断言),stdlib 只登记绑定不重复登记遮蔽。
pub fn binding(name : String) -> Binding
///| 对象宏定义(名 → 文本/类型)——只承载**预定义宏**(现 builtins.mbt
///| 23 个);头文件宏(DBL_* 等)留在 .h 单源,不进本表(§3.1 两分)。
pub fn macro_def(name : String) -> MacroDef?
pub enum Binding {
Bytecode // 走 libc 字节码索引
Host // 走 host 路由 id
HostRerouted // 名义 bytecode、实际改判 host(strcpy 族 5 个)
Special // typeck 专臂,不入数据表
}
pub struct Entry { name : String; header : String; binding : Binding }
3.3 为什么是纯函数而不是静态表(kimicc 报告原文理由)
查表键是二维
(name, arg_count)、结果三态(匹配/参数数不符/未知)、还必须配_arg_type给每个转发参数定型——塞不进静态表。
本仓同理:printf 的参数形状取决于格式串内容(编译期解析),但变参个数上限无限、还要给出隐式转换期望——这是一段函数,不是一行数据。
3.4 生成关系(谁生成谁)
| 来源 | 产物 | 方式 |
|---|---|---|
.h 文件 |
stubs_gen.mbt |
既有 gen_stubs(纯机械,保留) |
.h + 事实包 |
各层无需生成 | libc/typeck/codegen/host/vm 直接 import |
| 事实包 | capabilities 帧的 libraries 段 |
新增,见 §5 |
libc_src/src/*.c |
libc_data_gen.mbt |
既有 precompile_bytecode_libc(保留) |
关键:事实包落 L3 后,L5/L6/L7 的消费方都能 import 它(pkg_deps 判据 level(D) ≤ level(P) 天然成立)——死结解开,且不需要生成器。
3.5 专臂与 Special:与病 11 收编的关系(2026-10-10 修订重写)
现状(病 11 两层收编后):check_builtin_* 26 方法 = 表驱动总入口 check_builtin_table + 25 具体臂;15 个纯骨架臂已走 @libc.sig_of 表驱动、memset 死臂已删、libc 表 57 条 param_kinds 已填值;变参/格式串/FILE*/指针语义族仍走专臂、随逐臂收编补列。病 11 的表(libc L5 的 LibcSig)与本方案的 stdlib 是同一事实的两代载体:stdlib 落地 = 吸收并取代 libc 表,病 11 接线是过渡态——P2 接线 libc 面时刚以 typeck_diff 385 hash 验收过的表驱动链路会被再重排一次,该面验收线 = 重排前后 typeck_diff hash 等价(与病 11 验收同口径)。
决策点(病 11 第二层是否停手):第二层「逐臂收编补 param_kinds」与本方案 P2 同面同物。继续收编 = 同一面两次重排(先填 libc 表、再迁 stdlib);建议第二层剩余臂停手,等 stdlib 落地后一步到位(§8 时机段的补充裁定项)。
事实包内对应条目 binding = Special:
- 生成/派生工具跳过它们;
- 闸门要求它们存在(
Special条目 ↔typeck/builtin.mbt内同名臂,双向对账); - 签名也入对账:
sig()对专臂返回期望签名(源自 .h),闸比「专臂实现实际返回类型 ↔ 期望签名」——存在性 + 签名双维度(§3.2)。
把"不可数据化"变成显式一行,而不是假装能派生;签名期望则全部数据化,不留盲区。
3.6 加一个头文件:前后对比
| 步骤 | 现在 | 之后 |
|---|---|---|
| 1 | 加 X.h |
加 X.h |
| 2 | 改 gen_stubs.go 硬编码 stubNames |
(无——headers() 数据驱动) |
| 3 | 跑 gen_stubs |
跑 gen_stubs |
| 4 | 手抄 libc.mbt 表 + bytecode_sig.mbt |
(无——事实包加 1 条) |
| 5 | 手抄 host_func_id_gen.mbt / libc_index.mbt |
(无——由 binding 派生) |
| 6 | 手抄 builtins.mbt 宏表 |
(无——预定义宏由 macro_def 派生;头文件宏留 .h 单源,两分边界见 §3.1) |
| 7 | 同步文档 + 各闸 | 同步文档 + 各闸 |
| 8 | — | 新增:跑 capabilities 出口对比(下游可见)+ headers 三方一致(目录扫描 ↔ stubs_gen 产物名集 ↔ headers()——Go 侧不能 import MoonBit 包,扫目录与 headers() 是两份独立枚举,必须有对账闸;对账媒介 = capabilities.libraries JSON 出口,见 §5.1) |
净变化:手抄 4~5 处 → 事实包 1 条 + 闸基线连坐(宏值/基线刷新是机械动作但非零,别按「纯 1 条」排期)。
3.7 include 解析与 token 级缓存(速度)
- 表面仍是标准 C
#include(Clang parity 不可动)。 <>判定改为查stdlib.headers()(数据驱动,取代硬编码路径判断)。- token 级 PCH 缓存:存根内容不可变 ⇒ 首次 include 时记住 token 序列,后续复用;存根内的
#define副作用每次重放(includer 宏表状态不同)。- 安全前提(实测成立):14 个存根无
#if/#include;内容形态两极——部分近乎空壳(limits.h全文一行注释,宏已预定义于 lexer),其余为#define/typedef/声明。 - 守卫:生成/闸门断言"存根出现条件指令则禁缓存",fail loud。
- 收益预判(2026-10-10 审阅):存根总量小、词法成本微秒级、教学场景单 TU——收益大概率不成立,P4 维持「先测量后决定」,倾向不做。
- 安全前提(实测成立):14 个存根无
4. 多文件编译与编辑设计
4.1 现状模型(保留)
moonbit/session/session.mbt:CompileUnit{filename, source}数组 +FileRange{filename, start_line, end_line}。moonbit/gateway/serve_compile.mbt:merge_units顺序拼接(无尾换行补\n,空文件补)+ 产file_ranges(全局行号 → 文件)。- 诊断定位靠
resolve_filename(line, ranges)反查。
4.2 统一 SourceGraph(新)
现状 #include 解析(lexer/internal/pp/*)与多文件合并(gateway/serve_compile.mbt)是两套独立实现,但本质同一件事——文本图上的拼接 + once + 环检测 + 行号映射。设计统一为一个 SourceGraph:
SourceGraph {
roots : Array[String] // 编译单元根(多文件)
nodes : Map[String, Node] // 文件节点(内容 + 来源类型)
edges : Array[(String,String)] // include 边(含 <stub> 与 "path" 两类)
ranges : Array[FileRange] // 全局行号 → 节点
}
- once / 环检测 / 行号映射 单点实现(现在两套各自维护,判据容易分叉)。
- include 的
<stub>边指向事实包的headers()集合;"path"边走 quote 候选链。 - 注记(2026-10-10 审阅):两套实现的不变量其实不同——
merge_units是 TU 级拼接(无 once 语义),include 展开是预处理级(once/宏重放);统一的真实收益 = 行号映射 + 环检测单点,成本 = 行为等价证明。收益/风险比中等偏下,P4 维持「先测量」,倾向可砍——多文件编辑的关键路径不在它(见 §4.4)。
4.3 语义边界(写进 C语言子集规范.md)
- 多文件 = 拼接为单一翻译单元(unity build 语义),保留现状。
- 明确不做:分离编译 + 链接(无链接器、无多 TU 重名/
static跨文件规则)。理由:与教学定位不符,且是另一个量级工程。
4.4 编辑面(多文件 UI 需要什么)
多文件编辑(demo / 未来 IDE)在引擎侧需要的最小支撑:
| 能力 | 引擎侧依据 | 现状 |
|---|---|---|
| 文件列表 + 内容注入 | CompileUnit[](协议 compile 帧 params.files) |
✅ 已有 |
| 逐文件诊断定位 | file_ranges + resolve_filename |
✅ 已有 |
| 跨文件符号可见性 | 拼接单 TU ⇒ 天然可见 | ✅(语义副作用) |
| 文件级增量重编 | 无 | ❌ 未做(见 §4.6) |
| 默认宿主读多文件 | VfsProvider(内存文件表) |
✅ 注入路径已通(2026-10-05 files-vfs 批:serve_compile units 全量注入内存 vfs、集合内 #include "..." 互引可解析;demo 侧 js/workspace.ts 经 params.files 消费,真机 FSA 四条验收过) |
剩余缺口(2026-10-10 收窄):注入集合之外的 quote-include 在默认 StubsProvider 下报 UnsupportedHost(lexer/internal/host/provider.mbt,错误文案已明示「需注入 VfsProvider」)——这是协议文档化 + 前端 UX 工作(错误提示引导、文件树越界警告),不是引擎工程。引擎侧多文件支撑面已齐。
4.5 速度
- 拼接后单次词法化,O(total)——已线性。
- 可选:分单元词法化再合并 token 流(省重复扫描)。等价性需先证(行号映射、宏状态跨单元边界)——列 P4,先测量再决定。
5. 对外出口(方案 A 的外部边界)
5.1 capabilities 帧新增 libraries 段
现状 serve_capabilities_frame(gateway/serve_static.mbt)已导出 schema / behavior_contracts / languages / memory_model,但没有标准库函数面。下游语言(CSharp前端引入计划)正需要它。
新增(由事实包生成,不手写):
"libraries": {
"headers": ["stdio.h", "stdlib.h", ...],
"functions": [
{ "name": "printf", "header": "stdio.h", "binding": "Host" },
...
],
"macros": [ { "name": "INT_MAX", "header": "limits.h" }, ... ]
}
载体注记(2026-10-10 审阅):全量函数面 100+ 条若进 capabilities 握手帧会每次携带、帧体膨胀——建议独立帧(libraries.get 惰性拉取)或 capabilities 只带摘要计数。双重用途:该出口同时是 Go 侧的对账媒介——headers 名单经它供给 Go 闸(§3.6 步骤 8 的三方一致),一处出口两处消费。
5.2 这解决了什么
- 下游语言可机器读取"引擎支持哪些函数、什么签名、在哪个头"——无需读源码。
- 同时也是该段的正确性证明:出口由真源生成 ⇒ 出口不会与引擎行为漂移。
6. 分层与门禁连坐
6.1 新包分层
moonbit/stdlib 落 L3(与 names「名字单源」并列;L3 import L2 ast 合法)。须在 scripts/moonbit/pkg_deps/rules.json 的 levels 登记(未登记即红,fail loud)。
6.2 连坐清单
| 门禁 | 变化 |
|---|---|
pkg_deps |
新增 stdlib 层登记(否则红) |
libc_single_source |
判据重写:从"三名单对账"改为"事实包 ↔ 各消费面 逐名 + 逐签名对账"——签名维度含专臂(专臂实际返回类型 ↔ sig() 期望签名,期望源自 .h;7780284d 的 param_kinds charset 锚并入门加载层) |
moonbit_surface |
新包出口符号登记(surface_allowlist.txt + surface_edges.txt) |
mbti_sync |
新包 .mbti 纳入 |
gen_stubs |
stubNames 硬编码改为扫目录/读 headers() |
gen_host_route |
输入从 Rust 快照改为事实包(清掉 host_func_id.snapshot.rs 依赖) |
single_source |
libc 相关条目重指向 |
| headers 三方一致(新) | 目录扫描 ↔ stubs_gen 产物名集 ↔ headers(),对账媒介 = capabilities.libraries JSON 出口(§5.1)——否则「消灭 10 处对账、引入第 11 处」 |
facts |
若本文/规范文档引入新数字须连坐 |
6.3 可退役项
libc_single_source的"名字集对账"角色 → 被"事实包单源"取代(可降级为签名对拍闸)。host_func_id.snapshot.rs(Rust 快照)→ 删除。- 已有基础(无需新建):遮蔽 handler 死面清单
shadowed_host_names()(bytecode/route.mbt)已把"20 个写而不可达"显式化为可断言清单——stdlib 的Binding语义直接沿用,不重复登记(§3.2)。
7. 迁移分期(红→绿,全程门禁)
| 阶段 | 内容 | 风险 | 验收 |
|---|---|---|---|
| P0 | 只读盘点:10 个点逐条抽出「事实 → 现居 → 新归 → 照搬物/自有事实」,产出对照表;必答三题:① 病 11 已收编臂的过渡路径;② 遮蔽清单 20 名的 handler 处置(保留教学展示 / 删);③ 宏两分边界清点 | 零 | 对照表经人工确认(已产出:P0 对照表 20261010——遮蔽名实测 17 非 20,见其 §2②) |
| P1 | 建 stdlib 包(从现有表反向提取)+ 接线一个消费面(stubs_gen / headers()),与现状逐字节 diff 必须一致 |
低 | 该面产物字节相同 + 差分门禁绿 |
| P2 | 逐面接线:libc(吸收病 11 表,验收 = 重排前后 typeck_diff hash 等价)→ bytecode_sig → host 路由(顺带删 Rust 快照)→ libc_index → builtins 宏 |
中 | 每面 libc_single_source + 对应 -check + 差分红→绿 |
| P3 | capabilities.libraries 出口 + pkg_deps 层登记 + surface 白名单 |
中 | 出口结构对拍 + 四组闸绿 |
| P4 | 多文件收尾:quote-include 越界协议文档化(引擎侧已通,见 §4.4);SourceGraph / token 缓存 / 分单元词法化 = 可选优化,先测量、倾向砍 |
中 | 性能对照 + 行为等价 |
8. 风险与「不做」
- 时机:项目有明文「脆弱期不做重构」(
S9脱钩与裁定批判定书.md§5.2);当前 S9 未收(剩 #10/#20 + #47 + 0.9.0 发版)。P2 起同时动libc/host/codegen/lexer四包,与并行会话撞车概率高(本仓多会话共写为常态)。建议 P0 立刻可做(零风险),P1 起排 0.9.0 之后。 - 病 11 第二层停手(2026-10-10 补充裁定项):病 11 第二层「逐臂收编补
param_kinds」与本方案 P2 同面同物(§3.5)——继续收编 = 同一面两次重排。建议剩余臂停手,等 stdlib 落地一步到位;已收编的 15 臂 + 57 条 kinds 零作废(stdlib 从 libc 表反向提取即吸收)。 - 照搬纪律约束(#4):
typeck/builtin.mbt与host/host_format.mbt两份格式串扫描器均为照搬 Rust(各文件头注自述),真值锚未改到 C 标准。⇒ 本次不动它们(总计划 §10.5 G-6 已裁「暂缓」;其依据句提到的format_spec_kinds在当前树中已不存在——2026-10-10 复核仍零命中,需另行勘正)。注意区分:G-6 暂缓的是「照搬物(格式串扫描器)统一」;本方案动的是「自有事实(名字/签名/路由表)单源化」——G-6 自己写明 kimicc 模板适用于自有事实,两者正交不冲突。 - 明确不做:分离编译 + 链接(§4.3);AST 级 include 缓存(宏泄漏语义风险);改造既有
json.mbt体系(§2.2 边界 3)。
9. 开放问题(待定)
- P0 对照表 中"照搬物 vs 自有事实"的分档结果,决定 P2 各面能否立刻接线。
stdlib包名与归属:独立包,还是并入names(L3「名字单源」)?(2026-10-10 审阅倾向:独立包——names是产名族,语义不同。)libc_single_source的最终形态:降级为签名对拍闸,还是整体被新闸取代?- 病 11 第二层是否停手(§8 补充裁定项):剩余专臂收编暂停 vs 收完再迁——影响 P2 工作量与重排次数。
多文件编辑面(demo)的注入路径(已答:2026-10-05 files-vfs 批已通,§4.4);剩capabilities.libraries载体形态(独立帧 vs 握手摘要,§5.1)。