这个项目里反复出现同一类缺陷。累计已经十几次。 它们的形状几乎一模一样:

同一个概念在两个地方各写了一份规则,然后两份漂移了。

要命的是它们的长相:

  • ❌ 不发生异常
  • ❌ 不报错
  • ✅ 只是行为不对
赋值"成功"了,不报错,只是没效果。
程序"跑完"了,不报错,只是答案不对。

所以不能靠「跑起来没报错」判断正确性。 这句话写在 ADR 索引的第一条教训里,因为它是这个项目最贵的一课。

案例一:兜底分支把数据变成 0

早期解码失败时会「返回 0」兜底。于是:

$([1, [2, [3]]])      → [1, 0, [0]]      内层数据全丢,但不报错
$(["a,b", "c"])       → 被切成 3 个元素

根因:列表原先用 "," 拼接、str.split(",") 切分。 但内层列表编码后本身含逗号:

L:[N:2,L:[N:3]]

被外层一刀切开就成了残缺片段,正则匹配失败, 于是又落进那个「返回 0」的兜底分支—— 同一个兜底分支连犯两案。

修法是改用长度前缀编码,元素边界由类型自己界定, 不依赖任何分隔符。这就是 ADR-0003 的由来。

兜底分支如果不区分「合法的空值」和「我解析失败了」, 它就会静默地把数据变成 0。

案例二:报错文不对题

在函数外写 全局「0」,报的是:

这里需要一个缩进的语句块(if 后面必须跟缩进)

而实际错误跟 if 毫无关系。 这条信息是加 while/for/def/class 之前写的,后来语句种类变多了,文案没跟着改。

这类坑的恶劣之处:它会报错,看起来很正常, 但把你引向完全错误的方向。你按提示去查 if, 查了半天,问题在别处。

案例三:一条永远绿的测试

这个我要单独讲,因为它比 bug 本身更值得警惕。

test_keyword_sync.py 从第二版起就在核对 「8 份关键字表是否同步」——这个项目里有 8 个地方各存了一份关键字表 (解释器、IDLE 的语法高亮、自动补全、光标提示……), 加一个关键字要改 8 个地方,漏一个就静默失效。

所以这个测试很重要。但它只 print 比对结果, 从不返回非 0。

结果是:在总入口里,它永远显示 [OK]—— 即使某份表真的少了一个关键字。

一个永远绿的测试,比没有测试更坏。 没有测试你知道自己没测;永远绿的测试让你以为自己测了。

同一个教训还适用于用例的期望值。ADR-0008 里有个 短路求值的用例,第一版写成「输出里有 0 就算过」—— 那样即使短路完全失效也会通过,等于没测。 改成检查「副作用函数的输出没有出现」才真正起作用。

案例四:改名不报错,只是不生效

这个坑我栽了不止一次,而且每次都是同一个形状。

想改一个东西的行为,改了「看起来该改的那个名字」, 跑起来什么都没发生——没报错,只是没效果。 因为真正被读取的是另一个地方的同名副本。

我甚至在某次提交信息里写了:

修复:又一个「看起来该改的名字不是真正被读的那个」(第 9 次)

所以本项目的可靠做法只有两条:

  1. 读源码找出真正的读取点,不要猜哪个名字「应该」生效
  2. 写一个会失败的对照测试——测不出问题的测试等于没测

为什么专门记这些

不是为了说「我们修了 31 个 bug」。是为了指出: 它们几乎全是同一个形状。

看清形状之后,防御手段才有针对性—— 不是「更仔细一点」,而是结构上消除重复: 能用一份的就不要写两份,必须写两份的就用测试钉死。

完整清单见 wiki 的踩坑记录。