Skip to content

踩坑记录

tujue edited this page Oct 1, 2026 · 1 revision

踩坑记录

这个项目里反复出现同一类缺陷。这一页把它们归类——

不是为了记录"我们修了 31 个 bug",而是为了指出: 它们几乎全是同一个形状。


那一个形状

几乎每一个坑都是:

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

它们的共同特征是:

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

所以不能靠"跑起来没报错"判断正确性。

累计已 12 次以上。


分类

A. 兜底分支吞掉错误(最要命的一类)

#12 / #13

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

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

根因:列表原先用 "," 拼接、str.split(",") 切分。 内层列表编码后本身含逗号(L:[N:2,L:[N:3]]), 被外层一刀切开就成了残缺片段,正则匹配失败, 于是又落进那个"返回 0"的兜底分支——同一个兜底分支连犯两案。

修法:改用长度/个数前缀编码,元素边界由类型自己界定, 不依赖任何分隔符。见 ADR-0003。

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

B. 错误信息与实际原因不符

#22

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

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

而实际错误跟 if 毫无关系。

根因:这条信息是加 while/for/def/class 之前写的, 那时能开块的只有 if,所以直接写死了 "if 后面"。

#31

未知 > 1 抛出的是 Python 的裸异常:

TypeError: '>' not supported between instances of 'str' and 'int'

用户看到的是宿主实现的报错,而不是带位置的「炑」。

教训:错误信息里的具体名字会过期。加新语法时要回头改旧提示。

C. 只看"现在是错的",没看"以后会错" / 状态没清零

#23

空行会让整段缩进比较被跳过,DEDENT 全丢, 块永远不结束,第二个顶层 def 被当成还在 class 体内。

#26

break 写在函数里的循环外,直到运行到那一行才报错。 根因:最初只管解析、不管上下文,循环合法性留到运行期由异常冒泡发现。

#27

循环体里定义函数、函数体里写 break,会把调用方的循环打断:

while i < 3
    def f()
        break      ← 这个 break 干掉了外层的 while
    f()
$(i)               → i 停在 0,而不是 3

根因:loop_depth 是解析器的一个字段,进入函数体时没有清零。

教训:作用域类状态(loop_depth、缩进栈)在跨界时要显式重置, 不能靠"反正会自己恢复"。

D. 拿"读"当"写"

#19

self.x「1」 报 实例上没有属性 'x'。

根因:把整个 AttrNode 传给了赋值函数,于是它去求值 self.x—— 那是在读这个属性,而此时属性还不存在,所以报错。

赋值要做的是"求值左边的主体(self),然后往它上面写属性名"。

#30

m[0][1]「55」 写不进去,读回来还是老值,而且不报错。

根因(两层):_postfix_tail 把 m[0][1] 解析成嵌套的 IndexNode(IndexNode(m, 0), 1),写入路径没处理嵌套。

教训:读和写是两条路径。只测读不测写, 写入路径的 bug 会一直躲着。

E. 两条规则打架

#29

改正则加 {} 和 : 时把 - 漏掉了,于是所有减法/负数都报错。

教训:正则里每加一个字符都要想"原来能过的那些还过得去吗"。


关键字表漂移(专门一类)

IDLE 原设计是"每种语言一个目录",所以关键字表散落在各模块里。 本项目继承了这种散落,于是同一个概念有 9 份副本。

漏改一处的症状:

漏改哪张表 症状
colorizer 新关键字不高亮
autocomplete Tab 补不出来
pyparse 块开启者 写完按回车多缩进一级
STATEMENT_KEYWORDS if 新词 语法坏了

全都是静默失效,不报错。

所以 tests/test_keyword_sync.py 逐项检查, 包括反向断言("某某不该在哪张表里")。

教训:反向断言比正向断言重要。只说"该有的都有", 挡不住"不该有的混进去了"。


还有一个更隐蔽的:表和代码的语义分叉

第四版加 未知 时发现:关键字表不只一份,而且它们的"归属"不一样。

  • and / or / not 要高亮补全,但绝不能进 STATEMENT_KEYWORDS
  • break / continue 要高亮补全,但绝不能进"块开启者", 反而必须进 pyparse._closere
  • 未知 要高亮补全,但绝不能进 STATEMENT_KEYWORDS (它是值不是语句,加进去会破坏 if 未知)

所以"同步关键字表"不是无脑复制——得先想清楚每张表的语义。


测试自己也会错

#34

写三值逻辑测试时,有一项挂了,查下来是测试写错了:

check("每次静默观测都带提示", all("提示" in obs.hint ...))

恒为假。因为 hint 字段存的是建议正文("若要确定分支……"), 而 提示: 这个标签是渲染时才加的。查错了地方。

ADR-0008 第三条记过同一类事:报告失败之前, 先确认测试测的是不是你以为的那件事。


一条工作纪律

这些坑有一个共同点:都是"看起来没问题"的代码。

所以本项目的验证纪律是:

  1. 改动后必须跑实际程序,不是只看代码像对的
  2. 自测要验证最终效果,不是中间步骤
  3. 一个测不出失败的测试比没有测试更坏——它会给你虚假的信心
  4. 验收要覆盖每一条进入路径(读路径、写路径、嵌套路径、空值路径)

本项目连"输出正确"都不追求,但每条记录都要求"已验证"。

Clone this wiki locally