> 为保住行号而做的填充,让一个宏的值能不能到达下游取决于宏名有多长
一个 LaTeX 抽取器有两个下游:
结论先行¶
一个 LaTeX 抽取器有两个下游:
check,机械检查,每条发现都要报file.tex:行号;read,把正文交给一个"冷读者"去读,读者看不到行号。
两个下游共用一个宏展开函数。为了让 check 能报行号,它填充:
替换值比调用长就会把后面所有偏移推走,所以长于调用的值一律不展开。
def sub(m):
val = macros.get(m.group(1))
if val is None or (pad and len(val) > len(m.group(0))):
return m.group(0)
return val + " " * (len(m.group(0)) - len(val)) if pad else val
这个判据是字符数,于是一个数字能不能到达读者,取决于作者给宏起的名字有多长。
我拿出厂函数量了一遍边界,调用 \dsCell{} 共 9 字符:
len(值) |
pad=True(给 check) |
pad=False(给 reader) |
|---|---|---|
| 8 | x VVVVVVVV y |
x VVVVVVVV y |
| 9 | x VVVVVVVVV y |
x VVVVVVVVV y |
| 10 | x \dsCell{} y ← 不展开 |
x VVVVVVVVVV y |
| 12 | x \dsCell{} y ← 不展开 |
x VVVVVVVVVVVV y |
截断点正好在 len(值) ≤ len(调用)。项目自己在真实宏上量到的是 7/7 全部
按长度分流:\Ubayes{}(9)→ Bayesian(8)活,\Uill{}(7)→ Ill-posed(9)死。
而真正造成损失的那个是 \dsCellZero{}(13)→ 0.60/0.20/0.20(14),
差一个字符,一组结果数字没有到达读者。
为什么¶
填充这条约束对 check 是正确的:没有它,check 报的
file.tex:行号 会在第一个长宏之后全部错位,而一个指不到可编辑位置的发现等于没有。
对 read 它是错的,而且错得看不出来 —— 读者从没有行号,
所以这条约束在那一侧只有代价没有收益。
两件事让它长期隐形:
一、损失是内容相关的,不是均匀的。 短值全部活下来,长值全部死掉。 看输出像是"这篇论文有些宏没定义",像数据特性,不像 bug。 如果它是均匀丢 10%,第一天就会被发现。
二、症状落在第三个地方。 读者拿到一个空洞(早期版本连名字都不留),
而读者不会把空洞留空。实际发生的事是:它给两处缺口填了数
(-0.115 到 0.076),然后以"论文自相矛盾"为由给了一条致命评价 ——
两个数字在论文里都不存在,真值是 -0.100 到 -1.000,原句完全自洽。
一份有洞的文件比没有文件更糟:没有文件会报错,有洞的文件会得到一份流畅的报告。
还有一个容易混的点:这里有两个根因,缓解方式不同。
一个是预载宏的范围 —— 在导言区 \input 进来的 \newcommand 从没被读到,
而导言区在每一遍里都被整体涂白。另一个就是上面这条长度约束。
项目里有一篇论文七个宏全定义在主文件、没有任何 \input,
把定义挪进被 \input 的文件对它毫无帮助 —— 先分清是哪一个,
再谈修法,否则会照着无关的那条去改。
怎么用¶
一个变换服务两个需求相反的下游时,不要用布尔开关,要问"谁需要这条约束"。
这里的正解是 read 不带约束地展开(已经这么改了),
因为"保住偏移"是 check 的需求,不是数据的性质。
布尔开关能工作,但默认值会决定哪一侧静默受损 —— 默认 pad=True
意味着新加的下游默认继承一条和它无关的约束。
凡是"条件性地不做"的分支,都要留痕。 这个函数在拒绝展开时
返回原文 m.group(0),不留任何记录。只要在拒绝时计数一次,
"本篇有 7 个宏因长度未展开"就会出现在报告里。
和 优雅降级设计好了,却没人被告知——退化必须在产物上留痕 同一条规矩:
降级必须在产物上看得见。
交给模型的输入要先过一道"形状完整"的检查。 这个项目的修法是
read 拒绝写出含空洞形状的文件。这比"叮嘱模型不要猜"可靠得多 ——
模型的输入薄了就会补,补出来的东西很流畅
(见 try/except 挡不住段错误,signal.alarm 送不进 C 代码——进程池会静默卡死 旁边那条:
机械核验比指令可靠)。
偏移保持型的文本处理,测试要带一个"比调用长的替换"。 这是最小的那个反例,两行就能写,而它是这一类流水线的通用故障点: 涂白、脱敏、宏展开、模板替换,全都有人为了对齐偏移而偷偷放弃替换。
出处¶
~/Tools/nonexpert-review:nonexpert_review.py:385-402(_expand_macros
与它自己写下的理由)、:1375(read 的 pad=False 调用点)、
fixtures/holes/numbers.tex(两个形状:一行两个 \newcommand、值比调用长)、
CALIBRATION.md 「最后那个宏 bug,和造成它的那条约束」与「虚构的那一条」两节。
上面那张边界表是我自己在 2026-10-11 导入 _expand_macros 跑出来的。