公告

FujoOS公布日延期,以仓库发布为准

文档 语言 写一个 Loment 库

写一个 Loment 库

库系统有一条宪法:学完 Loment,写库不该再学任何新东西。所以这里没有 pyproject.toml,没有 pom.xml,也没有第二种清单语言——任何「写库要额外学一门语法 / 格式 / 工具」的设计都被直接判负。

一件事 怎么做
一个库 一个目录里的一堆 Loment 文件——不是编译产物,没有单独的库文件
依赖 源码里的 use不用另行声明
导出 pub
清单 可选,而且它自己就是 Loment 源码(pkg.lomp

依赖从源码来,不从清单来

依赖不需要声明——它就是源码里的 use。解析器读一个模块的边,跟读它的函数一样自然。于是「依赖列表」这种东西在 Loment 里不存在,也就没有「声明与实际不符」这一类错。

这一条值得停一下:别的生态里,「清单写了依赖 A、代码其实不用 A」是一种长期存在的谎言类别——要靠额外的工具去检测它有没有过期。在这里,它连存在的地方都没有。

pkg.lomp:可选的标签文件

loment 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,零语言改动。不为此新增字符串常量:那是参考实现与自举镜像都要跟的语言特性,而它对这个设计没有必要。

身份 = 递归哈希

「同一个库」不是你说了算,也不是版本号说了算——是算出来的

text 实例身份
id(P) = H( 自身源码 ⊕ { 边名 → id(子) } )

只看自身源码是错的:同一份源码在两个构建里绑到不同的依赖上,语义不同,却会被错误地合并成一份。

多版本共存:靠物化,不靠运行期技巧

这是库系统最实的一块。同一个库的多个版本可以同时存在于一次构建里,而且不靠任何运行期机制:

  • 每个实例有自己的身份 —— 身份不同的就是不同的实例。
  • 实例被物化成各自独立的模块 —— 编译器眼里它们是不同名字的模块。于是 v1 的 Point 和 v2 的 Point不同的名义类型:把 v1 的喂给要 v2 的地方是类型错误——这正是对的。
  • 相同子树自动去重 —— 五个包依赖同一版,就是一份实例。

于是语义冲突由类型系统抓住,而不是靠「选一个版本」去回避。物化做三件事:

模块名带身份后缀:mathutilmathutil__7509dbd4
use <名字> 改写成指向它绑定的那个实例的显式路径
顶层名带实例后缀:scalescale__7509dbd4 / scale__2d54513b

第三步是共存的关键。编译器的发射符号是平的funcs[name] / structs[name]),同一库的两个版本必然导出同名顶层项。在物化这一层改名,而不是去改编译器内核——换来的是:编译器完全不需要知道「版本」存在,两个后端的发射符号一个字节都不用动,也就不牵动逐字节 IR 一致与自举定点。

改名只动顶层名。字段名、枚举变体、局部变量都不动:它们的作用域在类型或函数内部,两个实例各有一份也不撞。_start 也不动——那是链接器要找的入口名。

唯一一条边界,而且它是真歧义: 一个文件同时要用两个版本的同名项时,语言没有限定名语法可用。工具把它精确报出来,并拒绝物化。这不是「没做完」——多版本共存的前提本来就是「没有一个文件同时点两个版本的同名项」(app 用 mid1 和 mid2,两个 mid 各自用自己那份 mathutil)。

能力需求是算出来的

编译器看得见每一个 guard 和每一条 capability 声明,所以「这个包需要哪些能力」是一个可计算的事实

text 能力闭包
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,逐包哈希)。它有孪生实现与逐字节判据,所以保留着,供已经在用它的工具走。上面讲的才是库系统——两者不是同一个东西。

库的接口怎么设计

看一眼仓库里两张真库的签名,它们长这样:

loment 真实签名
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 —— allocstr_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 只做到 idtree / cap / check / materialize 还没有 Loment 版。别把「孪生」读成「全等」。
  • 实例会重复进二进制 —— 多版本共存意味着体积按实例数增长。工具会把它显示出来(tree / check 都会打印同名多实例);藏起来的多份才是问题。
  • _start 冲突 —— 依赖里也定义了 _start 时,check 报出来、materialize 拒绝。入口只能有一个,这条不是改名能消掉的。
  • 物化会改写源码副本 —— 它不碰仓库里那棵。报错行号指的是副本,materialize-map.json 记了副本回原文件的映射。
一句话总结: 库 = 一个目录;依赖 = 源码里的 use;身份 = 递归哈希;多版本 = 物化改名;能力 = 推导出来的。作者要学的东西是零。