Roc 编译器的工程铁律:从 AGENTS.md 到源码中 RedirectRule 的强制机制
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
Roc 是一个快速、友好且面向函数式的语言,其编译器对"纪律性"的要求近乎苛刻:根目录下的 AGENTS.md 用 10 条硬性规则定义了编译器的工程不变量——禁止 workaround、禁止 fallback 与启发式、各阶段必须消费显式数据、后端不得自行思考引用计数,并规定了解题器(checker)中"探测-改写"操作的声明先行流程。读完本文,你将理解这些规则各自约束的是什么行为、为什么这样设计,以及它们如何被 design.md 的"Solver-Mutating Rewrites"章节和 src/types/store.zig 中的类型签名机制落到实处。
规则总览:10 条不变量
AGENTS.md 的全部规则可归纳为四层:
| 层 | 规则 |
|---|---|
| 设计先行 | 修改任何代码前必须先阅读 design.md,它是已检查模块(checked modules)、post-check IR 管线、LIR、ARC、后端、LirImage 与编译器不变量的前瞻性设计参考 |
| 数据流纪律 | 禁止 workaround(绝对禁止);除解析与错误报告外,禁止一切 fallback 与启发式;除解析与错误报告外,每个编译阶段必须消费前一阶段产生的显式数据,而不是试图恢复、猜测、重建、近似或"尽力而为"地弥补缺失信息 |
| 后端纪律 | 后端不得以任何方式思考引用计数,只能笨拙地(dumbly)执行前序编译步骤显式发出的 LIRincref/decref语句 |
| 流程纪律 | Fuzzer 必须生成小型带类型语言结构并随机组合,禁止手工场景发射器的"目录式"fuzzer;zig build minici失败时先定向修复失败的 section;checker 中新增"探测-改写"重写前必须先声明规则 |
这些规则的边界措辞值得注意:几乎每条禁令都保留了同一个例外——解析(parsing)与错误报告(error reporting)。这意味着 Roc 把"语言核心判断"与"用户交互体验"明确分开:类型求解、IR 生成、优化、代码发射各阶段必须是确定性、无猜测的数据变换;唯有解析和报错这两类天然需要"猜用户意图"的环节被允许使用容错手段。
阶段间显式数据契约
"每个阶段消费前一阶段的显式数据"这一条,是对编译器工程中最常见坏味道——隐式恢复(implicit recovery)——的系统性封堵。从 design.md 的结构看,该不变量贯穿 checked modules、post-check IR、LIR 与后端:上游阶段若未产生某类信息,下游阶段不允许自己去推断,只能报错或走设计文档中声明的显式路径。这一约束直接服务于可审查性——如果某个阶段可以"best effort"地重建信息,那么代码评审时无法区分"正常路径"与"兜底路径",编译器行为也就无法被静态推理。
后端与 ARC:引用计数的显式化
AGENTS.md 中针对后端的规定是:"后端以任何方式思考引用计数都是绝对禁止的,唯一允许的行为是笨拙地执行前序编译步骤显式发出的 LIRincref和decref语句。"从 design.md 的相关章节可以印证这一设计的意图:后端只接收普通 LIR 与显式的 ARC 语句,"不得知道一个值究竟来自 public iterator、minted iterator、forced-dynamic callable state 还是被标量化的循环"。把引用计数决策完全上移到 IR 层,让后端退化为纯执行者,是保证多后端(当前与未来目标)行为一致的关键隔离手段。
Fuzzer 与 minici 的流程纪律
关于 fuzzer,AGENTS.md 规定其必须"生成小型带类型语言结构并随机组合",明确否决了由手工编写场景发射器拼成的"目录式"fuzzer。这保证了测试语料分布来自语言文法的随机组合而非开发者手挑的用例,避免覆盖偏向。
关于 build.zig 中的minicistep(其定义为Run a subset of CI build and test steps,见 build.zig),AGENTS.md 给出了一套明确的调试协议:当zig build minici在某个 section 失败时,先修复该 section 并反复重跑这个特定 section直到通过,再回到完整的zig build minici。完整运行只用于"找到下一个失败的 section",而不是充当内层重试循环——这是对"每次改动都全量重跑"这种低效调试习惯的显式纠正。
核心深潜:探测-改写重写与 RedirectRule 签名强制
AGENTS.md 的最后一条规则信息密度最高,也是最能体现"设计文档即 API"理念的条款。它针对的是 checker 中的一类危险操作——probe-then-mutate rewrite:对已求解类型做一次结构化探测,再依据探测结果去改写已求解的图,或重盖(restamp)已盖章的 dispatch-plan 元数据。
为什么这类操作需要"先立法规"
design.md 的 "Solver-Mutating Rewrites" 章节解释了动机:纯粹的 unification(统一)是"什么程序能通过类型检查"的最终权威。任何在普通 unification 之外改写已求解类型图、或重盖 dispatch-plan 元数据的代码,要么是:
- 机制(Mechanism):改写不能改变哪些程序通过类型检查,也不能改变无错程序的输出计划。例如对已报告错误的诊断恢复、"写出与 unify 完全相同结果"的描述符快速路径、孤儿变量回收;
- 策略(Policy):改写让原本会被纯 unification 拒绝的程序通过类型检查,或改变无错程序的 checked-module 输出。每一条 policy 改写必须实现本文档(design.md)中已声明的一条规则、以该规则命名,并有测试同时钉住其"接受侧"与"拒绝侧"。
该章节点破了这类代码的根本危险:一个 probe-then-mutate rewrite 在评审时与语言类型规则本身的一次修改无法区分——它能通过自己的复现测试,而类型系统中没有任何东西会标记"子型关系或分派策略变了"。因此规则要求:新重写必须先声明规则、以规则命名重写、测试钉住接受与拒绝两侧;"它让一个测试通过了"不构成一条规则。
源码层面的签名强制:dangerousSetVarRedirect
这套流程不是靠自觉,而是靠编译期签名强制执行。src/types/store.zig 中定义了RedirectRule枚举,目前恰好两个成员:
pub const RedirectRule = enum { /// (i) Diagnostic recovery: the target var belongs to an expression /// whose error has already been reported, and the redirect only lets /// checking continue past it. diagnostic_recovery_reported_error, /// (ii) design.md "Hosted Try Question Widening": ... hosted_try_question_widening, }; pub fn dangerousSetVarRedirect(self: *Self, comptime rule: RedirectRule, target_var: Var, redirect_to: Var) Allocator.Error!void { ... }关键设计有两点。其一,rule是comptime 参数且必须位于两个变量之前——没有规则引用的调用无法通过编译;新增调用点意味着必须引用一个现有成员或新增成员(并同步在 design.md 中声明其引用的规则),这让每一次图改写都是可 grep、可评审的。store.zig 中的测试 甚至用反射断言锁死了这个签名顺序(第一个参数必须是Store.RedirectRule类型)以及枚举的穷举性。其二,实现本身带防御性断言:把同一个求解类等价类重定向到自身会直接 panic("self-redirect of equivalent vars"),重定向到指向同一根的透明 alias 也会被断言拦截,避免静默产生自引用(无限)alias。
从源码结构看,src/check/Check.zig 中目前只有两个调用点,与枚举成员一一对应:
- Check.zig 第 22076 行:
.hosted_try_question_widening,用于把?条件的根重定向到期望错误行的新Try变量; - Check.zig 第 39788 行:
.diagnostic_recovery_reported_error,对已报告错误的表达式做恢复性重定向,让检查可以越过它继续。
已声明规则实例:Hosted Try Question Widening
design.md 声明的 policy 规则之一是 "Hosted Try Question Widening",其完整语义是:?解包Try条件并把错误行重抛到外层函数的返回行。当被调方错误行已闭合、而外层注解返回行是开放的(rigid extension)时,普通 unification 会拒绝这对——这正是设计意图。唯一的声明例外是对被托管(hosted)函数的直接调用:托管函数的边界类型是键控在其声明闭合行上的 ABI 契约,托管被调方无法采纳调用方更宽的行,若要求调用者手工重标托管错误会使托管函数在?下不可用。当?条件是托管函数的直接调用(函数表达式静态解析到e_hosted_lambdadef;分派调用与值携带函数均不合格),且被调方行中每个可见错误都包含在期望行中时,checker 在使用点把条件加宽(condition 的根重定向到期望行的新Try,即上文 Check.zig 的调用点),托管被调方自身的声明类型保持不变。单态化降低会为加宽后的特化请求生成一个 Roc adapter,在请求类型处调用声明类型边界并把错误重标进更宽的行——extern 边界本身永远按声明行发射。
该规则的两侧都被测试钉住,这正是 AGENTS.md 所要求的"接受侧与拒绝侧":
- 接受侧:test/fx-open/issue_9963_hosted_try_question_mark.roc——在开放行平台函数中直接对托管函数用
?,编译通过且 host 的Ok被观察为Ok(对应 issue #9963 的回归场景:加宽请求必须由 adapter 桥接,而不是把 host ABI 特化到加宽布局); - 拒绝侧:test/fx-open/hosted_try_question_not_included.roc——外层注解省略了托管错误时,类型检查必须失败;
- 反例回归:非托管
?进入开放注解行仍是类型错误,回归测试在 src/check/test/type_checking_integration.zig(对应 issue #9798)。
design.md 特别强调:这条规则只决定"哪些程序通过类型检查",仅此而已;保持 host ABI 完整的是 Monotype 降低阶段的 producer-side 检查,与这条类型规则正交。
机制类改写的完整清单:Rewrite Inventory
design.md 第 7040 行起 的 "Rewrite Inventory" 把 checking 阶段所有求解器改写逐一点名并分类。dangerousSetVarRedirect的调用点之外,还包括:
markErroneousBranchWithExpected(机制:诊断恢复)——表达式已有已报告错误,其变量被重定向到与期望返回统一的新变量;unifyWithFresh(dangerousSetVarDesc,机制:写出 unify 根 flex 占位符与全新内容时恰好会产生的描述符的快速路径);markErroneous(机制:已报告错误后的诊断恢复,直接标记 checker 节点求解类,保留类级联抑制);retireCallLikeExprWithErroneousOperands/ 语句拥有的 iterator plan 恢复(机制:Erroneous Call Operand Retirement——操作数已拥有已报告错误时,其 call-like 父节点在任何 dispatch 约束引入前进入两个表达式集合);checkMatchExpr的分支模式目标(机制:错误 scrutinee 无法关联各分支模式,改为与共享新变量统一;无错程序永远不会到达该探测);resetAnnotationNodes(resetVarToUnbound,机制:scheme 已作为独立孤儿复制后回收注解节点变量);finalizeTypeDeclarationValidity、occurs-check 污染、validateNominalDeclArgumentGrowth、finalizeFunctionEffectsAtBoundary(策略类,分别对应类型声明模板有效性、参数增长验证、泛化边界处的定向 effect 物化等已声明规则)。
评审清单:把规则变成可执行的 diff 检查
AGENTS.md 最后给出的评审清单把以上机制翻译成同一次变更中的检查项:一个 diff 如果新增了RedirectRule成员、在清单外的位置调用setVarContent/dangerousSetVarDesc、或重盖 CIR plan 元数据,则必须在同一变更中更新 design.md 的 Rewrite Inventory。换言之,规则声明(design.md)、签名引用(store.zig/Check.zig)、清单登记(Rewrite Inventory)与双侧测试四个工件缺一不可——这正是"设计先行 + 编译期强制 + 测试钉住"三层防线在 Roc 编译器中的具体形态。
对于任何想在类似"强不变量"代码库中工作的人(人或 AI),AGENTS.md 示范了一条可复制的路径:把不可协商的工程原则写成无歧义的短句,为每条原则标注唯一允许的例外,再把最危险的那类操作提升到 API 签名层面强制执行,最后用清单(inventory)+ 双侧测试让每次改动都可审查。
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考