docs/current/04-标准库与防线/标准库单源与头文件多文件设计20261010.md
GitHub ↗
当前有效

标准库单源与头文件 · 多文件编译设计(2026-10-10)

6018 字·约 16 分钟 阅读 2026-10-10 17:07

状态:设计稿(方案 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 三条钉死的病灶

  1. 现有闸只查"名字集合",不查签名。 scripts/moonbit/libc_single_source/main.go 判据是 builtin_all == (host ∪ bytecode) − excluded;全文 grep signature|签名|return_type|param 零命中。⇒ N3/N4 那类签名漂移(printf 返回 void→int、strcpy 返回 char* vs void)在当前闸下结构上不可见,当时靠人读 C 标准抓出。注入实证(2026-10-10 审阅):把 bytecode_sig.mbt 的 strcpy 返回类型改成 v_ty() 后该闸仍 PASS。7780284d 已补 param_kinds 字符集静态锚(P/I/D,表加载层即红)——但这只锁表内自洽,跨表/专臂的签名一致性仍无闸。
  2. host 路由的真源是一份冻结的 Rust 快照。 scripts/moonbit/gen_host_route/host_func_id.snapshot.rs 是 host_func_id_gen.mbt 的输入——native/ 已于 2026-10-05 删除,这张表仍在消费 Rust 冻结区的副本。
  3. "单源"想做但做不成。 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 三条边界(本次决策的完整口径)

  1. 内部事实 → 原生 MoonBit 包。不引入 JSON / manifest / CSV。
  2. 对外出口 → 从原生真源生成 JSON,不手写。本仓已有范式:gateway/serve_static.mbt 的 memory_model 直接由 @bytecode.MEM_SIZE 等原生常量导出。
  3. 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 维持「先测量后决定」,倾向不做。

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. 开放问题(待定)

  1. P0 对照表 中"照搬物 vs 自有事实"的分档结果,决定 P2 各面能否立刻接线。
  2. stdlib 包名与归属:独立包,还是并入 names(L3「名字单源」)?(2026-10-10 审阅倾向:独立包——names 是产名族,语义不同。)
  3. libc_single_source 的最终形态:降级为签名对拍闸,还是整体被新闸取代?
  4. 病 11 第二层是否停手(§8 补充裁定项):剩余专臂收编暂停 vs 收完再迁——影响 P2 工作量与重排次数。
  5. 多文件编辑面(demo)的注入路径(已答:2026-10-05 files-vfs 批已通,§4.4);剩 capabilities.libraries 载体形态(独立帧 vs 握手摘要,§5.1)。
✎ 在 GitHub 上编辑此页 最后更新 2026-10-10 17:07 · docs/current/04-标准库与防线/标准库单源与头文件多文件设计20261010.md