# Guide:证明在测的路径(绿色 ≠ 在测你以为在测的)

> 适用:写差分 / 差分测试 / 性能 / IC 快路径 / wasm 快路径 / 错误冒泡类**任何对路径执行做断言的测试**时,以及加 e2e 语料 / 设计验收 oracle 前;**或修完一个 issue / crasher 报上来的那个具体输入、准备收工时**(§4.1,一个 reported case 只是接受面的一个采样,要枚举到达同一段代码的全部通道);**或读到 / 写下「已知差异 / 已登记豁免」这类注释时**(§4.2,必须指出哪一行代码或哪个测试执行了它);**或回归测试期望与实现冲突、要决定改哪一边时**(§4.3,先取事实,默认怀疑实现会诱导去「修」一个正确的实现);**或扩接受面 / 换硬件 / 改 fuzz 参数后要不要立刻跑 fuzz 时**(§5);**或调试机制叠加多档的崩点时**(§6);**或收到「某条路径导致 N 倍退化」类归因、动手写止血/修复计划前**(§7,诊断侧对偶);**或收到「某机制没生效」的指控、要写复现测试时**(§7,复现侧);**或读一个投机/inline 快路径的性能收益数字、或复查一个历史 perf 裁决时**(§8,度量单位/时间窗口);**或分诊某 P4 tier bug 时**(§7 同源:P4 build 有多种 code kind 并存,直接读某一种 emit 之前先用探针分类走的哪条);**或想在差分 harness 里接受 / 跳过某类平台差异时**(§9.0,**先判这个差异该在渲染侧消除还是在比较侧归一** —— 判在渲染侧就不要设计判据,反复冒 edge case 是选址错误的信号);**或已经确定要在比较侧写跳过判据 / 加前置检查时**(§9.1–§9.5,判据的每个输入 —— 含它花掉的时间、分配量、matcher 步数预算 —— 都必须两侧相同,否则后果是单侧 skip);**或给运行时新增了任何写外部世界的 API 之后**(§9.7,新路径要同时接进 harness 的捕获,否则它静默退出比较范围);**或差分比较报一致而输出是空的 / 是错误文本时**(§9.8,两侧在同一个错误上一致 ≠ 行为一致);**或被测功能依赖 stdin / tty / 信号 / 工作目录这类进程级环境时**(§4.10,测试驱动没提供它,测出来的「不工作」是假的);**或写一个「代码里有某个标志 / 字段 / 调用」的检查型测试时**(§9.1b,别 grep 整个文件——一个注释就能满足的测试没有在测代码)。
> 来源:十六个独立实例聚合的家族纪律——`memory/reflections/2026-06-14-p3-pw5-table-ic-round.md`(inline-proof) + `2026-06-14-p3-pw6-crosslayer-call-round.md`(TierStuck no-op) + `2026-06-15-p3-pw9-acceptance-perf-round.md`(空测 vararg 顶层)+ `2026-06-15-p3-pw10-r3-call-indirect-round.md`(错误路径盲区)+ `2026-06-15-p3-pw10-r1-r2-callinfo-migration-round.md`(基准工作负载错配)+ `2026-06-15-p3-pw10-zerocross-stage3-round.md`(快路径命中盲区)+ `2026-06-16-vs0e-varargs-stack-underflow-round.md`(覆盖度先验证,正向侧)+ `2026-06-30-pr27-f3-3b-darwin-arm64-execute-roundup.md`(bypass 探针根因 isolate + CI runner 形式盲)+ `2026-07-03-issue40-arm64-stopbleed-round.md`(**诊断侧对偶**:退化归因前先证被怪罪的路径存在)+ `2026-07-02-p4-beat-p3-opset-round.md`/`2026-07-03-issue40-arm64-stopbleed-round.md`/`2026-07-03-issue45-issue39-round.md`(**fuzz 探索空间维度**:接受面 / 硬件 / 参数任一维动了就重探,§5 三实例)+ `2026-07-07-issue67-auto-mode-coverage-round.md`(**未强制测试静默退化**:auto 模式测试不带升层兜底断言,悄悄滑回纯解释器覆盖)+ `2026-07-08-issue67-amd64-nodehit-crossrun-round.md`(**度量单位/时间窗口**:单 Run 命中数 vs 跨 Run 稳态 dispatch,§8)+ `2026-07-18-review-p3-loopfuel-rearm-round.md`(**复现侧**:复现「机制没生效」的指控时,复现测试自己也要先证明走的是被指控的路径,§7.1)+ `2026-07-24-p4-template-forprep-deopt-round.md`(**分诊侧同源**:P4 tier bug 分诊必须先用探针分类 PerOpCode/nativeCode/p4Code 三大类 code kind 走的哪条,traceback 直觉只锁触发前提)。前八个实例在**测试侧**(证明「路径真被走到」);第九个在**诊断/归因侧**(证明「被怪罪的路径真的存在/被执行」)扩到止血/修复计划前;第十~十二个在**探索空间维度侧**(§5,证明「fuzz 覆盖度不是时长的单调函数」)扩到 fuzz 与接受面/硬件/参数联动;第十三个回到**测试侧**,贡献「自然触发路径(auto/natural)测试必须配白盒兜底断言」的具体解药形式;第十四个在**度量侧**(§8,证明「性能收益的度量单位/时间窗口选错,会把一个从没生效的优化误判成生效但太贵」);第十五个在**复现侧**(§7.1,证明「走错路径的复现失败是伪证据,会把真 bug 误判成不存在」);第十六个在**差分 harness 判据侧**(§9.1–§9.5,`2026-07-26-issue184-185-nan-coercion-noncomparable-round`:26 轮盲审、13 轮抓到的缺陷是修上一轮时引入的,证明「判据只在一侧命中会静默丢掉整条输入,而判据的输入不止是值,还包括它花掉的时间、分配量与 matcher 步数预算」);第十八个在**捕获面侧**(§9.7/§9.8 与 §4.10,`2026-07-29-issue205-206-208-io-userdata-debug`:新增 `io.stdout` 带来一条 harness 不捕获的输出通道 —— 两侧都不捕获、两侧都一致、连 skip 都不留;而给它写的捕获 wrapper 引用了一个不存在的 helper,两侧以完全相同的方式失败、比较照样报一致;同轮 `io.read` 的八个探针全返回 nil 是因为 `go test` 不转发 stdin,量到的是 harness 不是代码);第十七个在**选址侧**(§9.0,`2026-07-26-oracle-nan-render-redesign`:同一个问题走到第 31 轮后整体转向 —— 差异改在**产生它的地方**消除,前十六个实例都是「在某处证明某件事」,这一个是「先证明这件事该在哪里做」,证明「反复冒 edge case 是选址错误的信号,而一个否证结论的适用范围不写下来就会被悄悄外推」)。

**核心断言**:**测试通过 ≠ 在测的路径被走到**。同一段绿色结果可能来自三类「静默替身路径」:① 静态分析挑剔(F1/F2 结构性排除)使被测路径根本没被编译/触发;② 测试 harness 自身跳过(对错误 `Fatalf` / vararg 顶层不升层 / 缓存命中前路径死);③ 被测对象语义等价两条路径(inline 快路径 vs helper 慢路径),输出 byte-equal 但**走的哪条不能从输出反推**。

测试绿、性能数字不动、新机制就位三件套**单独任何一个都不证明在测路径被执行**。必须有**正交于输出**的路径执行证据。

同一物理基础还有一个**诊断侧对偶**(§7):**退化数字 ≠ 慢在你以为的路径**。输出(测试绿 / 性能慢)本身不携带路径信息——不管是「路径真被走到」还是「路径真的存在并背锅」,都必须用独立于输出本身的证据反推。

## 1. 反模式三档

| 档 | 形式 | 实例 |
|---|---|---|
| **空测 / 不公平基准** | 测的不是宣称在测的层 / 用相同负载形式对比不可比的两层 | PW9 loop `for` 写顶层 vararg chunk(F1 不升层),实测「crescent==crescent ≈1.0x」推出「memory-resident 根本限制」并准备立**错的**后续里程碑;PW10 R1-R2 bench 把 kernel 包内层函数调 50 次对裸顶层循环,工作负载错配致 loop「慢 20 倍」误读 |
| **静默替身** | 路径有显式 fallback / 等价语义,绿色来自 fallback 而非 happy path | PW5 IC inline 与 helper 输出 byte-equal,普通 e2e 区分不了 inline 走没走;PW6 TierStuck 吸收态使 force-all promoteProto 静默 no-op,深 baseline 测试自然 `proto.tier != TierGibbous` 但测试套不抓;PW10 R3 错误路径漂移在全成功语料 difftest 下结构性失明 |
| **覆盖度自欺** | 自行写 11 条语料看似全面,实则远不如已有官方/oracle 测试套 | VS0-e 子步 ⑥ 计划写 11 条 vararg 形式语料,但 `test/luasuite/testdata/vararg.lua`(官方 5.1 vararg 全套)+ `closure.lua`(NeedsArg + vararg + 协程多值 yield/resume 最复杂组合)已经字节级一致通过——**手写语料比官方测试权威性低 N 倍** |
| **未强制测试静默退化** | 测试套自称覆盖某条自然触发路径(auto/natural),但实际条件永远不满足,悄悄滑向另一条更平凡的路径 | issue #67 auto-mode 覆盖轮:CI 里 P3/P4 的差分/一致性/fuzz 套长期只经 `SetForceAllPromote(true)` 驱动,未强制的测试脚本从没长到能在生产阈值(entry 200 / back edge 1000)下自然越线,auto 在这些测试里**退化成纯解释器**,与真正测「auto 决策链」是两回事。同一形式还有**静态门维度**:issue #77 math intrinsic 第一版 e2e 用直接 `math.sqrt(i)`,过不了 F2-b unknown-call ⟹ proto 根本不升 native、intrinsic 路径永远到不了,须 `local sqrt = math.sqrt` 别名才升层——不只 auto 阈值没到会退化,调用形式过不了静态门也会,两者同靠 §2(b) `PromotionCount>0` 兜底断言防住 |

## 2. 反向侧解药(证「路径真被走到」)

针对档 ①/② —— 加**正交于输出的路径执行证据**。三招:

**(a) 毒化 / 哨兵 helper(IC 快路径类)**
被测对象 inline 与 helper 输出等价时,把 helper 改成「无副作用但断言不该到」/「不写目标寄存器」/「返回 error」,使被测路径走到 helper 时**输出可分辨**。例:PW5 inline-proof 测试把 `h_getglobal` 改成毒化助手,inline 命中则 R(A) 写入正确值,inline 漏掉走 helper 时 R(A) 仍是 nil(哨兵不写),断言区分。

**(b) 正向 tier / 命中计数器(升层 / 快路径类)**
对 tier-vs-tier 基准 / wasm 快路径 / inline IC,加**白盒计数器**(`atomic.AddInt64(&fastCallHits, 1)` 等),测试断言计数器单调增。例:PW10 ③b emitReturn 守卫快路径加 `doReturnHits` 计数,`TestPW9_ZeroCross_ReturnFastHit` 经 helper→f 不增计数验快路径命中;PW10 顶层升层加 `TopLevelUplift` 探针(DoReturn 增量证 wasm 入口被走)。也包含**直接断言 tier 状态**:`TestPW9_ForceAllPromoteReal` 断言 proto 真到达 `TierGibbous` 不是 `TierStuck` no-op skip。同族解药还包括**自然触发路径的兜底断言**:测试套自称覆盖 auto/natural 升层(而非 force-all)时,配 `PromotionCount>0` 一类断言证明该 run 里确实发生过至少一次真实升层——否则短用例语料可能在收益门调整后悄悄退化成纯解释器覆盖而无人发现(issue #67 auto-mode 覆盖轮:第一版语料就当场挂在这条断言上,逼语料从短核换成能通过收益密度门的长纯算术核)。同一条 `PromotionCount>0` 断言也把**能力级静默回退**抓出来:issue #91 relooper diamond 修复配的白盒 `TestP3_TopLevelDiamondPromotes` 断言 `PromotionCount>0`——byte-equal 分不清「真升层了」和「静默 CFAIL 回退解释器仍产出正确值」,顶层 diamond 永远升不了 P3 这个 bug 藏了这么久正是因为全成功语料的 difftest 全绿、没人有正交证据看它到底升没升(fixture 随判据多维化演进的配套纪律见 [[backend-capability-vs-profitability]] §6)。

⚠️ **白盒命中计数器要按稳态窗口(跨 Run)统计,不按单 Run 统计**(详见 §8)。「命中 N 次」是「机制被触发过」的证据,不是「机制在稳态生效」的证据——若快路径带一道会被跨 Run 重建打穿的身份 guard,它可能在第一个 Run 里碰巧命中(表还是 promotion 那一刻的那张)、后续 Run 全落空,单 Run 计数器看着漂亮但稳态从没真生效。读命中计数器时选跨 Run 窗口,别拿单个 Run 的命中数下结论。

⚠️ **测「吸收态分类」的载体调用次数要过完 force-all 的 warm-up retry window**。`PromotionCount>0` 一类断言只证升层吸收(TierGibbous)真发生,吸收到 Stuck 分类(`StuckNotCompilable` / `StuckDeclined` / `StuckCompileFailed`)的断言另有陷阱:`considerPromotion` 对 not-compilable 的 proto 在 forceAll 下有 `pd.EntryCount < 64` 的显式 return(issue #40 留下的 warm-up retry window,给 IC-gated 后端几十次机会热起来再判死),载体调用次数少于 64 就永远停在 `TierInterp`、吸收态计数器保持 0、断言 `StuckNotCompilable != 0` 直接挂——测的是 Interp 状态不是 Stuck 状态,与真正想测的分类语义无关。可复用判据:**测 tier 状态机吸收态相关的断言时,载体的调用次数要显式跨过 retry window**(当前是 64,若阈值改动就跟着改)。实例 `TestTierAdmin_StatsClassifyStuck` 第一版载体只调 vararg 函数 20 次,`StuckNotCompilable == 0` 挂了才发现;改到 100 次并把这个约束在 Lua 注释里写清楚后过线。

**(d) 运行期开关的三段式路径证明(kill switch 类)**
测运行期状态切换(kill switch / `SetTierEnabled` 类)不能只断言「结果一致」——结果一致在切换语义测试里等价于**没测**(关掉 tier 之后跑对了不等于关成功了,可能根本没关或者关了又自己开回来)。三段式:

1. **开启 + 驱动一遍**:断言路径命中(`NativeRunCount` / 相应 tier 的 dispatch 计数 delta > 0),否则后续两段测的是 vacuous 状态;
2. **关闭 + 驱动**:断言 delta == 0 **且** 结果 byte-equal 于第一段——两条断言各证一件事,delta==0 证「真的关掉了」,byte-equal 证「关掉之后走的解释器路径产出同样的正确结果」;
3. **重开 + 驱动**:断言 delta > 0 **且** 已有缓存指标(`TierStatsSnapshot().Promoted` 一类)相对第一段末尾不变——第二条把「重开不重编译」这个不在 issue 描述里的隐性约定钉死;如果实现把 tier off 意外拆掉了缓存表,Promoted 会掉到 0 再重新长回来,`==` 断言直接抓。

三段式的价值在把开关切换的语义拆成三个独立可验的正交断言,任一环回退都能被单点抓到,而不是靠一个「关掉之后结果对」的整体判断兜住。实例 `TestTierAdmin_KillSwitchRoutesToInterpreter` 完成 PR #115 的 `SetTierEnabled` 三向验证。

**(c) 非空载体 + 路径载体证据(空测类)**
加速路径 ≈1.0x 是红旗不是发现——默认「没走加速路径」。tier A vs B 基准必须**先证 tier B 真被执行**:把 kernel 包进可升层载体(内层函数反复调用而非顶层 vararg chunk),再读数。改 bench 时**先证路径,再读数**。

## 3. 反向侧解药 — 错误路径覆盖

全成功语料的 difftest / 差分测试套对错误路径**结构性失明**。harness 在出错时 `t.Fatalf` 则错误路径根本不被 byte-equal 校验。纪律:

- 任何差分测试 / 差分套必须**含一例黄金输出本身是错误消息**的语料(`error("..."): chunk:line: <msg>` 字面对比 + traceback byte-equal)。
- 全成功语料绿不构成错误路径正确性的证据——R3 出错点锚定 bug 在 V1-V13 全成功语料下全绿,加错误用例才暴露漂移。

## 4. 正向侧解药 — 覆盖度先验证再补

加 e2e 语料前,**先 grep 既有 oracle 测试套是否已覆盖**,避免冗余:

- 仓内 `test/luasuite/testdata/*.lua` 是 PUC 5.1 官方测试套移植,**覆盖度与官方一致**(vararg / coroutine / closure / errors / events / gc / pm / strings / 14 文件)。
- 加新 e2e 语料前先 `grep -l <feature> test/luasuite/testdata/`——若已含 happy path + 重要边界,**不写冗余语料**,直接以「luasuite 通过」作覆盖度证据(权威性高于手写)。
- 真要补**官方未覆盖的项目特化路径**(如 wangshu IC inline 命中,毒化助手等),`(a)/(b)` 双向断言路径走到。

**例**:VS0-e 子步 ⑥ task 描述列 11 条 vararg 语料,实际 luasuite/closure.lua 已含 `{coroutine.yield(unpack(arg[i]))}`(NeedsArg + vararg + 协程多值 yield/resume + 解包到 _G,5.1 作者本人写的最复杂组合)——不写冗余,直接以「luasuite 14 文件全 PASS」作 vararg 覆盖度证据。

### 4.1 一个 reported case 是接受面的一个采样,不是那个接受面本身

**适用场景**:修完一个 issue / crasher 报上来的那个具体输入之后,准备收工时。

**核心断言**:**一个 issue 报的往往是某个结构性原因的一个实例,不是那个原因本身。** issue
的措辞描述的是偶然触发它的那一个输入,不是产生它的那段代码的接受面;修完那一个输入,结构性
原因还在,只是暂时没有第二个输入撞上它——而 fuzz 迟早会撞。这是 §2/§3「枚举全部实现站点」
在**输入侧**的对偶:那边枚举「同一语义有几份实现」,这边枚举「同一段实现有几条到达通道」。

**实例(2026-07-28,#192)**:issue 报的是 `tonumber("nan(0)")` 返回 nil 而 PUC 返回 nan,
根因在 `internal/crescent/number.go` 的 strtod 前缀解析。修完之后按本节纪律枚举「哪些通道
会到达这个 parser」,查出 PUC 的 `luaB_tonumber` 在校验 base 范围之前、也在读 arg 1 之前
就判 `base == 10` 并路由到标准转换分支(与不给 base 完全同一条路),只有非 10 的 base 才走
`strtoul` 的逐字符解析;而 wangshu 把 base 10 送进了逐字符循环,于是**凡是那个循环表达不了的
写法全部返回 nil**——`tonumber("1.5",10)` / `("0x10",10)` / `("1e3",10)` / `("inf",10)` /
`("nan",10)` / `tonumber(1.5,10)`。一个结构性原因造出六条分歧,issue 里一条都没提;只修
reported case 会留五条活着。同一轮的扫描还额外带出一处活着的假豁免(见 §4.2)。反思实例
[[2026-07-28-four-diff-divergence-issues]] 教训 1。

**手法**:修完 reported case 后,**枚举所有到达同一段代码的通道并逐个验证它真的执行**——
参照实现有多条路由分支(`if base == 10` 这类早退分支)时逐条对齐;同一个 helper 被多个入口
调用时逐个入口构造输入。收工判据不是「reported case 现在过了」,是「到达这段代码的每条
通道都被验证过一次」。修完再补一轮定向 fuzz smoke(§5)。

### 4.2 「已登记为豁免」这类声明必须能指向执行它的代码

**适用场景**:读到或写下「这是已知差异 / 已登记豁免 / 已接受的偏离」这类注释时。

**核心断言**:**一个没有任何代码执行的「已豁免」声明比一个已知 bug 更糟,因为它读起来像
已经处理过了。** 读到它的人(包括写下它的人自己)会跳过这一处不再核对,而 fuzz 不会跳过。
物理基础与 §9 同族:绿灯 / skip / 一句注释本身都不携带「判据是否真的存在」的信息。

**实例(2026-07-28,`strtoul`)**:`internal/stdlib/stdlib.go` 的 `tonumber` base 分支里有
一句注释说负数回绕「已登记为 diff 豁免」——但仓库里没有任何代码实现那个豁免,分歧一直是
活的,nightly 随时可能把它开成 crasher。真值:PUC 直接调 C `strtoul`,负号在**无符号**算术
里取反(`tonumber("-7",8)` 得 2^64-7、`("-ff",16)` 得 2^64-255),溢出**饱和**到 `ULONG_MAX`
(20 个 `f` 配 base 16 得 2^64-1,而 wangshu 一直累加得 1.2e24)。这是有定义的 C,所以对齐
而不是跳过(判据见 [[cross-backend-semantic-fix-sweep]]「有定义 vs UB」)。

**手法**:写下「已知 / 已豁免 / 已接受」时,同时指出**哪一行代码或哪一个测试执行了它**;
指不出来就说明它只是一句安慰。反向动作:审计既有的这类注释时,第一步是去找它声称的执行体
(本仓的豁免执行体在 `test/difftest/corners_test.go::exemptions` 与
`internal/oracle/prelude.go` 的 sentinel 两处,注释应指向其中之一)。

### 4.3 期望与实现冲突时先取事实,别默认怀疑实现

**适用场景**:回归测试的期望值与实现的实际输出不符时,决定改哪一边之前。

**核心断言**:**「代码对、测试错」这个方向比反过来更危险。** 期望与实现不符时的默认反应是
怀疑实现,于是会去「修」一个本来正确的实现——修完测试绿了,产品行为被改坏了,而且这次改坏
有一条绿色测试替它背书。承 §4「覆盖度先验证再补」的同一元纪律(先取事实再动手),也是
[[2026-07-23-oracle-arg-coercion-round]] 教训 3(期望值以 oracle 实测为准)在**冲突时点**的
形式:那条讲写期望时别凭直觉,本条讲两边冲突时该先怀疑哪一边。

**实例(2026-07-28,三处)**:同一轮里三处期望写错,每一处都是代码对、测试错——① `% 5.0d`
的宽度(我以为像 NaN 渲染那样按声明宽度减一补齐,实际是完整 5 个字符,因为空格符号与
padding 在 buffer 里无法区分);② `insert(t,0,v)` 之后 `t[1]` 的值(我以为 pos 0 不动数组,
实际那次上移把 `"a"` 从 index 1 搬到 2);③ `strtoul` 取反的顺序(先取反后转 float 与先转
float 后取反结果不同,2^64-7 不是 float64 可表示的)。三处都靠跑 oracle / 查 C 取事实纠正。

**手法**:冲突时先去参照实现取事实——跑 oracle 差分、写一段 C 探测、读 `_lua515/` 对应
实现,三条任选;拿到事实之后再决定改哪一边。**不要从推理里取**,尤其不要用「另一处类似的
地方是这样的」做类比(上面 ① 就是拿 NaN 渲染的宽度规则去套 `%d`,两者不同族)。

**同一陷阱的另一个入口:issue 正文陈述的参照行为也不是已核实的事实**。issue 里写的是开
issue 的人当时的理解,混进一个别版本的行为很容易——尤其在 5.1/5.2 差异密集处。照它实现会
得到一个「按要求做完」但与参照实现不符的结果,而且有一条 issue 替它背书。实例
(2026-07-28,#199):issue 说 `io.write` 「返回文件句柄」,实测 5.1 的 `g_write` 压的是
**布尔**成功标志(`lua_pushboolean(L, status)`),句柄是 5.2+ 的行为。判据:修 issue 时对
它陈述的每一条参照行为都亲自跑一遍,**哪怕那个 issue 是自己开的**。反思实例
[[2026-07-28-issue197-199-stdlib-semantics]] 教训 5。

**第二个样本给出一条区分:集合式 issue 的错误率明显更高**(2026-07-28,#202)。同一条纪律
的第二次样本,错的比例更高——#202 七项里**三项不成立**(`coroutine.wrap` 的位置前缀、`%#g`
的指数交界、`math.deg` 的 1 ulp,实测都与 lua5.1 一致),另有两项要挪去新 issue,真缺陷
只有两项。区分在于**这个 issue 是怎么写出来的**:「顺手记下的一组」与「专门查证过的一条」在
issue 列表里长得一样,但前者每一行只经过「看起来不对」这一层,后者(如 #197)是**决定不硬凑、
把理由写清楚**才开的、每一句都经过一次判断。判据:集合式 issue(一个 issue 列 N 项、写的时候
是顺手记的)要**逐项重新核对**而不是逐项直接修,「这项不成立」与「这项修好了」一样是完成;
反过来,自己开这类 issue 时在正文里写明**未逐项核实**,别让它读起来像已经查证过。反思实例
[[2026-07-28-issue201-203-unpack-skip-thresholds]] 教训 2。

### 4.3b 做出来了但它破坏了别的东西 —— 撤回并写清「卡在哪」

**适用场景**:一个改动功能上已经做对,但让**既有的**压力测试 / 回归测试失败,而失败点在你不
熟悉的子系统契约上。

**核心断言**:这时「再调一会儿」的成本是**不可估的**,因为不知道要调什么;而撤回的成本是确定
的——一份「我做到哪、卡在哪、下一步要先搞清什么」的说明。功能正确的代码带着这种缺陷合进去比
不合更贵。这是 §4.3 的**时间对偶**:§4.3 管「动手前决定改哪一边」,本节管「做完之后发现它
破坏了别的东西」。

**实例(2026-07-28,#202 → #205)**:`io.stdout`/`stdin`/`stderr` 已经做成 file-handle
userdata + 共享 metatable(`write`/`close`),并为此补了三处 VM 缺口——`metaFieldOfValue` 不认
userdata、`indexWithMeta` 没有 userdata 分支、`getmetatable` 对 userdata 无条件返回 nil;补完
之后 `io.stdout:write()`、`:close()`、`getmetatable` 都对了。但随后
`TestGCStress_RandomScripts` 失败,创建句柄之后**一个** `collectgarbage("collect")` 就以
arena 索引越界 panic:句柄在仍然从 `io` 表可达的情况下被清扫,说明对象的构造方式没有正确进入
GC 的根 / 追踪路径。`internal/gc/mark.go` 确实会把 `OBJ_USERDATA` 追到它的 meta 与 env ref,
所以问题在构造侧不在收集器。这是运行时里第一个在 `__gc` finalizer 之外创建的 userdata,分配与
rooting 的约定得先搞清楚——整段撤回、开 #205。

**与「填 issue 而不是硬凑」是同一条纪律的两个时点**:那条在**动手前**(这个修法只在恰好如此的
情况下成立,见 [[2026-07-28-issue197-199-stdlib-semantics]] 教训 1),本条在**做完之后**。两条
共享同一个正向结算:#197 上一轮被主动填成 issue,那份「加个常数偏移只在中间恰好是 C 帧时才对」
的说明正是下一轮两层认识的入口。

**手法**:写下的「卡在哪」要具体到**哪个约定没搞清**(本例:非 finalizer 路径创建 userdata 的
分配与 rooting 约定),不要只写「GC 有问题」;把已经查明的证据一起写进去(哪几处 VM 缺口补了
什么、证据指向构造侧还是收集器侧),那部分下一轮不用重新发现。撤回的理由不是「太贵」——是这个
改动当前的形式会破坏某个不变式,而破坏原因是一份还没搞清楚的契约。反思实例
[[2026-07-28-issue201-203-unpack-skip-thresholds]] 教训 4。

**正向结算(2026-07-29,#205)**:下一轮真去做时,**根因只有一行**——原型直接调
`object.AllocUserdata`、跳过了 collector 的 `LinkSweep`,所以对象 header 里既没有颜色也没有 sweep
链、收集器根本看不见它;`internal/crescent/alloc.go` 里三个现成分配器每一个都写着
`AllocX` + `LinkSweep` + `AllocCharge` 三步。撤回时写下的那句「卡在**非 finalizer 路径创建
userdata 的分配与 rooting 约定**上」正好把下一轮的第一个动作指向了那三个分配器,而当时已经查明
的证据(三处 VM 缺口做了什么、`mark.go` 会追 `OBJ_USERDATA` 所以问题在构造侧)一条都不用重新
发现。这条印证「卡在哪」要具体到**哪个约定**:如果当时只写「GC 有问题」,下一轮还得从收集器
那边重新排查一遍。分配约定的判据已成文
[[design-claims-vs-codebase-physics]] §4.1,反思实例
[[2026-07-29-issue205-206-208-io-userdata-debug]] 教训 1。

## 5. 正向侧解药 — fuzz 探索空间维度动了就重探

**核心断言**：fuzz 的覆盖度不由语料和时长单独决定。**接受面（哪些形式能进被测路径）、硬件/OS、运行参数**都是探索空间的维度；任何一维变化 = 新一轮独立探索，「已跑过 N 分钟没抓到 → 就是安全」这个直觉在维度变动后立即作废。**任一维度变动后立刻跑一轮 60~120s 的相关 fuzz smoke，是廉价高产的默认动作**，不要归到「专门的 fuzz 里程碑」延后。

**Why**：fuzz 引擎的覆盖度依赖两类信号——（a）语料的语法结构多样性、（b）所走代码路径的分支覆盖反馈。改一件事就等于改了（b）：

- 扩接受面 = 改分支覆盖反馈的目标集（此前根本进不去的路径现在能进去），新放进来的形式**从未被 fuzz 变异走到过**；
- 换硬件/换架构 = 改 goroutine 调度时序 / 内存布局（ASLR）/ SIMD 指令流，从而改具体触发的分支组合；
- 换 fuzz 参数（`-parallel`、`-race`、`GOMAXPROCS`）= 改并发交织顺序。

维度动一格，「已探索过」这个状态就重置了。接受面扩一格尤其如此——那些形式在旧接受面下也可能潜伏 bug，但 fuzz 引擎从来没有理由把探索预算花在会带出它们的语料上。

**三个独立实例（已跨提升阈值）**：

- **[[2026-07-02-p4-beat-p3-opset-round]] 教训 4**：`opSupported` 扩面时三个 fuzz seed（`f7f0bb1a` arith NaN 别名 / `4b3d10ff` `insertNewKey` Brent relocate 无 BumpGen / `d9bce2e` LT/LE 缺 IsNumber guard）接连抓到 inline 快路径 bug，共同结构「inline 快路径省完整校验 → 常规 test 覆盖不到 → fuzz shape 混合扫出」；
- **[[2026-07-03-issue40-arm64-stopbleed-round]] 教训 5**：同一 fuzz corpus 在 Apple M5 Pro 上 9~80s 抓到 fatal stack overflow，同 corpus 在 amd64 侧多轮 120s/150s 跑过都没抓到；换硬件本身就是新一轮探索；
- **[[2026-07-03-issue45-issue39-round]] 教训 1**：LEN/MOD/POW 三个 op 进 `opSupported` 后 90s fuzz 立刻抓到 master 上潜伏的 PerOpCode 回放乱序 bug（seed `21c645c46a1268c6`，最小形式 `function f()return{0}end f(f())` → P4 报 `SETLIST: not a table` 而 P1 正常）；该 bug 在旧接受面下也存在，但之前多轮长时间 fuzz 都没抓到——不是 fuzz 不够长，是 LEN/MOD/POW 之前不在接受面里，fuzz 引擎的探索预算从没走到会带出这三个 op 的语料上。

**How to apply**：任何时候把新东西塞进 `opSupported`、换新硬件跑基线、改 fuzz 参数，把「跑一轮 60~120s 的相关 fuzz target」当作等价于 `go test` 的必经步骤。三个独立实例都在扩探索空间的第一次 fuzz 里就抓到既有 bug——这个动作的成本很低（1~2 分钟），命中率极高。

**与 §4 的关系**：§4 是「加语料前先 grep oracle 是否已覆盖」（避免冗余），本节是「探索空间维度变化后立刻跑一轮 fuzz」（避免维度盲区）；两者是正向侧的互补两面——前者管「不重复劳动」，后者管「维度动了就重探」。

## 6. 正向侧解药 — 多档机制叠加崩点的 bypass 探针根因 isolate

**适用场景**:机制叠加 N 档(本例 darwin/arm64 真机 JIT 执行 6 档:MAP_JIT mmap / W^X 翻面 `pthread_jit_write_protect_np` / `sys_icache_invalidate` / trampoline ABI / PAC 指针签名 / Hardened Runtime entitlement),崩点症状只有一种形式(一样的 SIGSEGV / SIGILL / wrong-result),症状无法区分是哪一档错位。PR comment / 调研报告把 N 个 hypothesis 并列时,**默认做法是穷举每条**(预估 1-2 天)——但若各档之间有干净的输入/输出 ABI 边界,**bypass 一档跳一层做 minimal payload 探针**可把 N 档收敛到一档,**20 行代码 + 5 分钟**。

**手法**:写 minimal payload 直接调下一层(末档)bypass 当前调试层(次末档),让 payload 经过相同的前 N-1 档但 bypass 次末档——若 payload 过,锅锁定次末档(本例 trampoline ABI / H1);若 payload 仍崩,继续 bypass 倒数第二档跳一层,直到 minimal payload 过,即可锁定崩点档位。每跳一层成本 ~5 分钟 / ~20 行代码,远快于穷举诊断每档。

**本会话证据(F3-#3b darwin/arm64 真机 execute 闭环)**:PR #27 留三 hypothesis(H1 trampoline ABI / H2 Apple Silicon PAC / H3 Hardened Runtime entitlement)。本机 M1 上写 `movz x0, #0x42 ; ret` 经 Go funcval 构造 BL 直接进 mmap 段(不经 `trampoline_arm64.s`),X0=0x42 成功返回 ⟹ MAP_JIT + W^X + icache + PAC + entitlement 全健康,**H2/H3 一次性排除,根因收敛到 H1**。后续 `lldb attach` 抓寄存器三相等指纹 `pc=lr=x19` 直接锁定 `STP (R19,R20), 0(RSP)` 覆盖 LR slot。

**与 §2/§3 反向侧解药的对偶关系**:反向侧解药(毒化助手 / 命中计数器 / 错误路径用例)是「证一条已知路径真被走到」,本节正向侧解药是「机制叠加 N 档时,证前 N-1 档健康以锁定崩点在第 N 档」——前者解决「绿色 ≠ 在测你以为在测的」,本节解决「N 档崩点症状无法直接归因」。

**判据**:**「档与档之间是否有干净的输入/输出 ABI 边界」**——若有(本例 trampoline 输入是 Go funcval / 输出是 ABI 标准 BL),探针成本低、收益高;若各档 ABI 互相纠缠,退回穷举。

**CI 形式盲区配套**(同会话第 8 实例新维度):多后端 / 多平台 CI 必须配真机 runner —— linux/arm64 QEMU + 字节级单测对比固定模板字节**不能替代真机 execute**(本会话:trampoline LR slot bug 实际 linux+darwin 一样的,但 linux/arm64 因 QEMU + 无 self-hosted runner 长期 latent,直到 darwin/arm64 macos-latest CI 真机 BL 跳段首次实测才触发)。「真机 execute 首次跑」是高风险事件,该 commit 应单独审查 —— 不是一次爆一个,而是一次爆一批(本会话:gate bug #1 修完开关 true 后,下游 #2/#3/#4 三个 emit bug 连环爆)。

## 7. 诊断侧对偶 — 退化归因前先证「被怪罪的路径」存在

**适用场景**:收到「某条路径导致 N 倍退化」类归因——不管来自 issue、用户描述,还是自己的第一直觉——准备动手写止血/修复计划前。

**核心断言**:**退化数字 ≠ 慢在你以为的路径**。这是 §1-§6 全部「测试侧」实例的对偶面:那些实例证明「路径真被走到」,本节证明「路径真的存在且被执行」。两者共享同一物理基础——输出(测试绿 / 性能慢)本身不携带路径信息,必须用独立于输出的证据反推路径。

**实例(issue #40 arm64 P4 止血轮)**:issue 把 arm64 P4 HeavyArith 慢 ~20x 归因于「PerOpCode head-op replay 逐 op 跨界路径」,并据此制定止血计划「收紧 arm64 升层接受面拒收算术密集形状」。但该路径在 arm64 二进制里**根本不存在**——`internal/gibbous/jit/peroptranslator/peropcode.go`/`translator.go` 都是 `wangshu_p4 && amd64`-only build tag,`internal/gibbous/jit/peroptranslator/register_arm64.go` 头注明确 arm64 是 native-only、无 replay fallback;arm64 有效接受面本来就在拒收算术形状,「收紧」会是无效操作(收紧一个已经在拒收的东西不改变任何数字)。真实根因由 force/auto 探针矩阵差分 + cpuprofile 在 1 小时内定位:forceAll retry window 让每回边都重跑全量后端分析,`recheckCompilabilityRuntime` 占 22.38% CPU、HeavyArith 1.5 GB/op。force/auto 不对称直接排除了「emit 慢」「接受面放坏形状」两类会同时影响双模式的假设,把嫌疑收窄到 force 专属机制。修复 `f921626`,反思 [[2026-07-03-issue40-arm64-stopbleed-round]]。

**手法**:动手修复前先用一个廉价动作验证该路径确实存在且会被执行——grep build tag、读函数头注、跑一次白盒探针(如 force-vs-auto 差分)。若验证失败(路径不存在,或存在但静态分析显示不该被触发),不要顺着错误归因去修,先重新定位。

**同源实例(P4 tier bug 的 code kind 分诊)**:P4 build 里可能是 PerOpCode(spec-template)、nativeCode(PJ10)、或 p4Code(compiler.go legacy shape-template)三大类,各有独立的 fast-path 假设与 deopt 路径。看到 P4 auto crasher 直接读某一种 code 的 emit 是猜。issue #177(2026-07-24,PR #178)的 FORPREP deopt slot 未恢复 bug 里,`NativeRunCount` 探针一次读数(auto 两个 run 都是 0)就把定位面从 3 大类代码收窄到 1 类(p4Code),省下猜 PJ10 native emit 的时间。可复用判据:碰上 P4 tier bug,先加或运行现有探针(`NativeRunCount` / `DispatchHelperCount` / PerOpCode debug 打印)分类走哪条 code kind,再深挖对应文件的 emit 代码;traceback 直觉(auto run 2 报某错)锁定「触发前提」但不锁定「代码路径」。反思 [[2026-07-24-p4-template-forprep-deopt-round]] 教训 1/3。

**与 §2/§3/§6 反向侧解药的对偶关系**:反向侧解药(毒化助手 / 命中计数器 / 错误路径用例 / bypass 探针)是「证一条已知路径真被走到」,本节是「证一条被归因的路径真的存在于当前二进制且可达」——前者面向**测试**,后者面向**诊断/归因**;两者互补,不是同一条纪律的重复。

### 7.1 复现侧 — 「没复现」不等于「问题不存在」,复现测试自己也要先证明路径

**适用场景**:收到「某机制 X 没生效」的指控(外部 review / issue / 用户报告),要写复现测试验证它是否属实时。

**核心断言**:复现测试与被指控路径是两回事——若复现测试实际走的是另一条路径,「budget 生效了 / 没复现」产出的是**伪证据**,会直接把一个真 bug 关掉。验证「机制 X 没生效」之前,必须先用白盒探针(`SafepointCalls` / `PromotionCount` 等)证明复现测试的执行路径就是被指控的那条路径。这是 §2「路径真被走到」在**复现方向**的应用:测试侧走错路径产出假绿,复现侧走错路径产出假「无 bug」——后者更危险,因为它以「验证过了」的姿态终结调查。

**实例(2026-07-18 review 修复轮,PR #161)**:外部 review 指控「P3 loop fuel 在预算开启后不重新武装」(`Safepoint` 无预算时把 fuel 字填成 `loopFuelUnlimited`,`SetStepBudget` 只改 Go 侧字段,先无预算跑热 P3 循环再开预算,新预算要等约十亿次 back-edge(回边)后才被查询)。第一版复现测试用 `local function spin()` + FORLOOP 循环 + `SetForceAllPromote`,结果 budget 居然生效报错——差点据此把真问题判为「不存在」。原因是那个循环写法的 back-edge 没有在段内跑,预算走的是解释器路径,测的根本不是被指控的路径。换成既有 `test/regression/loop_stepbudget_p3_test.go` 已用 `SafepointCalls > 0` 证明「back-edge 在段内跑」的写法后,bug 稳定复现(warm 后 `SetStepBudget(10)`,100 万次段内循环照常跑完、err=nil、safepoints-delta=0)。反思 [[2026-07-18-review-p3-loopfuel-rearm-round]]。

**手法**:写「验证机制没生效」的复现测试时,第一条断言不是「机制是否生效」,而是「本测试的执行路径就是被指控路径」的白盒证据(段内路径用 `SafepointCalls`、升层用 `PromotionCount`、快路径用命中计数器);优先复用仓内已经证明过路径的既有测试写法,而不是新造一个未证明的载体。复现失败时,先怀疑复现路径走错,再考虑「问题不存在」。

**同错误文本也可能是静默替身(issue #177,2026-07-24)**:验证 P4 deopt loop 是否保留 step budget / cancel context 时，auto 路径如果因促升阈值、shape matcher 或 timeout 先后关系变化而留在 P1，解释器也会产生相同的 `instruction budget exceeded` / `context canceled` 文本。只比错误文本会假绿。此类「自然促升后命中特定 deopt 分支」测试至少需要两层正交证据：`PromotionCount` 证明离开解释器，分支专用 hit counter（本例 `SpecForLoopDeoptHits`）证明命中被测 deopt；两个计数都应比较本次运行前后的 delta，避免其他 case 的累计命中污染证据。与 §2 的原则一致，但这里强调错误路径的最终输出完全相同，单一 probe 仍不足以排除静默替身。反思 [[2026-07-24-p4-template-forprep-deopt-round]] 教训 5。

**与 §7 主体的对偶关系**:§7 主体是「退化归因前先证被怪罪的路径存在」,本节是「否证指控前先证复现走的是被指控的路径」——一个防「错怪」,一个防「错放」。

### 7.2 归因来自仓库自己的注释时更难怀疑 —— 先用探针证明那条路径真的被执行

**适用场景**:调查一个缺陷时,在仓里读到一段**既有注释精确描述了手头的症状**,并且顺带给了解释。

**核心断言**:§7 主体讲的归因来自 issue / 外部 review / 自己的第一直觉;本节讲归因来自**仓库自己写下
的注释**——那是最难怀疑的一种,因为它可信度高、而且同时提供了「症状描述」与「解释」,读到它的瞬间搜索
就停了。但注释描述的是**那个机制**下的症状,而同一句错误消息可以由任何写坏那个数据的东西产生。

**实例(2026-08-04,#229)**:`internal/gibbous/jit/peroptranslator/translator.go` 里 `lastReplayPC`
降级机制的注释就写着 `NEWTABLE head + SETLIST → "SETLIST: not a table"`,与手上的症状逐字匹配,于是我
在那一层查了很久;**探针一行没打出来**,那条路径根本没被走到。真因在完全另一层(共享调用层的
`doReturn` 没恢复 caller 的 top,GC 把 caller 的活寄存器清成 nil)。`SETLIST: not a table` 只说明
SETLIST 读到的不是表,不说明是谁让它不是表。

**手法**:在动手读那段代码之前先加一行打印或一个计数器,跑一次 reproducer 看它是否被打到;症状相同不
等于路径相同。反思 [[2026-08-04-issue228-229-lazy-capture-and-nested-tailcall-top]] 教训 3。

### 7.3 抬错的位置不是缺陷的位置 —— 栈迹里缺少哪一层也是证据

**适用场景**:拿到一个「X 报错 / X 断言失败」形式的缺陷,准备去读 X 的实现时。

**核心断言**:报错点是唯一直接给到的坐标,从它开始读代码最自然;而对「读到坏数据」这一类缺陷,报错点
恰恰是**唯一确定没有 bug** 的地方——它正确地发现了数据不对。所以第一个问题不是「X 自己哪里写错了」,
而是**「X 读到的坏数据是谁写的」**。

**实例(2026-08-04,#229)**:P4 抬 `SETLIST: not a table` 而 P1 成功,看起来是「JIT 怎么编译 SETLIST」
的问题。在抬错点打栈迹拿到的是 `executeLoop -> doSetList`、**没有任何 JIT 帧**——这件事本身就是关键
信息:抬错的是普通解释器,破坏发生在更早的某个已升层调用里,搜索范围于是从「JIT 的 emit」翻转成
「谁在 SETLIST 之前动了那个寄存器」。

**两个可复用手法**:

- **具体坏值往往能直接指认写入者**。本轮抓到的 `tag=65528` 就是 `value.TagNil`,而 nil 不是任何一条
  指令的自然产物、是**清理动作**的值——线索直接指向 GC 的栈根扫描
  (`visitThreadValues` 把 `[top, size)` 当陈旧残留清成 nil),从那里回推就找到了没被恢复的 top。
  把坏值打出来,问「什么东西会写出这个值」;答案是某个清理动作时,就去查那个清理动作的边界条件。
- **栈迹里缺少某一层也是证据**。这与 §7 的分诊侧实例(`NativeRunCount` 探针一次读数把定位面从三类
  code kind 收窄到一类)是同一手法的反面:那里靠探针**确认**走了哪条路,这里靠栈迹**排除**了一整层。

反思 [[2026-08-04-issue228-229-lazy-capture-and-nested-tailcall-top]] 教训 4。

## 8. 度量侧 — 度量单位/时间窗口选错,把从没生效的优化读成「生效但太贵」

**适用场景**:读一个投机 / inline 快路径的性能「收益」数字时,或复查一个历史 perf 裁决(某优化「实测反而回归、故某平台 only / 故放弃」)时。

**核心断言**:**加速数字 ≠ 机制在稳态生效**。这是 §1-§7 全部实例的**度量侧**新维度——前面几节管「路径有没有被走到 / 被归因的路径存不存在」,本节管「读收益时的度量单位/时间窗口选没选对」。收益归因前必须先证:收益来自机制在**稳态**(跨 Run)生效,而不是单个 Run 内的一次假信号。

**度量教训**:任何投机 / inline 快路径的收益,度量单位必须是**跨 Run(稳态)的 exit-reason dispatch 计数**(或等价的稳态窗口指标),不是单个 Run 内的命中数。单 Run 命中数是「机制被触发过」的证据,不是「机制在稳态生效」的证据——两者的差就是一次假信号。选错度量单位会把「从没生效的优化」读成「生效但太贵」,再叠一个碰巧同向的噪声,就能推出完全错误的架构结论。

**实例(issue #67 amd64 NodeHit 跨 Run inline 补齐轮,PR #82)**:PR #74 当年给 GETTABLE/SETTABLE 常量字符串 key 的段内 NodeHit inline 观察到「命中 17 万次」并据此认定 inline 机制在工作,却同时看到 ~3% 回归,推出「inline 生效但太贵」的结论,进而定下 arm64-only 的架构裁决。真相是那「命中 17 万次」是**单个 Run 内**的假信号——最初的 inline 用一道**身份 guard**(把编译期 IC 记录的表指针烤进段,运行期比对当前表指针),而 n-body 的 `bodies[i]` 这类局部表**每个 Run 都在新的 arena 偏移重建**、promotion 只烤一次快照,身份 guard 跨 Run 100% 落空:段里每次都发射 guard、每次都不通过、每次都照样 exit-reason 往返到 `host.GetTable`/`host.SetTable`。inline 快路径**跨 Run 从没真正生效过**,那 ~3% 是「每次都白发一遍身份 guard 又白退一次 host」叠出来的净开销,不是 inline 本身的成本。真正的度量单位是**跨 Run 稳态的 exit-reason dispatch 计数**,而它从来没有下降。修正(commit 6a10721)把身份 guard 换成两道与表地址无关的 guard(hmask 边界 `Index <= hmask` + `nodeRef != 0`,一个 node 自己的 key 字段就能唯一标识 entry,身份比较对正确性并不必要),跨 Run 重建的同 shape 表也能真命中,amd64 n-body auto 从 43.5ms 降到 7.0ms(~6.2×)、跨 Run dispatch 从 875k 降到 50k,PR #74 的 arm64-only 裁决被证伪。反思 [[2026-07-08-issue67-amd64-nodehit-crossrun-round]]。

**解药**:`TestPJ10_TableNodeHit_CrossRunInline` 提升后的 kernel 每次调用重建一张同 shape 的表(新 arena 偏移,专门打身份 guard),断言**每 Run 的 exit-reason dispatch < 500**(身份 guard 时代 ~18000)。身份 guard 时代它稳在 ~18000、identity-free guard 时代降到 < 500,两条路径在这个断言上才可分辨。正确性 e2e 不能抓这个陷阱(inline 走没走都产出正确值,身份 guard 落空只是退 host 变慢不变错),单 Run 命中计数器又看着漂亮——缺的正交证据是「跨 Run 稳态的 dispatch 不下降」这一条。

**手法**:
- 读加速收益时,度量单位选**跨 Run 稳态的 dispatch 计数**,不是单 Run 命中数;看到「命中 N 次」漂亮但整体数字反而回归,默认怀疑「机制被触发过但跨 Run 稳态从没真生效」,加跨 Run dispatch 断言而不是信单 Run 命中计数器。
- 给 IC / inline 快路径写「把编译期身份快照烤进段」的 guard 前,先问「这个身份在段的存活窗口内会不会被跨 Run 重建打穿」(promotion 快照 vs 运行期重建,[[design-claims-vs-codebase-physics]] §5);若被测对象每 Run 在新 arena 偏移重建,身份 guard 会跨 Run 100% 落空,优先换成与对象地址无关的 key/shape 内容 guard。
- 遇到一个历史 perf 裁决(某优化「实测反而回归、故某平台 only / 故放弃」)时,先复查它当年的度量单位是不是单 Run 命中数 / 单窗口数,用跨 Run 稳态计数重测再决定要不要推翻。

**与 §2(b)/§7 的关系**:§2(b) 是「白盒命中计数器证路径被走到」,本节补的是「命中计数器要按稳态窗口读,不按单 Run 读」——同一个计数器,窗口选错就从「证据」变成「假信号」。§7 是**诊断侧**(退化归因前先证被怪罪的路径存在),本节是**度量侧**(收益归因前先证收益来自稳态生效);两者都是「表面数字不携带路径信息」的对偶面,一个管退化、一个管收益。

## 9. 差分 harness 侧 — 跳过判据的每个输入都必须两侧相同,包括它花掉的资源

**适用场景**:给差分 / 差分测试 harness 写任何**跳过判据 / 判定判据**时(「这类输入不可比,两侧一起跳过」),以及给这类 harness 加任何前置检查时。

**核心断言**:harness 判据分叉的后果不是误报,是**单侧 skip** —— 在「任一侧报 limit 即 skip」的框架下,判据只在一侧命中会把这条输入**连同同一次运行里所有真实差异**一起静默丢掉。误报会亮红灯,单侧 skip 什么都不说。这是本 guide 家族里最隐蔽的一档:§2/§3 那些失效模式至少留下「绿灯可疑」,这一档连可疑状态都不留。

**实例来源**:issue #184/#185 那一轮(2026-07-26,分支 43 个 commit、**26 轮独立盲审**,反思 [[2026-07-26-issue184-185-nan-coercion-noncomparable-round]])。同一类缺陷在那一轮里出现了十几次,每次换一个载体,而 26 轮里有 **13 轮抓到的缺陷是修上一轮时引入的**。**那一轮最后被整体放弃**(见 §9.0 与 [[2026-07-26-oracle-nan-render-redesign]]):差异改在**产生它的地方**消除,下面 §9.1–§9.5 的判据纪律因此只在**确实该在比较侧处理**的差异上才用得着 —— 先过 §9.0。

### 9.0 先判这个差异该在哪一侧消除 —— 判在渲染侧就不要设计判据

**这一节在 §9.1–§9.5 之前,因为它决定后面那套要不要用。** 那 31 轮的代价本身就是这条缺失的证据:整个过程里从来没有一个环节会问「这个差异该在哪一侧消除」,每一轮的动作都是把比较侧的判据做得更精细。

**两类平台差异,判法不同**:

| 差异来源 | 处理位置 | 本仓实例 |
|---|---|---|
| **同一个抽象值的不同书写方式** | 在**渲染处**消除 | NaN 符号位:同一个 NaN、两种合规拼写,IEEE 754 不赋那一位数值语义。最终在 `internal/oracle/lua515.c` 覆盖 `lua_number2str` + 对 `lstrlib.c` 局部 shadow `sprintf` 去掉符号 |
| **两侧本来就是不同的东西** | 在**比较时**归一或跳过 | 引用值地址(两个引擎堆布局本来不同,没有「正确值」可对齐 → 归一为 `0xADDR`);两侧独立选定的实现上限(`stack overflow`、`too many syntax levels`、200 local variables,触发点差几个输入,改 oracle 的常数去凑会让它不再是独立基准 → 跳过) |

**前者放到比较侧处理一定不收敛**,而且原因是**可证的**,不是「暂时没找到好判据」:同一个抽象值的不同书写会流入普通数据 —— 那个符号字节一进字符串就是普通字节,`#`、`==`、`string.sub`、`..`、算术能把它搬到任何地方,`string.len(0/0)` 是 4 对 3、`string.len(0/0)*100` 是 400 对 300,输出里连一个 nan 字节都不剩。没有可锚定的标记,任何下游规则都必须读一个两侧不相等的量,所以都做不到对称(§9.1 那五个载体是这一条的五次具体表现)。后者不会:地址永远带 `table: 0x` 前缀,护栏永远以一条固定错误消息出现,标记是**结构性的**、不会被算术冲掉。

**判在渲染侧时,还要确认要改的那一侧是可控的** —— 这一步是那 31 轮真正漏掉的东西。当时对 oracle 的心智模型是「外部真值」,这在**语义**上对(它就是要对齐的真值),在**物理**上错(它是仓库自己 vendor 进 `internal/oracle/_lua515/`、自己用 cgo 编译进测试二进制的一个目标,而且已经为确定性 stub 掉了 `os.time`/`math.random`/`pairs` 顺序)。模型错在物理层,于是把「改 oracle 的渲染」这一整类方案排除在视野之外,**而这一步排除没有留下任何痕迹**。

所以动手之前先查参照实现的**供给链**:源码在哪、谁编译的、编译时已经打过什么补丁、改它会不会破坏它作为基准的意义。最后一问在本仓的答案是「不会」,而且**先例就在源码里**(确定性 stub 三处)。

**配套的两条纪律**:

- **「我实测过某条路不行」保护的是那一个选项,不保护由它推出的范围。** 那一轮实测证明了「改 wangshu 的值层不可行」(`value.NumberValue` 把所有 NaN 规范化到单一 `canonNaN`,符号位在值层就丢;x86 上每个 NaN 运算产出的位模式与 `TagNil` 逐位相同),两次实测都做对了、结论也对 —— 错的是把它悄悄外推成「只能在 harness 侧容忍」。**写下否证结论时同时写下它没有排除什么**:① 被否证的是哪个**具体**选项(「改 wangshu 的值层」,不是「改产品侧」);② 与它并列的其他选项各是什么状态(未查证 / 已查证不可行 / **未想到** —— 最后这一格写不出来,正是要靠列清单逼出来的)。
- **反复出现 edge case 是选址错误的信号,不是实现不完善的信号。** 缺陷密度长期不下降本身就是判据,不需要等到能说清哪里错了。这条与 §9.4 是**两个尺度**:§9.4 管「一个机制内部该不该继续修」(第三版还在出同类缺陷 = 抽象选错),本节管「这个机制装在哪一层」。那一轮第 5 轮起 §9.4 是真的用上了、也确实一步收敛了那一处,但它的作用域是「这个机制」,所以用它只会让机制修得更好,不会让人去问装错了层。**作用域比判据本身更要紧。**

**顺序**:先过 §9.0 判位置 → 判在渲染侧就去改渲染(并确认那一侧可控)→ 只有判在比较侧,才轮到 §9.1 之后的判据对称性纪律。顺序反了的代价是那 31 轮。

选对层以后还要证明 hook 只覆盖它声称的入口。拦截一个底层通用函数时,不能用输出内容
反推调用来源:本仓为归一 oracle 的 NaN 渲染而局部 shadow `lstrlib.c` 的 `sprintf`,初版
只认 buffer 里的 NaN 文本,却漏了同一个函数也承载 `%s` / `%c`,于是用户字符串 `"-nan"`
也被改写。修法是先按 conversion verb 把入口限定为会自行产生浮点 NaN 的
`%e/%E/%f/%g/%G`,再检查内容。验证也不能只放 reported case:格式器至少把 verb、flag、
width、precision、参数类别作为独立维度做笛卡尔扫描,并放入「内容相同但来源不同」的负例;
本轮 26160 个组合又抓到左对齐宽字段因 scratch buffer 定长顺序错误而漏归一。**选址正确只
回答“在哪修”,入口枚举与维度扫描才回答“有没有误伤、有没有漏掉”。**

#### 9.0a 归一化只能救「被打印出来」的差异,救不了「被测量」的差异(2026-08-05,#233)

§9.0 那张表把差异分成两类,**判去比较侧的那一类里仍然可能有一个子量必须在渲染侧对齐**——这一格是那张
表原先没有的。

**核心断言**:**归一化的前提是那个值在输出里以它自己的形式出现;脚本可以不打印它而是测量它,而测量
结果是一个普通数字、与那个值再无字面联系。** 这时归一化在物理上到不了——它拿到的字节流里已经只有一个
数字。

**实例(#233)**:引用值地址在那张表里一直被列在「两侧本来就是不同的东西」那一格(没有正确值可对齐 →
归一为 `0xADDR`),这对地址的**值**是对的。但 `t={0}print(#tostring(t))` 量的是它的**宽度**:PUC 的 `%p`
在本平台给 12 个十六进制位、`#tostring(t)` 是 21,望舒的 `0x%08x` 给 8 位、是 17,而 `NormalizeOutput`
拿到的字节流里只有 `21` 和 `17`。这与 [[2026-07-26-oracle-nan-render-redesign]] 那一轮的
`string.len(0/0)`(4 对 3)是**完全一样的机制**。修法因此回到渲染处:`internal/oracle/prelude.go` 的
prelude 包一层 `tostring`,把 PUC 自己的地址渲染成望舒的 8 位宽度,与 NaN 符号位是同一个选择;望舒这一
侧的宽度于是成为一份契约,由 `fuzz_234_test.go::TestAddressLengthIsComparable` 钉住(`#tostring({})`
是 17、`#tostring(print)` 是 20)。**归一化管值、渲染处管宽度,两件事都要做。**

**判据**:给差分 harness 加归一化时,问一句「脚本能不能**不打印它、而是测量它**」——能的话(`#`、
`string.len`、比较、当表键、算术),归一化就不是完整的修法,必须回到渲染处去消除那个会被测量的子量。
自查办法:为你要归一的那个量写一个**不打印它**的脚本(`print(#tostring(t))` 这种);写得出来就说明
归一化有洞。反思 [[2026-08-05-issue232-234-address-width-and-gsub-escape]] 教训 2。

### 4.7b 一段共享代码服务语义不同的多条路径时，「在这里判断」本身就是缺陷

2026-07-28 那轮同一处帧计数错了六次，根因是「这是一个 host 边界吗」**对每种调用边界的答案不同**，
而条件写在了共享的被调者里：`pcall` / sort 比较器 / gsub 替换有真实 C 帧要算，TFORLOOP 的迭代器
派发与元方法派发都不插帧不能算，但元方法处理器**内部**的调用又要算。放在共享处就让迭代器多一级，
从共享处拿掉又让元方法内的 `pcall` 少一级。拆成三个入口之后才停止反复。

判据：给一个共享 helper 加条件之前，列出它**所有**调用方并逐个问「这个条件对它成立吗」；只要有
一个不成立，这个条件就属于调用方而不属于 helper。

### 4.7b2 两个耦合的决定不能分别评估

2026-07-28 那轮 `os.time` 的 `isdst` 有两个决定互相掩盖：搜索上限（447 还是 381 步）与 equal-offset
邻居的 delta（默认一小时还是 0）。任一单独改动都实测「持平到略差」，于是被当成否决；两个一起改才从
1% 残差降到**零**（4400 例与 6600 例留出集都是零）。

判据：一个改动实测「持平到略差」时，先问「它是不是被另一个还没改对的决定掩盖着」，尤其当两者读同一
段逻辑。把这种耦合写进注释——否则下一轮会把其中一个当成已经否决过的方案，而那正是这一轮多花了一轮
的原因。

### 4.7c 手挑的用例集不能用来估计错误率

在 81 个手挑用例上量出 97.5% 一致，机械枚举 4400 例量出的真实数字是 75%。差距不是审计更细心，
而是那 81 个用例来自历次报告里点到的时区——**构造上就是有偏的**：它们恰好是已经被发现过的那些。

判据：要声称「基本一致」「只剩边缘情况」时，样本必须是机械枚举出来的（全时区 × 全年份 × 全参数
组合），不能是从历次报告里攒出来的。攒出来的集合只能当回归用，不能当估计用。

### 4.7d 别人给的数字要自己复现，连同它的对照项一起量

2026-07-28 那轮审计报「与 C 参照程序比错 25%」，我据此准备撤掉整个 `os.time` 的 `isdst`。自己写了
参照程序复现后，25% 确认了——但同时量出「完全不实现是 2156 例不一致、现在这版是 44 例」，结论因此
反过来：这版明显好于不实现，该留。

判据：一个数字将要驱动「保留还是撤掉」这类取舍时，先用自己的手段量一遍**它和它的对照项**。只量被
批评的那一侧，会漏掉「替代方案更差」这种情况。附带收获是有了可测量的 harness 之后，主因往往一眼可
见——那一轮 1120 例不一致里每一例都是同一个症状，而它不是审计报的两条性质中的任何一条。

### 4.8 状态类缺陷对单次调用不可见

泄漏、漂移、累积这一类缺陷，**在构造上**就躲得过只调用一次的探针。2026-07-28 那轮的 host 帧
计数器每次边界都永久 +1，第一次 `error(msg, level>=2)` 是对的、之后每次多走一步；两轮独立审计
与我自己 96 个形式的组合清扫全都没抓到，因为那 96 个全是**单次**调用。第三轮把同一形式在一个
chunk 里重复三次，立刻复现。

判据：验证任何跨调用保存的状态时，把「同一形式重复 N 次、检查第 N 次」当成**独立**的覆盖维度，
而不是把 N 个不同形式各跑一次。前者抓泄漏，后者抓不到。

### 4.9 「这个上限远高于任何真实用法」必须用构造出的反例检验

写下一个宽度、阈值或容量够用的理由时，去**构造**一个超过它的输入；构造不出来才算证明。
2026-07-28 那轮我把 host 帧计数写成 4 位，理由是「reentry 上限远低于 15」——那句话从来没被构造
检验过，而它是假的：嵌套 15 层以上的 host 边界毫无阻碍，20 层尾调用链三行就能写出来。

同时注意构造本身可能先撞上**别的**限制。我第一次验证时把 50 层 pcall 写成字面量嵌套，它先撞
parser 的 syntax-level 上限，于是我量到「没有分歧」并差点据此驳回——深度必须在运行时构造。探针
撞到无关限制时得到的「一致」是探针失效，不是结论。

### 4.4 与参照实现直接比对，往比再开一轮审计划算

审计**连续两轮**都在你的清扫覆盖面之外找到同类问题时，问题不在"扫得不够多"，而在**清扫的
生成方式不够系统**——人工列清单必然漏掉自己没想到的函数。改成机械生成（函数 × 参数位置 ×
边界值）并直接跑参照实现：2026-07-28 那轮用 913 个探针对系统 `lua5.1` 比对，找到 5 个六轮
审计都漏掉的分歧，成本是一轮审计的几十分之一。

它还能到达**差分 harness 结构上到不了的地方**：被 fuzz prelude stub 掉的函数（`os.*`、
`math.random`）在差分里永远是两侧相同，只有直接比对才看得见。

前提是先把参照实现与本仓的**已知有意差异**过滤干净（那轮是系统 `lua5.1` 未打补丁、会渲染
NaN 符号），否则噪声淹掉信号——过滤写对之前我一度以为有十几个新分歧。

### 4.5 给上限定数值之前先测参照实现的代价

规则是"拒绝不可中断的挂起"时，参照实现能在几秒内完成的输入就不该被拒绝。2026-07-28 那轮
`tableInsertShiftCap` 改了三次（2^22 → 2^26 → 2^27），每次都是**估**代价：实测 2^22 是
56ms、2^24 是 0.21s、2^26 是 5.5s。前两次都把参照实现能完成的工作拒绝掉了。

判据：写下"这个上限足够宽"之前，跑一遍参照实现在上限附近的耗时，把数字写进注释。

**量参照实现的边界时，让决定它的那个量变化再测一遍**（2026-07-28，#201）。`unpack` 的上限
不是固定的 8000 而是 `LUAI_MAXCSTACK` **减去参数个数**：PUC 的 `luaB_unpack` 调
`lua_checkstack(L, n)`，它的拒绝条件里有 `(L->top - L->base + size) > LUAI_MAXCSTACK`，而对
一个 C 函数来说 `L->top - L->base` 就是参数个数。三参数调用接受 7997、拒绝 7998，而**阈值随
参数个数变化**正是排除「硬编码 7997」这个错误结论的那一条证据——只测三参数那一种写法时，
7997 与 `8000 - nargs` 在数据上完全不可区分，会写下一个在两参数调用上就错的常数。判据：测出
一个边界值之后，先问「什么量可能参与决定它」，把那个量变化一格再测；四种调用环境（直接
pcall、包在 Lua 函数里、有额外活跃局部变量、经 select）阈值一致才算定准。

> **同一个函数上的第二次(2026-08-11,#244)**:这条判据在 `unpack` 上又被需要了一次,而这次漏掉的量是
> `e`(默认 `#t`)。#244 量的是 PUC 的**崩溃窗口**而不是上限:实测得到「只有 `-2147483648` 与
> `-2147483647` 会崩」,但那批测量全部取自 `unpack({}, i)` 这一种写法,而崩溃条件
> `(e - i + 1) > INT_MAX` 里带着 `e`,所以 `unpack({1,2,3},-2147483646)` 同样让 PUC 段错误、却落在守卫
> 之外。**判据在同一个函数上两次被需要,说明它最容易被跳过的情形是:边界看起来只与一个显式参数有关。**
> 补一条自查:写下那个边界之前,先写出决定它的**表达式**——表达式里的每一项都是一个取样维度。详见
> [[cross-backend-semantic-fix-sweep]] 第八个刻度与反思
> [[2026-08-11-issue244-oracle-segv-unpack-int32]] 教训 2。

### 4.5b 一个上限必须区分它约束的是正确性还是资源

§4.5 讲一个阈值的数值怎么定，本节讲**一个数值不该同时服务两个目的**。

**核心断言**：一个阈值的正确数值是从**它防的东西**推出来的。防「与参照实现分歧」时，数值由
参照实现的能力决定（§4.5）；防「harness 自己付不起」时，数值由 harness 的运行方式决定（并行
度、per-input 看门狗、arena 上限）。这两个量之间没有任何关系，让它们相等通常只是因为一开始
只想到了一个用途。**两个阈值服务不同目的时，把它们绑成同一个数迟早会在其中一边错。**

**实例（2026-07-28，#203）**：`table.insert` 的移位跨度上限被当成一个数用了三轮。产品侧要它
**宽**——在它之下 wangshu 必须做这个移位，否则就是拒绝参照实现能完成的工作（上一轮四轮审计都
在反对这件事）；harness 侧要它**窄**——并行 corpus 重放里放不下一条耗数秒的 seed，而这一写法
已经被 nightly 开成三个 crasher issue。两个方向矛盾，而它们读同一个常数。拆开之后：产品侧
`tableInsertShiftCap` 留在 2^27（正确性边界），harness 侧的 skip
（`internal/oracle/prelude.go`）降到 2^20（资源），`test/regression/insert_shift_cost_test.go`
仍然串行跑一次真实的 100M 元素移位，所以「这个工作确实被完成而不是被上限拒绝」没有丢覆盖。

**与 §9.2 的关系**：§9.2 说「判据自身消耗的资源也是它的输入」，本节是它的对偶——资源与正确性
是**两个**判据，不该共用一个常数。**与「skip 要与产品规则逐字对应」不冲突**：那条
（[[2026-07-28-four-diff-divergence-issues]] 教训 7）管**判据的形状**（两边都得按「低于
index 1 的距离」算，一边按元素个数就会让 skip 遮住产品的错），本节管**阈值的数值**。形状一致、
数值分开。反过来读处置侧：同一写法反复被 fuzzer 开成 issue 时该动的是被接受的区间，见
[[unreproducible-crasher-triage]]「同一写法第三次被开成 issue 时」。

**手法**：为一个阈值命名时写清它防的是什么；如果两个地方读同一个常数而防的东西不同，那就该是
两个常数，并在两处注释里各写清自己那一侧的理由。反思实例
[[2026-07-28-issue201-203-unpack-skip-thresholds]] 教训 3。

### 4.5c 钉住两个阈值的关系要按行为断言，不要复制字面量

§4.5b 讲两个数为什么该分开，本节讲**分开之后怎么把这件事钉住**。

**核心断言**：一个「关系」的断言不该建立在关系两端的**副本**上。测试里写死 `1<<20` / `1<<27`
时，比对的是自己的两个副本而不是那两个真实的阈值——任一侧改了而测试里的字面量没跟着改，测试
照旧是绿的。**阈值之间的区间是可以直接观察的**：落在里面的输入应该被执行、落在外面的应该被
拒绝或跳过；用输入的**行为**表达这个关系，无论哪一侧变了、只要关系被破坏测试就红，而关系没被
破坏时（例如两个数一起等比例调整）测试也不该红。

**实例（2026-07-29，#209）**：`tableInsertShiftCap`（`internal/stdlib`，2^27）与 harness skip
（`internal/oracle/prelude.go` 的 prelude Lua 源码字面量，2^20）**都不导出、也不在同一个包**，
测试根本拿不到它们。`test/regression/insert_shift_cost_test.go::TestInsertShiftThresholdsStayDistinct`
因此断的是**区间里一个输入的行为**：`table.insert(t,-2097151,"")` 的跨度约 2M，在 skip 之上、在
产品上限之下，于是它①必须被产品**执行**（`prog.Run` 不返错）②必须**便宜**（断言耗时上界，因为
skip 的取值本来就建立在「刚超过 skip 的这一段是便宜的」这个假设上）。

**两个方向都要断**：只断①的话，两个数被合到高的那个（2^27）仍然全绿——那一段照旧被执行，只是
它重新进入了并行 corpus 重放；只断②的话，合到低的那个（2^20）也全绿——那一段被产品拒绝，`Run`
立刻返错，当然便宜。

> **修正（同轮审计）**：我原先以为「一个测试里断两个方向」就够了，实测不够。两个阈值在**两个包**里，而断言只能看见自己 import 的那一侧：`test/regression` 不 import `internal/oracle`，所以把 skip 从 2^20 提到 2^27 之后它仍然全绿，而 corpus 里的 seed 各自从 0.00 秒回到 3 秒。**判据要加一句：先问「这个断言能不能看见它要保护的那个量」**——看不见就得在那个量所在的包里再写一个。现在产品侧与 harness 侧各有一个执行体。

**手法**：判据 = 要钉住两个阈值的关系而它们不导出 / 不在同一个包时，把**区间**做成用例，用
「这个输入应该被执行 / 应该被拒绝」表达，不要复制数字；并且把区间的两个方向都断上。这与 §4.2
（声明要能指向执行它的代码）是同一条纪律在**阈值**上的形式，处置侧的对应条目见
[[unreproducible-crasher-triage]]「改完区间之后要问有什么东西固定住这个改动吗」。反思实例
[[2026-07-29-issue209-stale-crasher-threshold-pin]] 教训 2。

### 4.5d 一个资源预算量的是什么：单位要与真实成本同量纲，同类资源只该有一个计量器

> **补记(2026-08-03 第二次审计)**:记账的**时机**和单位一样要紧。给 `string.gsub` 记账时我在循环外按「每个字节位置都匹配」估了个上界,结果两个方向都错:一个只匹配一次的模板替换被记成约 67 MB 而被拒(lua5.1 正常返回),而替换是函数/表时长度无从预知、回退只记主串,一个每次返回 200 KB 的闭包跑了 15 秒都没触发。**精确值就在循环内部每次匹配处**。判据:先问「这个量什么时候变成已知」,在它已知的地方记,不要在入口按最坏情况记。另外——在「超限即跳过」的 harness 里,**多收费不是保守选择**,它静默删掉合法输入的覆盖,而且比少收费更难发现,因为它表现为测试通过。

§4.5/§4.5b/§4.5c 讲一个阈值的**数值**怎么定、两个数为什么该分开、分开之后怎么钉住；本节讲那个数
**量的是什么**，以及同一类资源该有几个计量器。两条判据，都来自 2026-08-03 那轮（#222）。

**（a）计量单位要与真实成本同量纲，别用「个数」代替「字节」。** 一个批量操作的「规模」往往有好几个
可读的量（元素个数、循环次数、输入长度、产出长度），它们只有一个与真实代价成正比，而**最容易拿到的
那个常常不是它**。挑错量的后果不是记账偏小一点，而是**整类输入完全绕过记账**：只要那个量被别的东西
界住，预算就再也拦不住成本沿另一个维度增长。

实例：`table.concat` 的遍历本来就被表的长度界住，所以按元素个数看它永远便宜——**256 个元素**听起来
微不足道，而每个元素 2 KiB 时实际要 **53 秒**（在 1<<20 step budget 内、且完全没有触发预算）。代价
是拼出来的字节数，不是走过的元素个数。这与 §8「度量单位选错，把从没生效的优化读成生效但太贵」是同一
个错误在**限流侧**的形式：那条讲读性能数字时选错单位，本条讲写预算时选错单位。

**手法**：判据 = 给一个批量操作定预算前，先写下一句「这次调用的代价 ≈ 什么的函数」，再让记账直接读
那个量。自查办法：构造一个让「便宜的那个量」保持很小、而「真实成本的那个量」推到很大的输入（256 个
2 KiB 的元素就是这样构造的），看记账有没有跟着涨；不涨就说明量错了东西。

**（b）新增一个资源判据要复用既有的那个计量器，而不是自建第二个阈值。** 如果几个算子各自定一个阈值，
它们迟早会互相漂移——几个数字分别在几次不同的调优里被改动，而**哪个先触发**会变得难以预测，于是
「这个脚本为什么超预算」不再有单一答案。更要紧的是预算本身是一个**可加的量**，多个阈值表达不了
「几种批量操作叠加起来超了」这件事，而这恰恰是 fuzzer 会构造的写法。

实例：`string.rep` / `string.format` / `table.concat` 三个函数都走 CONCAT 那一个 `chargeBulkWork`
（为此把它导出成 `internal/crescent/state.go::ChargeBulkWork`），于是「批量工作」在 step budget 里
只有一个定义。**这与 §4.5b 互补**：那条讲两个**目的不同**的阈值不该共用一个数（正确性 vs 资源），
本条讲两个**目的相同**的判据不该拆成两个数。两条一起就是「按目的分组，同组共用一个计量器」。

**手法**：判据 = 给一个新算子加资源限制前，先 grep 同一类资源已经有没有计量器；有就接上去（必要时
把它导出），不要在新算子里写一个自己的阈值。自查办法：问「这个新限制与既有那个，同时被一个脚本触碰
时谁先触发」——答不出来就说明这里应该只有一个计量器。

处置侧的对应条目见 [[unreproducible-crasher-triage]]「那三个候选算子已结算」。反思实例
[[2026-08-03-issue221-222-bulk-builder-budget]] 教训 2 / 教训 3。

### 4.6 为某个 bug 加的 skip 必须随那个 bug 一起撤掉

**适用场景**:修完一个曾经在 harness 里被 skip / 豁免绕过的 bug 之后。

**核心断言**:**一个还活着的 skip 与一句没有执行体的豁免声明,在阅读上是一样的**——都表示
「这里不用看了」,区别只是前者真的在运行,把一整类输入静默挡在比对之外。这是 §4.2 的**时间
对偶**:§4.2 讲「声明要有执行体」,本节讲「执行体要随 bug 撤」。

**实例(2026-07-28,#197)**:`fuzz_oracle_test.go` 为 `error(msg, level)` 选帧错误加过一条
skip,覆盖**任何提到 error 第二参数的输入**(`errorLevelAtLeastTwo`,按源码文本判定、故意
写宽)。#197 修好后如果不撤,整个 level ≥ 2 的区间就一直不参与比对;撤掉后 24 种 error level
写法**零 skip** 参与比对,只剩「非有限 / 超出 int64 的 level」跳过(那是 `luaL_checkint`
窄化的真 UB,属 [[cross-backend-semantic-fix-sweep]] 的 UB 格)。反思实例
[[2026-07-28-issue197-199-stdlib-semantics]] 教训 2。

**手法**:修完一个 bug,`grep` 一遍为它加过的 skip / 豁免 / 已知边界条目,逐条撤或收窄,并
跑一遍确认相应用例现在**零 skip** 参与比对(不是「测试绿了」——差分 harness 里 skip 也是
绿的,见 §9.6)。收窄剩下的那条要**连名字一起改**:旧名字描述的是已经不存在的理由,留着
会把下一个读它的人引回错的模型(本轮 `errorLevelAtLeastTwo` → `errorLevelUBRange`)。

### 4.7 「不给可选参数」是一个独立的覆盖维度

**适用场景**:测格式化 / 转换 / 解析类接口(`string.format`、`tonumber`、`os.date` 一类)。

**核心断言**:**参照实现的默认值差异只在「没有显式参数」时暴露**。「省略参数时取什么」是
参照实现里一段独立的逻辑,与「给了参数怎么用」不是同一段代码;按参数值扫矩阵(各种精度 ×
各种值)会把默认路径整条漏掉,因为矩阵的每一格都带着参数。

**实例(2026-07-28,#198)**:`string.format("%g", 1/3)` 得 `0.3333333333333333`、C 得
`0.333333`——C 的 `%g` 默认精度是 6,Go 的默认是「能往返的最短表示」。**显式精度一直是对
的**,`%e`/`%f` 的默认也一直是对的(Go 对这两个的默认与 C 一样是 6),只有 `%g` 的默认不同。
这一条在任何带精度的用例里都看不见。反思实例
[[2026-07-28-issue197-199-stdlib-semantics]] 教训 3。

**手法**:把「不给可选参数」当成与「参数取各种值」并列的一个维度扫。对每个可选参数问一句
「省略它时参照实现取什么值,那个取值是谁定的」——是 Lua 层写死的常量(照抄)、还是转发给
宿主标准库的默认(那就要查那一侧的默认,两个标准库的默认经常不同)。

### 4.9x 断言「两者一致」时要用能让两者不同的输入

2026-08-02 那轮我有一个测试断言「方法调用的行号本来就与 PUC 一致」,用的是 `t\n:nope()`——那里方法名
那一行与参数列表那一行**恰好重合**,所以测试通过,而性质其实不成立(`t:nope\n{}` 报第 2 行而 lua5.1 报
第 3 行)。错误的结论还被写进了两份文档。

判据:为「X 与 Y 一致」写测试时,先构造一个**能让 X 与 Y 不同**的输入;构造不出来就说明这个测试没有
验证任何东西。与 §4.7c(手挑用例不能用来估计错误率)同源——都是样本恰好落在性质成立的那一侧。

### 4.10a 测试自己不能依赖环境里的 stdin / tty / 工作目录

2026-07-29 那轮最重的一条发现在**我自己写的回归测试**里：它依赖环境里的 stdin，于是编译出的二进制
在终端下**永久阻塞**、在管道下**断言失败**，而且断言的正好与它声称要固定的性质相反。它在 `go test`
下通过，只因为 `go test` 恰好接了 `/dev/null`，而 `make test` 是直接跑二进制的。

判据：写会读进程级输入的测试时，自己把它重定向掉（`os.Stdin = devnull`，cleanup 里还原），并且
**用编译出的二进制在有真实 stdin 的情况下跑一遍**。只在 `go test` 下验证，会同时漏掉「阻塞」与
「断言方向反了」这两种失效。

### 4.10b 同族缺陷扫描要把本次新增的函数也算进同族

那一轮我给 `string.byte` 补了 `lua_checkstack` 上限，还顺着 vendored 源码扫了其余调用点——却漏了
**同一个 commit 里我自己新写的** `io.read`，它有完全一样的缺口。旧代码里 grep 不到它，因为它还不
存在。

判据：做同族扫描时，把本次改动**新增**的函数一起列进去；「扫一遍现有调用点」这个动作天然看不见
自己正在添加的那一个。

### 4.10c 从别处搬来的判据要重新验证它在新位置成立

「下标 0 就是 main chunk」在 `buildTraceback` 里是对的——它只给栈底贴标签；搬到 `debug.getinfo`
就错了——它要指名函数，而尾调用会把普通函数放到下标 0。同一份数据，两个调用方问的是不同的问题。

判据：复用一个条件时先问「原来的调用方问的是同一个问题吗」。不是同一个问题，判据就不能共用，哪怕
它读的是同一个字段。

### 4.10d 一个标识符「有没有被用到」是按构建组合判定的（2026-08-04）

把 fuzz harness 里重复四次的 `SetStepBudget(1 << 20)` 提成常量 `fuzzbudget.Steps` 时，第一版写在一个
**没有 build tag** 的文件里，golangci-lint 报 unused——因为消费它的两个文件都在
`(wangshu_p3 || wangshu_p4) && wangshu_profile` 之后，**默认构建看不见它们**，于是默认构建里这个常量
确实没有任何使用者。

**Why**：「有没有被用到」不是全仓库范围的事实，而是**每一种构建组合各自的事实**。tag 化的文件在某些
组合里整个不存在，所以只要一个标识符的可见范围比它的消费方**宽**，就一定存在一种组合让它孤立。本仓
的组合不少（默认 / p3+profile / p4+profile / oracle cgo / trace），一次只跑其中一种 vet 不足以覆盖。
这与 §4.10a/b/c 同族：都是「测试或其支撑代码自己的前提，在另一个位置不成立」。

**判据**：新增一个只被 tag 化文件使用的标识符时，给它**同样的** tag；并且用**消费方的每一种 tag
组合**各 vet 一次。自查办法：写完之后问「哪一种构建组合里这个标识符会没有使用者」——答得出来说明 tag
还不对，答不出来就把组合列全跑一遍。加上相同 tag 之后五种构建组合都干净。反思实例
[[2026-08-04-issue224-225-watchdog-margin]] 教训 4。

### 4.10 测试驱动没提供的进程级输入,测出来的「不工作」是假的

**适用场景**:被测功能依赖 stdin / tty / 信号 / 工作目录 / 环境变量这类**进程级环境**时。

**核心断言**:探针测的是「测试驱动给了没有」而不是「代码对不对」。这与 §2 是同一命题在**输入
侧**的形式:那边讲「输出相同不代表走了被测路径」,本节讲「输入根本没到,所以路径压根没跑」。
危险在失败方向——它产出的是「不工作」这个**否定**结论,而否定结论会直接改变修法(去改一个本来
正确的实现,与 §4.3 同一个陷阱的另一个入口)。

**实例(2026-07-29,#205 的 `io.read`)**:给 `io.read` 写的八个探针**全部返回 nil**,第一读法
是「实现不工作」。真实原因是 `go test` 不把 stdin 转发给测试进程。换成真实二进制 + 喂真实 stdin
之后,八种写法(`*l`、`*n`、`*a`、字节数、EOF、默认、`io.stdin:read`、`io.lines`)全部与
`lua5.1` 一致——实现一处都没改。

**手法**:先确认测试驱动是否提供那个环境;不提供就换真实二进制来验。判据:**一个探针的结果全是
同一个「什么都没有」的值(全 nil、全空串、全 0)时,先怀疑输入没到,再怀疑实现。** 反思实例
[[2026-07-29-issue205-206-208-io-userdata-debug]] 教训 2。

### 9.1 判据的输入包括它读的值和它查表用的键

把每个判据当一个表达式**端到端求值**,列出它读到的所有量,逐个问「这个量两侧相同吗」。合格的只有引擎无关的固定字面量,以及完全不接触被测差异的运行级标记。

那一轮踩过的位置,每一个当时都不像「判据的输入」:

| 载体 | 漏掉的那个量 |
|---|---|
| needle 写成 `tostring(0/0)` | 判据直接读了一个两侧不同的**值**(PUC 找 `-nan`、wangshu 找 `nan`,于是 `print("banana")` 只在一侧被拦) |
| 对称标记武装一次扫描 | 开关对称,但扫描**读取的对象**是运行时字符串内容,内容两侧不同(偏移差 1 就翻转方向) |
| provenance 按字符串值索引 | 判据没问题,**查表用的键**两侧不同,于是两侧查的不是同一格 |
| 剥符号的键只剥第 1 字节 | 归一化只对「token 紧贴串首」成立,fmt 带前缀或宽度 padding 时符号落到串中间 |
| `#t` 作预扫上界 | 判据**遍历时猜的那个边界**两侧不同(带空洞的表 `#t` 在 Lua 5.1 未规定) |

判据:**「一个对称的规则作用在不对称的数据上,整体依然不对称。」** 自检要问的不是「我的规则对称吗」,而是「这个判据端到端求值,两侧结果一样吗」。两条推论:① 决定「是否去读某个量」的开关对称,**不代表**判据对称;② 「归一化让两侧落进同一桶」这类修法要检查归一化对**所有位置**成立,不只对最常见那个位置成立。

#### 9.1a 一个锚点的「反例集合」要用它自己的失败输入去检验,不能靠读它的注释(2026-08-05,#232)

判据里的正则**锚点**也是判据的一部分,而它有一种特有的失效方式:**注释写的是意图,而正则的行为由字符类
语义决定,两者可以相差一整类字符**。

**实例(#232)**:`internal/oracle/compare.go` 的 `addrRe` 用 `\b` 锚定类型名,注释清楚地写着它为什么在
——不加锚点会把脚本自己打印的 hex(`0x1`、`myfunction: 0x1`)一起归一掉,而那是脚本自己的文本、必须逐
字节可比。**注释是对的、锚点的意图也是对的**,但 `\b` 的语义是「word 字符边界」,而 word 字符**包含数字
和下划线**:`io.write` 不带换行,于是 `io.write(0)print(print)` 的输出是 `0function: 0x...`,`0` 与
`function` 之间没有 `\b`,整个地址逃过归一化——**凡是这种写法都必然分歧**。意图里的「单词」与 `\b` 里的
「word」不是同一个集合,差的正好是数字和下划线,而这个差**只能从行为看出来,读注释永远发现不了**。改成
用「不是字母」锚定左边之后两个目的同时满足;因为那一版锚点吃掉了前导那一个字符(最终实现不吃),替换
也改成只重写匹配内部的 `0x...`(

**判据**:锚点类的正则,除了「该匹配的」用例,还要专门构造「**紧贴各类字符**」的用例——数字、下划线、
字母、行首。自查办法:把锚点左右各能出现的字符类列一张表,每一格写一个用例;写不满就说明还没检验这个
锚点。并且**两侧都要有用例**——只写「必须归一」那一半时,「把锚点整个删掉」也照样通过
(`internal/oracle/normalize_addr_test.go` 因此是六条必须归一 + 四条必须原样)。这与 §4.9x「断言两者
一致时要用能让两者不同的输入」同源:那条讲测试输入恰好落在性质成立的一侧,本条讲注释描述的意图恰好落
在写法正确的那一侧。反思 [[2026-08-05-issue232-234-address-width-and-gsub-escape]] 教训 3。

#### 9.1b 检查「代码有某个属性」的测试不能作用在文件文本上,注释会满足它(2026-08-09,#236–#241)

同一族的相邻一格,载体是 **shell 自测脚本**:一个检查「某个标志存在」的用例,如果搜索范围是整个文件,
那么**一段注释就能满足它**,而那个测试对代码零约束。

**实例(#236–#241)**:`scripts/test-fetch-lua-tarball.sh` 的第 5 个用例要钉住
`scripts/fetch-lua-tarball.sh` 的 curl 调用带 `--connect-timeout` / `--max-time` / `--retry`
(它们的缺席正是 #236–#241 的直接原因)。第一版 grep 整个文件,于是把 `--max-time` **从 curl 调用里
删掉之后它照旧通过** —— 因为那个词在文件顶部的注释里还在(注释正好在解释「bounded:
`--connect-timeout` and `--max-time`」)。改成先截取 curl 那条调用、再在调用文本里查。

**为什么容易错**:shell 脚本没有 AST 可查,「检查代码属性」最方便的实现就是文本搜索,而文本搜索的
作用域默认是整个文件。更糟的是**这类脚本的注释里必然提到那些标志**(注释就是在解释为什么要限时),
所以注释与代码在文本上不可区分,而且注释写得越好、测试越容易假绿。

**判据**:检查代码属性的测试要作用在**代码本身**上,不是文件文本;**一个注释就能满足的测试没有在测
代码**。自查办法:把被检查的那个属性从代码里删掉、**保留一切注释**,跑一次 —— 不变红就是没有在测代码。

与 [[2026-07-25-issue179-test-go-fuzz-retry-revive-round]] 教训 1 的关系(同一个 `test-scripts` 门禁、
同样是 shell 自测脚本):那条定下「shell 判据不能只用 `rc` + 反向 grep,必须配 positive marker」;本条
补上它的另一半 —— **positive marker 的作用域也要证明**。本轮这个用例是有 marker 的(它查「必须出现」
的字节串,不是反向断言),假绿的原因是那个 marker 的**搜索范围**里存在一个非代码的来源。所以那条纪律
要读成两半:有 positive marker,而且它只能被被测代码满足。同时这也是
[[unreproducible-crasher-triage]]「一个防住某个数值的回归测试必须用变异实测确认」在**非数值属性**上
的形式(那条改回一个数值,本条删掉一个标志),而**变异实测是本轮唯一发现这个缺陷的手段** —— 那段
grep 逻辑读起来完全正常。反思
`memory/reflections/2026-08-09-issue236-241-curl-timeout-misread-as-enospc.md` 教训 5。

### 9.2 判据自身消耗的资源也是它的输入 —— 而这是三个独立的问题

在两侧语言实现不同的 harness 里(一侧解释执行、一侧 C),任何随输入规模增长的检查都要多问一句「**另一侧付得起吗**」。这一问与「值对称吗」相互独立,而且它不是一个问题,是三个 —— 三者的区别在于哪一侧先耗尽、以及耗尽时的**表现**:

| 资源 | 为什么不对称 | 实测 | 表现 |
|---|---|---|---|
| **时间** | 检查在 wangshu 侧是解释执行的 Lua、在 PUC 侧是 C | 逐字节扫 80 KiB 的 fmt(不含 NaN、不含任何转换):PUC 392ms 判 OK,wangshu 1m22s 后 `instruction budget exceeded` | 一侧撞 step budget → 单侧 skip |
| **分配量** | PUC 的 GC 在 alloc 帽内回收临时串,wangshu 的 arena 回收不了 | straddle walk 每个转换复制一次 fmt 前缀:**103 字节**的脚本耗尽 wangshu 的 arena,PUC **706ms** 跑完 | 一侧撞 `MaxArenaBytes` → 单侧 skip |
| **matcher 步数预算** | 只有 wangshu 给 pattern 重试计步(`maxMatchSteps`),PUC 的 C matcher 没有预算 | 悬崖在 **1 MiB** 附近;含前导量词的模式约 **1024 字节**就撞上(量词在每个起始位置重试,代价是运行长度的平方) | 一侧报 `pattern too complex`;脚本用 pcall 包住时是**假分歧**而不只是丢输入 |

**三者需要三个测试,不能合并**。最反直觉的是 matcher 预算这一维**成本比值测不出来**:预算触发时失败的那一侧反而**更快**(提前放弃),比值缩小而不是增大。分配量这一维成本比值也测不出来,因为它量的是时间。

三步过法:① 这个检查的复杂度随输入的哪个维度增长;② fuzz 能把那个维度推到多大;③ 更吃紧的那一侧推到那么大,会不会撞它自己的那道限制。合格的写法:让复杂度跟着「真正相关的东西」走(那一轮把扫描从「跟着 fmt 长度」改成「跟着转换个数」,plain find 跳到下一个 `%`,38s → 264ms);只在确实可能有东西被拦时才启动扫描(启动判据每参数 O(1) 且由参数类型推导,所以两侧一致);需要 pattern 的地方只在**固定长度的切片**上跑。

> **补记(2026-08-04,#224/#225):调紧 harness 的资源上限之后,要用「注入一个必须被发现的缺陷」证明检测能力还在。** 上表讲资源耗尽让输入被**单侧 skip**;反过来动那些上限(step budget、arena cap、输入长度、超时)时,风险是同一个物理现象的另一面——预算变小之后,某些输入可能在到达被测代码之前就退出,于是测试变绿的原因悄悄从「两侧一致」换成「这个输入根本没跑完」,而**表现是测试照旧通过**。所以「减半不损失覆盖」这句话不能只靠推理:那一轮把 p4 fuzz harness 的 step budget 从 `1<<20` 减到 `1<<16`(为什么是 `1<<16` 而不是减半的 `1<<19`,见 [[unreproducible-crasher-triage]]「上限的余量」),推理是「这一族每种写法在两个预算下都触发,fuzzer 走同样的路径、只是更早停下」,而检验分两步:**注入一个真实的 P1-vs-P4 分歧**(把升层侧的返回值截断)确认新预算下 harness 仍然 FAIL、撤掉注入之后通过——这一步管**检测能力**;再**读一个白盒计数器**——harness 自己的 seed corpus 在 `1<<20` / `1<<19` / `1<<16` 三个预算下 `PromotionCount` 完全相同——这一步管**覆盖**。判据:调紧一个 harness 的资源限制之后,注入一个必须被发现的缺陷跑两个方向、再用白盒计数器读一遍覆盖,不要只看现有用例是否仍然通过——只跑「仍然通过」这一半,等于用一个恒真的观测支撑一个具体结论(与 §9.5「只验一种时,通过的信息量几乎是零」同一条)。**变窄的地方要如实记下**:那一轮 `1<<20` 时有两个 seed 会把 arena 推到上限、`1<<16` 时没有,所以那条 arena-cap 错误分支覆盖到的写法变少了,这是收窄而不是空洞(`test/regression/issue144_regression_test.go` 直接覆盖 arena cap,且两个预算下 step budget 都先于 arena cap 触发)。反思 [[2026-08-04-issue224-225-watchdog-margin]] 教训 5。

### 9.3 「不比较那个量」往往比「把比较做对」更可行

那一轮「限制扫描规模」失败了**五次**,五次都是同一个错误 —— 比较了一个两侧不相等的长度:输出长度 → 输入长度 → 分桶长度 → 放宽容差的长度 → 带内容例外的输入长度。每一次都以为找到了「两侧相等的那个长度」,下一轮都被证否。

最终的解法不是第六种比较方式,而是**取消比较**:把扫描本身做成不需要上限的(plain find 在两侧都没有 matcher 步数预算;确实需要 pattern 的地方只在固定长度切片上跑)。八处扫描点全部这样改写之后,harness 里不再有任何长度阈值。

判据:**当一个量在两侧不可能相等时,不要试图容忍这个差异。** 容差、分桶、内容例外都只是把出问题的窗口挪个位置,窗口本身还在。要问的是「**能不能不读这个量**」。三步:① 先问这个量**能不能**两侧相等(能构造出反例就别去设计容差);② 再问这个比较是**为了什么**;③ 最后问那个目的**有没有不读这个量的实现**。第 3 步经常有答案,而且答案通常更简单。

那一轮的「不可能相等」是**可证的**,不是「暂时没找到」:`..` 强制转换出来的 NaN 文本带一个符号字节,而 `#` 与 `==` 能把那个字节搬进一个**数字**(`("y"):rep(N + #(""..(0/0)))` 两侧长度必然差 1),这时输出里连一个 nan 字节都不剩,任何基于内容的例外都看不见它。

**反面的代价也要记住**:规模上限只能限制 harness 自己的**循环次数**,一旦让它变成「这个输入可不可比」的判据,就会切掉一片覆盖面 —— 那一轮的 `__concat_scan_cap` 曾在上限处抬 sentinel,后果是**所有**超过 4096 元素的表整类不可比,`table.concat` 的边界处理与错误文本比对全丢掉。

### 9.4 同一处代码改到第三版还在出同类缺陷 = 抽象选错,不是实现没写对

§9.1–§9.3 是判据的正确性;这一节是**改法**的纪律,它比前三节层级更高。

那一轮 span recorder 的吸收逻辑改了**六版**才收敛,前五版的共同点是每一版都「修好了上一版暴露的那一个问题、又引入一个新的」:逐字节 walk 太慢 → 加 offset 上限 → offset 本身两侧差 1 → 换个基准点数 offset → 还是差 1 → 改用一次 pattern match → 量词重试是平方、撞 matcher 预算 → 换个锚定位置 → 还是平方。

收敛的那一版做法不同:**先把需要的性质当作约束列出来,再找同时满足全部约束的写法。** 那三条约束是**无重试**(否则撞 matcher 预算)、**无迭代**(否则一侧付解释执行成本)、**无 offset 运算**(否则那一个符号字节让两侧所有绝对位置差 1)。三条一起看答案几乎唯一;而单看任何一条,前五版里都有某一版是满足它的 —— 这正是逐个打补丁一定会来回震荡的机制。

判据:**同一处代码第三次出现同类缺陷时,停下来把这段代码必须满足的全部性质列出来(包含前几版各自暴露的那条),再找同时满足全部性质的写法。** 有就直接写那一种,不要在当前版本上继续加分支;没有就说明这段职责本身分配错了,去问它的前提。

配套信号:**如果第 N 版的改动内容是「给上一版加一个上限 / 加一个特殊情况 / 换一个基准点」,它几乎肯定是第 N+1 个缺陷的来源。** 收敛的那一版通常反而**更短**。

### 9.5 harness 的守护测试自己也要过 §2

那一轮的守护测试两次「看不到它命名保护的东西」,都值得记:

- **断言选错了量**:成本测试第一版只断「两侧 verdict 一致」,把 bug 改回去照样通过 —— 一侧慢了 **74 倍**但仍在预算内跑完。必须断的是**成本比值**,因为那才是「输入再大一点就变成单侧 skip」的那个连续量;verdict 只是比值越过阈值后的离散后果,用它做断言等于要求测试输入刚好落在阈值另一侧。改成断比值后反向验证报 **645 倍 / 上限 120 倍**并失败。写法上每个形式与**自己的短输入版本**比,让共享机器的负载同时影响比值两端。
- **用例根本没到达被测代码**:后来为防住 matcher 预算这一维加的输入上限,让 12 个大变体里的 **9 个**在扫描启动之前就短路了,成本测试于是在量一段没跑起来的代码。这就是 §2 的原始命题,只不过载体是 harness 自己的守护测试。

判据:harness 守护测试也要配「本用例真的走到了被保护的那段代码」的证据,并且每个已知的历史缺陷各做一次反向验证 —— **只验一种时,通过的信息量几乎是零**。

### 9.6 验证一个差分 crasher 修好了:断 equal 不是断绿,并且双向验证

**适用场景**:声称某个差分 crasher(`FuzzOracleDiff` 一类)已经修好之前。

**核心断言**:差分 fuzz 里 **skip 也让测试变绿**,但含义与 equal 相反 —— equal 说明两侧输出逐字节相同,skip 说明这条输入被丢掉了,连同它这一次运行里可能携带的真差异。**绿灯本身不携带这个区别的信息**(与 §9 全节同一物理基础)。把 skip 读成「修好了」等于把这条输入又藏了一次,而不是修好了它。

**判据(两个方向都要走)**:

1. **修复后的 HEAD 上,断言它以逐字节 equal 通过** —— 不是「测试绿了」,要看 verdict 本身。少这一步就无法区分「修好了」与「被 skip 藏起来了」。
2. **修复前的 base commit 上,断言它确实 FAIL** —— 少这一步就无法排除「这条 crasher 本来就是 nightly 的 flake」,那种情况下 HEAD 上的 equal 什么也没证明。

**实例**:#187–#190 四个 crasher(2026-07-26,分支 `fix/187-190-nan-crasher-corpus`,反思 [[2026-07-26-oracle-nan-render-redesign]] §9)在 redesign 之前的 base `c982610` 上全部 FAIL、在当前 master 上全部以真正的逐字节 equal 通过。它们一行代码没改就修好了,因为四个都是 §9.0 那个 NaN 符号字节的表现,而那个根因已经在产生处被消除 —— **正是因为消除在产生处,四个表现才一起消失**;如果当初走的是下游识别那条路,四个 issue 会各自需要一条新的判据分支。

**推论**:一批 crasher 能被同一个改动一起解决,本身就是「根因修在了正确位置」的一个事后确认信号。这批 crasher 的分组纪律见 [[unreproducible-crasher-triage]]「一批 crasher 先问会不会被同一个改动一起解决」。

#### 9.6a 加 skip 守卫时,两侧用例要沿「机制」的每个维度取,不能只沿被报的那个写法取(2026-08-11,#244)

**适用场景**:给差分 harness 加一条「这类输入两侧一起跳过」的守卫时。

**核心断言**:「该 skip 的确实 skip、紧邻不该 skip 的仍在比较」这个两侧检查是必须的,但它防住的只是
「在**已采样的那一格里**宽了或窄了」。**两侧用例的取样维度由机制决定,而选用例时手边只有那个 seed 的
形状。** 沿 seed 形状取样时,两侧检查会以为自己已经验过宽度。

**两种失效的反馈完全不对称,而且都不指着守卫说话**:

| 失效 | 后果 | 反馈形式 |
|---|---|---|
| 守卫**太宽** | 静默削掉合法输入的覆盖面(单侧 skip,§9 全节) | **测试通过** |
| 守卫**太窄** | 该跳的没跳,那一族 crash 还活着 | 下一次 nightly 再开一个 issue,而它长得像一件新事情 |

**实证(#244)**:`unpack` 索引落在 int32 崩溃窗口时抬 sentinel 跳过。两侧检查**做了**——确认了两个崩溃值
被跳过,而 `-2147483646`、`-2`、`INT_MAX`、显式区间、整表 unpack 全都仍在比较。**守卫仍然两个方向都错**:
两侧用例都取自 `unpack({}, i)`,于是「紧邻不该 skip 的」取到 `unpack({},-2147483646)`——它**确实**不该
skip;而真正该跳却没跳的 `unpack({1,2,3},-2147483646)` 不在用例表里(崩溃条件里有 `e = #t`,`e` 一变窗口
就平移,详见 [[cross-backend-semantic-fix-sweep]] 第八个刻度)。同时 `i >= 2147483648` 被一律跳过,而
`4294967297` 窄化成 `1`、两侧都返回 3——**窄化之后的值才是判据的输入,源码里的字面量不是**(与 §4.5c 那轮
差点按文本删掉 `4294967298` 那个 seed 是同一个机制)。

**判据**:加 skip 守卫时,写下**守卫的条件**与**机制的条件**,**逐字对照两者的自由变量**——机制里有的量
而守卫条件里没有,那个量就是漏掉的取样维度(本轮守卫只读 `i`,机制读 `i` 与 `e`)。两组用例都沿机制的
每个维度各取两侧,而不是沿被报的那个写法取。自查办法:说「两侧用例都写了」之后再问一句「这两侧是沿哪个
量取的两侧」。

**与既有条目的关系**:§9.6 讲验证一个修复要**双向**(base 上 FAIL、HEAD 上 equal),§4.5c 讲钉两个阈值
的关系要**两个方向都断**——本节是这两条的前置一格:两侧 / 两个方向这件事本身还有一个「沿哪些维度取」的
问题。处置侧的对应条目见 [[unreproducible-crasher-triage]]「同一写法第三次被开成 issue 时」(守卫太窄
的表现就是同族 crasher 继续被开出来)。反思
[[2026-08-11-issue244-oracle-segv-unpack-int32]] 教训 3。

### 9.7 新增一条输出路径时必须同时把它接进捕获,否则它退出比较范围

**适用场景**:给运行时加任何**写外部世界**的 API 之后(新的输出函数、新的句柄方法、新的日志
出口)。

**核心断言**:差分比较的对象不是「程序做了什么」,是「harness 捕获到了什么」。新增一个写外部
世界的 API 就是新增一条绕过捕获的通道,而它的失效表现是**最安静的那一种**:不是误报(会亮
红灯)、也不是单侧 skip(至少这条输入被丢掉),而是**这类输出在所有输入上都不再被比较**,却
没有任何迹象——两侧都不捕获、两侧都一致、两侧都不报错。这是 §4.2 / §4.6 那一族在**捕获面**上
的新成员:那两条是「一句声明说这里不用看」与「一条还活着的 skip 在挡输入」,本条是「捕获面
本身有个洞」。

**实例(2026-07-29,#205)**:加了 `io.stdout` 之后,`io.stdout:write("A")` 直接漏到真实 stdout,
那段文本在**两侧**的捕获输出里都不存在,于是两侧仍然一致、不报分歧,而比较实际上已经不覆盖
这条路径写出的任何东西。修法是在 `internal/oracle/prelude.go` 里把 file-handle 的 `:write`
也接到 `io.write` 用的那个累加器上(`io.stderr` 的写入丢弃而不累积,与 harness 别处只比 stdout
的口径一致)。

**手法**:加了任何写外部世界的 API 之后,写一个「经新路径输出 + 经老路径输出」的用例,确认
捕获输出里**两段都在**。判据不是「测试绿了」,是能在捕获结果里逐字节看到新路径写出的那几个
字节。反思实例 [[2026-07-29-issue205-206-208-io-userdata-debug]] 教训 3。

### 9.8 两侧在同一个错误上一致,不等于两侧在行为上一致

**适用场景**:差分比较报「一致」,而捕获输出是**空的**或者是**错误文本**时;以及 harness 层的
共享代码(prelude、wrapper、归一化层)刚有改动时。

**核心断言**:比较器只回答「两串字节相等吗」。两侧共享同一层 harness 代码时,那层代码自己的
bug 会**对称地**打坏两侧,于是「相等」这个结论仍然成立,只是它已经不再是关于被测语义的结论
了。这是 §3 那句「harness 自身失效的默认表现是假绿」的具体化,提供的是一个**廉价的可执行
动作**。

**实例(2026-07-29,#205)**:给 file-handle 的 `:write` 写捕获 wrapper 时调了一个 prelude 里
**并不存在**的 helper `__ts`,于是每个碰到 wrapper 的脚本都死在
`attempt to call global '__ts'`、捕获输出变成**空串**——而比较报的是「一致」,因为两侧以完全
相同的方式失败了。发现它靠的是打印 verdict 与实际输出,不是看 diff 是否为空。

**手法**:差分比较报一致时,如果输出是空的、或者是错误文本,额外确认那不是「两侧同样地坏掉
了」——**空输出与错误文本是两个红旗**,正常脚本的捕获输出很少恰好是空的。harness 层的新代码
上线时,至少跑一个**已知应当有输出**的用例并肉眼确认那段输出真的在捕获结果里。反思实例
[[2026-07-29-issue205-206-208-io-userdata-debug]] 教训 4。

**与 §2/§3/§8 的关系**:§2/§3 是「证明测试走到了被测路径」,§8 是「读收益时选对度量窗口」,本节 §9.1–§9.5 是「**证明 harness 的判据在两侧算的是同一件事、花的是两侧都付得起的代价**」,§9.6 是同一命题在**验证修复**这个时点的形式(断 equal 不是断绿 + 双向验证),§9.7/§9.8 管**捕获面**(新增的输出路径有没有进捕获 / 两侧一起坏掉时的假一致),而 §9.0 在这之上一层:**先证明这个判据该不该存在**。它们共享同一物理基础:绿灯 / skip / 数字本身不携带「判据是否成立」的信息。本节与 §3 还有一层直接相连 —— §3 说 harness 自身失效的默认表现是**假绿**(两侧同样地坏掉 → 输出相同 → 比较器判等),本节说的是判据**只在一侧**坏掉时更糟:输入被静默丢掉,连绿灯都不留。

## 10. 触发场景速查

写新测试 / 改基准 / 看一个数字反常时,问自己:

- **看到加速路径 ≈1.0x 或机制就位但数字不动** → 默认「没走加速路径」,加白盒计数器或正向 tier 断言
- **快路径与 helper 输出等价** → 必须加毒化/哨兵 helper 测试,普通 e2e 测不出
- **基准 A vs B 跨结构** → 先证两侧负载形状一致,跨结构的「慢 N 倍」先归因不归因 perf
- **差分测试 / 差分套全成功语料绿** → 加错误路径用例,否则 R3 类漂移 bug 结构性失明
- **加 e2e 语料前** → `grep test/luasuite/testdata/` 看是否已覆盖,优先官方套件而非手写
- **修完一个 issue / crasher 报上来的那个具体输入、准备收工时**(§4.1) → 一个 reported case 只是接受面的一个采样;枚举**所有到达同一段代码的通道**并逐个验证它真的执行(参照实现的早退路由分支逐条对、共享 helper 的每个入口各构造一次输入),再补一轮定向 fuzz smoke。收工判据不是「reported case 现在过了」(#192 报 `tonumber("nan(0)")`,枚举通道才发现 base 10 整条路由错、六条分歧里五条没有 issue)
- **读到 / 写下「已知差异 / 已登记豁免 / 已接受偏离」这类注释时**(§4.2) → 立刻指出**哪一行代码或哪个测试执行了它**(本仓执行体在 `test/difftest/corners_test.go::exemptions` 与 `internal/oracle/prelude.go` 的 sentinel);指不出来就当成活着的 bug 处理——一句无执行体的豁免声明比一个已知 bug 更糟,因为它读起来像已经处理过了
- **回归测试期望与实现冲突、要决定改哪一边时**(§4.3) → 先跑 oracle / 写 C 探测 / 读参照源码取事实,再决定;默认怀疑实现会诱导去「修」一个本来正确的实现,而且改坏之后有一条绿色测试替它背书。别用「另一处类似的地方是这样的」做类比。**同一陷阱的另一个入口**:issue 正文陈述的参照行为也要亲自跑一遍再实现,哪怕那个 issue 是自己开的(#199 说 `io.write` 返回文件句柄,实测 5.1 返回布尔,句柄是 5.2+)
- **修一个「顺手记下的一组」式 issue 时**(§4.3) → 逐项**重新核对**而不是逐项直接修,「这项不成立」与「这项修好了」一样是完成;集合式 issue 的错误率明显高于专门查证过的单条(#202 七项里三项实测与 lua5.1 一致、根本不成立)。自己开这类 issue 时在正文写明**未逐项核实**
- **一个改动功能上做对了、但让既有压力测试失败且失败点在不熟悉的子系统契约上时**(§4.3b) → 撤回并写清「做到哪、卡在哪、下一步要先搞清什么」,「卡在哪」要具体到**哪个约定没搞清**(#202 的 `io.stdout`:非 finalizer 路径创建 userdata 的分配与 rooting 约定;三处 VM 缺口已补、证据指向构造侧不是收集器)。撤回的理由不是「太贵」,是这个形式会破坏某个不变式
- **给一个阈值定数值、或发现两处读同一个常数时**(§4.5/§4.5b) → 先跑参照实现在上限附近的耗时把数字写进注释;再问它防的是**正确性**还是**资源**,防的东西不同就该是两个常数(形状保持逐字对应、数值分开)。量参照实现的边界时,让**决定它的那个量**变化一格再测(`unpack` 的阈值随参数个数变化,这才排除了「硬编码 7997」)
- **给一个批量操作(拼接 / 重复 / 格式化 / 序列化)定预算、或给一个新算子加资源限制时**(§4.5d) → 先写下「这次调用的代价 ≈ 什么的函数」再让记账读那个量,别用「个数」代替「字节」(`table.concat` 的遍历被表长界住,256 个 2 KiB 的元素按个数看很便宜、实际 53 秒);并且先 grep 同类资源已有的计量器接上去,不要自建第二个阈值(自查:两个限制同时被触碰时谁先触发,答不出来就说明只该有一个)
- **把两个阈值拆开之后、要写测试钉住这件事时**(§4.5c) → 不要在测试里复制那两个数(不导出 / 跨包时尤其,复制品会与任一侧各自漂移而测试照旧绿);把两个阈值**之间**的区间做成用例,用「这个输入该被执行 / 该被拒绝」表达,并且**两个方向都断**(`TestInsertShiftThresholdsStayDistinct` 断「必须被执行」+「必须便宜」,只断前者时两个数合到高的那个仍然全绿)
- **修完一个曾经被 skip 绕过的 bug 之后**(§4.6) → `grep` 为它加过的 skip / 豁免 / 已知边界,逐条撤或收窄,确认相应用例现在**零 skip** 参与比对(不是「测试绿了」);收窄剩下的那条要连名字一起改,旧名字描述的是已经不存在的理由。一个还活着的 skip 与一句无执行体的豁免声明读起来一样,区别只是前者真的在挡输入
- **测格式化 / 转换 / 解析类接口时**(§4.7) → 把「不给可选参数」当成一个独立维度扫,而不只是扫各种参数值;对每个可选参数问「省略时参照实现取什么、那个取值是谁定的」(`%g` 的显式精度一直是对的,只有默认精度 C 是 6 而 Go 是最短往返)
- **写 force-all / 缓存裁决 / IC 命中类** → 同时 (a) 白盒断言「真到达加速 tier 不是 stuck no-op」+ (b) 输入侧也加结构盲区用例(vararg 顶层 / 字符串常量值 / 协程不升层)
- **写「auto / 自然触发路径」测试套(不带 force-all)** → 配 `PromotionCount>0` 一类白盒兜底断言,证明该 run 里确实发生过真实升层;否则短用例语料可能悄悄退化成纯解释器覆盖(见 §1「未强制测试静默退化」)
- **测 tier 状态机的吸收态分类断言**(`StuckNotCompilable` 类)→ 载体调用次数要显式跨过 force-all 的 warm-up retry window(当前 64),否则测的是 `TierInterp` 状态不是 Stuck 状态(见 §2 补丁)
- **测运行期开关 / kill switch 类切换语义**(`SetTierEnabled` 一类)→ 三段式(promote→off→on),每段各配路径断言(`NativeRunCount` delta > 0 / == 0 / > 0)+ 第三段的「缓存指标不变」断言把「重开不重编译」隐性约定钉死(见 §2(d));只断言「结果一致」等价于没测
- **机制叠加多档崩点诊断**(§6) → 第一步不是穷举 N 档分别诊断,是写 minimal payload bypass 末档跳一档,把 N 档收敛到一档;多后端/多平台首次「真机 execute」上线时配真机 runner
- **收到「某条路径导致 N 倍退化」类归因,准备写止血/修复计划前**(§7) → 先 grep build tag / 读函数头注 / 跑白盒探针证该路径在当前二进制里真的存在且可达,再动手修
- **分诊某 P4 tier bug 时**(§7 同源) → 先跑白盒探针(`NativeRunCount` / `DispatchHelperCount` / PerOpCode debug 打印)分类走的是 PerOpCode / nativeCode / p4Code 哪一类,再挖对应 emit;traceback 直觉锁定的是「触发前提」不是「代码路径」
- **收到「某机制没生效」的指控,要写复现测试时**(§7.1) → 第一条断言是「复现测试走的就是被指控路径」的白盒证据(`SafepointCalls` / `PromotionCount` / 命中计数器),优先复用已证明路径的既有测试写法;复现失败先怀疑路径走错,再考虑「问题不存在」
- **扩接受面(opSupported 加 op) / 换新硬件跑 fuzz / 改 fuzz 参数(-parallel、-race、GOMAXPROCS)时**(§5) → 立刻跑一轮 60~120s 相关 fuzz smoke,把它当 `go test` 必经步骤,不要延后到「专门的 fuzz 里程碑」;三个独立实例都在维度动的第一次 fuzz 里就抓到既有 bug
- **读一个投机 / inline 快路径的性能收益数字、或复查一个历史 perf 裁决时**(§8) → 度量单位选跨 Run 稳态 dispatch 计数而非单 Run 命中数;看到「命中 N 次」漂亮但整体数字反而回归,默认怀疑「机制被触发过但稳态从没真生效」;给「把身份快照烤进段」的 guard 前先问会不会被跨 Run 重建打穿
- **想在差分 harness 里接受 / 跳过某类平台差异时**(§9.0,**先过这一条再看下面几条**) → 先判差异属于哪一类:「同一个抽象值的不同书写方式」(NaN 符号位)→ 在**渲染处**消除,别设计判据,因为这类差异会流入普通数据(`string.len(0/0)*100` 是 400 对 300,输出里没有可锚定的 token);「两侧本来就是不同的东西」(引用值地址、两侧独立选定的实现上限)→ 才在比较时归一或跳过。判在渲染侧时先查那一侧的**供给链**(源码在哪、谁编译的、已经打过什么补丁、改它会不会破坏它作为基准的意义)
- **写下「我实测过了,这条路不行」时**(§9.0) → 同时写下它**没有**排除什么:被否证的是哪个**具体**选项,与它并列的其他选项各是什么状态(未查证 / 已查证不可行 / **未想到**)。实测保护的是那一个选项的结论,不保护由它推出的范围
- **发现自己在为同一个机制反复补 edge case 时**(§9.0) → 先数缺陷密度有没有在降;没降就问「这个机制装在了正确的位置吗」,而不是继续想下一种情况。这与 §9.4 是两个尺度:§9.4 管一个机制内部该不该继续修,§9.0 管这个机制装在哪一层
- **要给差分 harness 加一条归一化时**(§9.0a) → 问一句「脚本能不能**不打印它、而是测量它**」;能的话(`#`、`string.len`、比较、当表键、算术)归一化就不是完整的修法,那个会被测量的子量必须回到**渲染处**对齐。判去比较侧的那一类里仍可能有这样一个子量:地址的**值**只能归一,而地址的**宽度**被 `#tostring(t)` 量出来时归一化已经到不了(#233:PUC 21 对望舒 17)
- **改一个带锚点的正则时**(§9.1a) → 给锚点左右各能出现的字符类列一张表,每格一个用例(数字、下划线、字母、行首),别靠读注释判断意图是否被写法正确表达;并且**两侧都要有用例**,只写「该匹配的」那一半时「把锚点整个删掉」也会通过(#232:`\b` 的注释是对的,而 word 字符包含数字,`0function: 0x...` 整段逃过归一化)
- **写一个「代码里有某个标志 / 字段 / 调用」的检查型测试时**(§9.1b) → 让它作用在**那段代码**上(先截取那条调用再查),不要 grep 整个文件;**一个注释就能满足的测试没有在测代码**,而这类脚本的注释里必然提到那些标志。自查:把属性从代码里删掉、保留一切注释跑一次,不变红就是没测到(#236–#241:`--max-time` 从 curl 调用里删掉后测试照旧通过,因为那个词在注释里还在)
- **给差分 harness 写跳过 / 判定判据时**(§9.1,**前提是已经按 §9.0 判定该在比较侧处理**) → 把判据当表达式端到端求值,列出它读到的每一个量(字符串内容、长度、字节、**查表用的键**、**遍历时猜的边界**)逐个问「两侧一样吗」;只放过引擎无关的固定字面量和完全不接触被测差异的运行级标记;决定「是否去读某个量」的开关对称不代表判据对称;后果是**单侧 skip**,比误报严重
- **给差分 harness 加任何随输入规模增长的检查时**(§9.2) → 多问一句「另一侧付得起吗」,而这是**三个独立的问题**:时间(一侧解释执行一侧 C)、分配量(一侧 GC 回收一侧 arena 不回收)、matcher 步数预算(只有一侧计步)。三者要三个测试:matcher 预算这一维成本比值**测不出来**(触发时失败那侧反而更快),分配量那一维也测不出来(比值量的是时间)
- **准备给一个两侧不相等的量设计容差 / 分桶 / 例外时**(§9.3) → 先问「能不能不读这个量」。容差和分桶只是把出问题的窗口挪个位置;三步:① 这个量能不能两侧相等(能构造反例就别设计容差)② 这个比较是为了什么 ③ 那个目的有没有不读这个量的实现。另外规模上限只能限制 harness 自己的循环次数,别让它变成「这个输入可不可比」的判据
- **同一处代码改到第三版还在出同类缺陷时**(§9.4) → 停止改这一版,把这段代码必须满足的**全部**性质列出来(含前几版各自暴露的那条),再找同时满足全部性质的写法;信号 = 第 N 版的改动是「加一个上限 / 加一个特殊情况 / 换一个基准点」,那它几乎肯定是第 N+1 个缺陷的来源;收敛的那一版通常反而更短
- **被测功能依赖 stdin / tty / 信号 / 工作目录这类进程级环境时**(§4.10) → 先确认测试驱动是否提供了它,不提供就换真实二进制 + 喂真实输入来验;探针结果全是同一个「什么都没有」的值(全 nil / 全空串 / 全 0)时先怀疑输入没到、再怀疑实现(`io.read` 的八个探针全返回 nil,是 `go test` 不转发 stdin,实现一处没改)
- **给运行时加了任何写外部世界的 API 之后**(§9.7) → 同一轮把它接进差分 harness 的捕获,并写一个「经新路径输出 + 经老路径输出」的用例确认捕获结果里**两段都在**;漏接的表现最安静:两侧都不捕获、都一致、都不报错,连 skip 都不留
- **差分比较报一致、而输出是空的或是错误文本时**(§9.8) → 额外确认那不是「两侧同样地坏掉了」;空输出与错误文本是两个红旗,harness 层新代码上线时至少跑一个已知应当有输出的用例并肉眼确认那段输出在捕获结果里
- **要说「这个差分 crasher 已经修好了」之前**(§9.6) → 断言它以**逐字节 equal** 通过(不是「测试绿了」——差分 harness 里 skip 也是绿的,含义却相反:输入被丢掉了),并且在修复前的 base commit 上**确实 FAIL**。少第一个方向分不清「修好了」与「被 skip 藏起来了」,少第二个方向排除不掉「本来就是 flake」
- **给差分 harness 加一条 skip 守卫时**(§9.6a) → 把**守卫的条件**与**机制的条件**逐字对照自由变量:机制里有而守卫里没有的量就是漏掉的取样维度;两侧用例(该 skip 的 / 不该 skip 的)都沿机制的每个维度取,不要沿被报的那个写法取。自查:说「两侧用例都写了」之后再问「这两侧是沿哪个量取的两侧」(#244:两侧检查做了,而两侧都取自 `unpack({}, i)`,于是 `e > 0` 时窗口整段挪出守卫范围——太窄那一半让整个测试二进制 SIGSEGV,太宽那一半把两侧都返回 3 的输入 skip 掉)
- **调紧一个 harness 的资源限制之后**(§9.2 补记) → 注入一个**必须被发现**的缺陷,确认新限制下仍然 FAIL、撤掉注入之后通过;「减小预算不损失覆盖」这句话不能只靠推理,因为它的失效方式是「输入根本没跑完」而表现是测试照旧通过。上限的余量怎么算见 [[unreproducible-crasher-triage]]「上限的余量」
- **新增一个只被 build tag 化文件使用的标识符时**(§4.10d) → 给它**同样的** tag,并按消费方的每一种 tag 组合各 vet 一次;「有没有被用到」是每种构建组合各自的事实,可见范围比消费方宽就一定有一种组合让它孤立
- **在仓里读到一段既有注释精确描述了手头症状时**(§7.2) → 先加一行打印或一个计数器证明那条路径真的被执行,再开始在那里查;症状相同不等于路径相同,而仓库自己写的注释比外部归因更难怀疑(#229:peroptranslator 那段注释逐字写着 `NEWTABLE head + SETLIST → "SETLIST: not a table"`,探针一行没打出来)
- **拿到一个「X 报错 / X 断言失败」形式的缺陷时**(§7.3) → 第一个问题是「X 读到的坏数据是谁写的」,不是「X 自己哪里写错了」;把坏值打出来问「什么东西会写出这个值」(`tag=65528` = `value.TagNil`,nil 是**清理动作**的值不是任何指令的自然产物,线索直接指向 GC 的栈根扫描)。**栈迹里缺少哪一层也是证据**——没有 JIT 帧就说明抬错的是解释器、破坏在更早的已升层调用里

## 11. 与本仓其他 guide 的关系

- 与 [[design-claims-vs-codebase-physics]] 构成对偶双防线:那是**实现前**重验设计稿主张,本 guide 是**实现后**证明在测路径。
- 与 [[perf-optimization-workflow]] §1「profile 先行」§3「benchmark 否决门」配:profile 先行决定**做什么**,本 guide 决定「机制就位后基准/测试**真的在测**什么」,数字完成前必过两关;§7 诊断侧对偶是 §1「profile 先行」的又一确认——不是「先假设瓶颈再优化」,是「先证明瓶颈在哪再优化」。
- 与 [[backend-capability-vs-profitability]] 配:那篇管「接受面按能力/收益分层」,本 guide §5 管「接受面动了立刻重跑 fuzz」;后端接受面每扩一格,能力层跑一轮 fuzz、收益层跑一轮 bench,是配对的两个廉价动作。
- 与 [[multi-doc-drafting]] §"主动盘点不确定决策" 同源:都强调「绿色 / 通过」之外的正交证据维度。
