写一个 Loment 库
库系统有一条宪法:学完 Loment,写库不该再学任何新东西。所以这里没有 pyproject.toml,没有 pom.xml,也没有第二种清单语言——任何「写库要额外学一门语法 / 格式 / 工具」的设计都被直接判负。
| 一件事 | 怎么做 |
|---|---|
| 一个库 | 一个目录里的一堆 Loment 文件——不是编译产物,没有单独的库文件 |
| 依赖 | 源码里的 use。不用另行声明 |
| 导出 | pub |
| 清单 | 可选,而且它自己就是 Loment 源码(pkg.lomp) |
依赖从源码来,不从清单来
依赖不需要声明——它就是源码里的 use。解析器读一个模块的边,跟读它的函数一样自然。于是「依赖列表」这种东西在 Loment 里不存在,也就没有「声明与实际不符」这一类错。
这一条值得停一下:别的生态里,「清单写了依赖 A、代码其实不用 A」是一种长期存在的谎言类别——要靠额外的工具去检测它有没有过期。在这里,它连存在的地方都没有。
pkg.lomp:可选的标签文件
module pkg
pub fn name() -> str {
return "mathutil";
}
pub fn version() -> str {
return "0.1.0";
}
- 没有 deps 段 —— 依赖在源码里。
- 可选 —— 没有它就用目录名当包名,版本记为 0.0.0。
- 只认字面量返回 —— 清单是标签,不接表达式,免得逼出「清单能算多复杂」这种问题。
pub const NAME: str = "x"; 是语法错。所以标签写成 pub fn … -> str,零语言改动。不为此新增字符串常量:那是参考实现与自举镜像都要跟的语言特性,而它对这个设计没有必要。
身份 = 递归哈希
「同一个库」不是你说了算,也不是版本号说了算——是算出来的:
id(P) = H( 自身源码 ⊕ { 边名 → id(子) } )
只看自身源码是错的:同一份源码在两个构建里绑到不同的依赖上,语义不同,却会被错误地合并成一份。
多版本共存:靠物化,不靠运行期技巧
这是库系统最实的一块。同一个库的多个版本可以同时存在于一次构建里,而且不靠任何运行期机制:
- 每个实例有自己的身份 —— 身份不同的就是不同的实例。
- 实例被物化成各自独立的模块 —— 编译器眼里它们是不同名字的模块。于是 v1 的
Point和 v2 的Point是不同的名义类型:把 v1 的喂给要 v2 的地方是类型错误——这正是对的。 - 相同子树自动去重 —— 五个包依赖同一版,就是一份实例。
于是语义冲突由类型系统抓住,而不是靠「选一个版本」去回避。物化做三件事:
| 一 | 模块名带身份后缀:mathutil → mathutil__7509dbd4 |
| 二 | use <名字> 改写成指向它绑定的那个实例的显式路径 |
| 三 | 顶层名带实例后缀:scale → scale__7509dbd4 / scale__2d54513b |
第三步是共存的关键。编译器的发射符号是平的(funcs[name] / structs[name]),同一库的两个版本必然导出同名顶层项。在物化这一层改名,而不是去改编译器内核——换来的是:编译器完全不需要知道「版本」存在,两个后端的发射符号一个字节都不用动,也就不牵动逐字节 IR 一致与自举定点。
改名只动顶层名。字段名、枚举变体、局部变量都不动:它们的作用域在类型或函数内部,两个实例各有一份也不撞。_start 也不动——那是链接器要找的入口名。
能力需求是算出来的
编译器看得见每一个 guard 和每一条 capability 声明,所以「这个包需要哪些能力」是一个可计算的事实:
requires(P) = 自身 guard 的域 ∪ ⋃ requires(子)
工具把它打出来,而且带来源链——哪一段来自哪个依赖。它不要求作者手写一遍,因为写错的声明比没有声明更坏:没有声明时工具会去算,写错了则会让所有人信一份假的清单。
工具
| 命令 | 作用 |
|---|---|
python tools/loment.py lib tree DIR | 依赖树,以及每个库的实例数 |
python tools/loment.py lib id DIR | 实例身份(递归哈希) |
python tools/loment.py lib cap DIR | 能力需求闭包,带来源链 |
python tools/loment.py lib check DIR | 冲突检查:同名顶层项 / 同名不同域 / 入口 |
python tools/loment.py lib materialize DIR --out DIR | 把嵌套与多版本摊成编译器能直接吃的树 |
这一层只在源码仓库里可用:python tools/loment.py lib ... 派发到 tools/lomlib.py。发行包没有 Python,所以发行版的 loment 命令面里没有 lib 这一组——把它列进 help 而敲下去得到「未知命令」,那是骗人。use 本身的解析则在编译器里,而且 Pre1 把它改成了三层:① <项目根>/deps/<名字>/ ② 工具链自带 <工具目录>/../share/lompi/store/<名字>/<版本>/ ③ 内置的四根。前两层是搜索路径(像 PYTHONPATH),先命中先用;只有第 ③ 层要求唯一,找不到或有歧义都报错,不静默取第一个。单文件的 use 条数上限也从 8 提到 300,超了报 E020。
lompkg,读 pkg.json,逐包哈希)。它有孪生实现与逐字节判据,所以保留着,供已经在用它的工具走。上面讲的才是库系统——两者不是同一个东西。
库的接口怎么设计
看一眼仓库里两张真库的签名,它们长这样:
pub fn sha_update(s: ptr, p: ptr, n: u32) pub fn json_parse(js: ptr, len: u32, arena: ptr) -> u32 pub fn load32(p: ptr, off: u32) -> u32
没有切片参数、没有按值的 struct、没有容器类型。这是三个约束叠在一起的结果,不是风格:
- 没有标准库 —— 没有
String/Vec/HashMap,也没有内存管理。要什么自己用alloc从堆里划。 - 按值传聚合目前走不通 —— 包里的自举链接器还不支持按值传 struct / enum。所以库的接口一律是 ptr、偏移量和 u32 下标。
- bump 堆只有 64 KiB ——
alloc与str_concat都从它里面拿。库想省内存,就得让调用方把缓冲传进来,而不是自己分配。
第三条正是 json.lomt 的设计来源:它自己不分配,调用方给一块 arena,库在里面建节点,返回 u32 下标。想写一个能被别人用的库,这是当前最站得住的一种接口形状。
写库时会踩到的坑
- 形参最多 10 个 —— 超了会让自举阶段的编译器自己崩掉,症状是「编译这个文件时编译器死了」,而参考实现编同一份文件正常。超了就把参数打包。
-
let必须写类型 —— 语言没有类型推断,let x = 1;不合法。另外没有无值的return;,要写return <expr>;。 - match 的臂体是块 —— 写
Kind::A => { return 0; },不是Kind::A => 0,。 - 泛型调用的实参要能把类型定出来 ——
pick(4, 9)报的是「调用未定义的函数 pick」,这条消息会骗人,它不是跨模块问题。先let a: u32 = 4;再pick(a, b)就对了。 - 结构体字面量不能直接当实参 ——
f(S { a: 1 })会被原生后端拒。先let x: S = S { a: 1 };再传x。 - 先定义后使用 —— 模块内不要依赖前向引用。
- 一次只改一处 —— 一个解析错误会中止整个检查,后面的诊断一条都不出来。「没有别的错」通常只是没检查到。改完立刻跑
loment check。 - check 通过不等于能出可执行文件 —— 按值传聚合、以及用
==比较 struct / enum,检查器放行而原生后端会拒。库的接口避开这两个形状。
现在做不到的
- 不做二进制分发 —— 而且不可能:泛型是类型定向单态化的(
Result_i64_u32),使用方必须看得到源码才能定出实例。 - 不做版本区间语言 —— 机器的身份是内容哈希,版本号是给人看的标签。选版只有一条规则:版本最大(按
.分段做数值比较,所以0.10.0大于0.9.0)。没有区间、没有回溯求解、没有冲突消解策略这一整类复杂度;要钉死就写名字@版本,或者用锁。Lompi 就是这条规则的实现。 - 不做注册中心服务端 —— 网址库就是一个 git 仓库,布局与 store 相同,没有服务端要运维;取件与安装由 Lompi 做,网络那一步交给 git。
- 没有版本区间语言 —— 选版只有一条规则:版本最大;要钉死就写
名字@版本,或者用锁。 - 没有动态链接 —— 目标是 freestanding 的 PE / ELF,单一
_start,没有动态装载器。
已知边界(诚实清单)
- 改名是 token 级的 —— 它靠「作用域在类型内部的名字不动」加「逐文件统一改名」保语义。失效路径是响亮的(漏改或多改 → 编译器报未声明 / 未定义,不会静默编错),但它是这一层的实现方式,不是语言保证。真要语言级保证,得让编译器按实例做名字解析——那时这一层就该删掉。
- 孪生只覆盖了身份这一步 ——
lomlib.lomt只做到id:tree/cap/check/materialize还没有 Loment 版。别把「孪生」读成「全等」。 - 实例会重复进二进制 —— 多版本共存意味着体积按实例数增长。工具会把它显示出来(tree / check 都会打印同名多实例);藏起来的多份才是问题。
-
_start冲突 —— 依赖里也定义了_start时,check报出来、materialize拒绝。入口只能有一个,这条不是改名能消掉的。 - 物化会改写源码副本 —— 它不碰仓库里那棵。报错行号指的是副本,
materialize-map.json记了副本回原文件的映射。
use;身份 = 递归哈希;多版本 = 物化改名;能力 = 推导出来的。作者要学的东西是零。