构建流水线
一次 build 到底发生了什么:发现、顺序、schema 探测、写入、Niagara 编译、存盘、盖戳,以及为什么"没改就跳过"。
一次构建把源码变成资产,中间经过六个阶段 —— 诊断码的首位数字就是它抛出的阶段:
| 阶段 | 干什么 | 码段 |
|---|---|---|
| 1 | 驱动与文件 I/O | DFX1xxx |
| 2 | 词法与语法 | DFX2xxx |
| 3 | 声明与文档结构 | DFX3xxx |
| 4 | 值、类型与表达式 | DFX4xxx |
| 5 | 生成与资产写入 | DFX5xxx |
| 6 | Niagara 编译 | DFX6xxx |
另外两族不在这条主线上:DFX7xxx 是溯源、漂移与 lint,
DFX8xxx 是反编译器。
顺序:模块 → emitter → 系统
build -All(以及编辑器里的 Rebuild DFX)按这个顺序建,理由很实在:一个 .dfm 和调用它的
.dfs 同时排队时,模块必须先存在,否则系统解析不到它。
编辑器侧的 watcher 用同一套顺序 —— 它把改动的文件盖进一个待建队列,防抖 ticker 排空这个队列。
一个文件的旅程
解析
词法 + 语法 → 一份文档。位置信息(文件、行、列)在这里就挂上了,一路带到最后一条诊断上, 所以失败的构建能弹出一个直接跳到出错列的 Open in VSCode 链接。
解析模块名与 schema
模块短名在 ModulePaths 上解析(L4)。名字解析出来之后,输入签名是探测出来的,不是猜的:
把模块加进一个临时系统,读它在那个栈里真正暴露的输入。
这是必要的:资产层面的 schema 看不见内联 edit condition,也看不见静态开关揭示出来的输入。
dfx schema <Module> -Stack <Stack> 跑的就是这同一趟探测。
Niagara 编译
写完之后系统要编译。编译不干净就是 DFX6xxx —— 包括引擎自己的栈问题
(缺依赖、未绑定参数),DreamFX 把它们转述成带位置的诊断。
没改就跳过
第二次构建同一个没动过的文件,报的是 0 built, 1 up to date:资产上戳着的 hash 与源码的 hash
一致,就没有事可做。
| 想要 | 用 |
|---|---|
| 强制重建,无视 hash | -Force |
| 建但不写包(只看能不能过) | -NoSave |
| 只做静态检查,不碰资产 | lint |
| 只对照资产检查,不写任何东西 | verify |
编辑器里构建 vs 命令行构建
同一条管线,两个入口。差别只有一个,而且是硬的:
编辑器开着时不要跑写包的命令。 两个进程存同一批包,谁后存谁赢,两边都不吭声。
build / corpus / mirror-diff / decompile-all 都属于写包。
还有一条同样是硬的:重建一个系统之前先让它的实例消失 —— 见日常循环。
性能:为什么全树构建不是线性的
一次全树构建里,绝大部分时间不在解析也不在写入,而在 Niagara 编译。单个资产上量到过 84% 的时间花在那里,每个资产平均触发约 4 次系统编译。
几个可调项,默认值都已经是快的那一侧,它们存在主要是为了 A/B 和逃生:
| 开关 | 作用 |
|---|---|
-Window=<n> | 流水线深度:允许多少个系统同时处在「已请求编译、尚未 finalize」之间。1 退回完全串行 |
-NoWriteScope | 每次写入都重建编辑上下文(旧行为,慢得多) |
-RebuildOnStructural | 每次结构性写入后丢弃编辑上下文(旧行为) |
-RebuildOnSwitch | 每次写静态开关后丢弃编辑上下文(旧行为) |
-RebuildPerAdd | 每加一个模块都付一次引擎的栈刷新,而不是每个栈批量刷一次 |
这些开关不是给日常用的。它们的价值是:一台机器上、一份二进制里,把"快的做法"和"慢的做法" 各跑一遍,于是「这次优化到底有没有用」是量出来的而不是读代码读出来的。