翻看 TeX Live 2024 的可执行文件,会发现一件怪事:latex、pdflatex 和 etex 根本不是程序,而是指向 pdftex 的符号链接。只有 Knuth 写的 TeX 本身——命令 tex——仍以独立程序的形式存在。想通这一点,围绕 LaTeX 的大部分困惑就消散了,因为它揭示了整个生态赖以成立的区分:引擎(程序)与格式(预先载入的宏集合)是两回事。本页要讲的,就是位于最底层的那个被冻结的程序,以及 1990 年代加在它之上的 e-TeX 扩展——如今人人都在用却毫无察觉的地基。
tex 这条命令实际运行的是什么
tex 运行的是这样一个程序:把字符和盒子装进行与段落,优化断行位置,然后把结果写进文件——此外几乎什么也不做。混乱的源头在于,TeX 这个词既指这个程序,又指程序所解释的那套命令体系。分节、交叉引用、参考文献,程序本身一概不知。
Knuth 于 1978 年开始写第一版 TeX,起因是他自己那本《The Art of Computer Programming》第二卷的校样,其中的公式排得让他无法忍受。初版只是摸索性的原型,1982 年被彻底重写,这次重写就是 TeX82;今天说「TeX」,实际上指的就是这一支。Knuth 直接内建在程序里的最小命令集称为原语——\def(定义命令)、\hbox(水平盒子)、\vbox(垂直盒子)等等。
只用原语写文档并不现实。所以实际上人们使用格式,也就是叠在原语之上的一层宏。载入 plain.tex 得到 plain TeX,载入大得多的宏集则得到 LaTeX。格式不必每次重读:它会被预先展开、保存为 .fmt 文件,启动时瞬间恢复。这套机制的细节——.fmt 里装着什么、如何用 fmtutil 重建、LaTeX2e 与 LaTeX3 是什么关系——由「LaTeX 格式」那一页负责。
tex、initex、etex 之中,哪个才是真正的程序
只有 tex。 initex 是指向 tex 的符号链接,而 etex 竟是指向 pdftex 的链接——也就是说,作为独立程序的 e-TeX 在当前的发行版里已经不复存在。这不是看法,而是在自己机器上敲一句 readlink 几秒钟就能核实的事实。在 TeX Live 2024 上,tex --version 会回答 TeX 3.141592653 (TeX Live 2024)。
$ tex --version | head -1
TeX 3.141592653 (TeX Live 2024)
# Which of these are real binaries, and which are links?
$ cd $(dirname $(which tex))
$ readlink tex initex etex latex pdflatex amstex csplain
# tex prints nothing: it is a real executable
tex # initex -> tex
pdftex # etex -> pdftex
pdftex # latex -> pdftex
pdftex # pdflatex -> pdftex
pdftex # amstex -> pdftex
pdftex # csplain -> pdftex为什么会这样?因为 e-TeX 的扩展被其他引擎整个吸收了。 TeX Live 随附的 doc/etex/base/README(Karl Berry, 2024)说得毫不客气:多数发行版提供的 etex 可执行文件,是「以 DVI 模式运行的 pdfTeX 的化身」,这样做正是为了让超出 e-TeX 的那些扩展也能一并使用。同理,latex 并不是「老 TeX」,而是 DVI 模式的 pdfTeX——运行它,横幅会自报 This is pdfTeX, ...。正因为引擎与格式是彼此独立的两条轴,这种替换才成立。
plain TeX:一个没有 \documentclass 也没有 \begin{document} 的世界
plain TeX 是 Knuth 在《The TeXbook》中给出的标准格式。它把最低限度的工具——字体设置、基本数学符号、\bye 之类的便利宏——汇集到 plain.tex 里,随 TeX 本体一同分发。在 LaTeX 出现之前,这是使用 TeX 唯一现实的途径。在 TeX Live 2024 中,tex 引擎从 tex.ini 转储出这个格式,保存为 tex.fmt。
它的写法与 LaTeX 相当不同。行内公式同样是 $...$,但水平盒子写作 \hbox{...},垂直盒子写作 \vbox{...},表格对齐用 \halign,定义命令用 \def,正文宽度是 \hsize,文档以 \bye 结束。没有 \documentclass,也没有 \begin{document}——这些都是 LaTeX 用宏搭出来的。反过来说,这透露了一件要紧事:LaTeX 的种种便利属于格式的功能,而不是引擎的功能。
% plain TeX — process with: tex hello.tex (produces hello.dvi)
\hsize=10cm
\font\big=cmr10 at 17pt
{\big Hello, plain \TeX!}
\medskip
This paragraph is set in the default font.
Inline math works too: $E = mc^2$.
\bye用 tex hello.tex 处理它,会得到一个 DVI 文件——不是 PDF,下一节会讲到。\TeX 这类徽标宏和 \medskip(中等垂直间距)这类间距宏,同样由 plain TeX 定义。把它和一份 LaTeX 的 \documentclass 文档并排看,就能看清 plain TeX 给裸引擎披上的皮有多薄。
文学式编程与 WEB:TeX 自身的源码是怎么写的
TeX 本身是用 WEB 写成的,那是 Knuth 自己的系统,也是 文学式编程 的开山实践。WEB 源码是一份单一文档,其中面向人的说明与 Pascal 代码交织在一起。随后由两个工具从中抽取不同的产物:tangle 产出可编译的 Pascal,weave 产出排好版的注释文本,也就是《TeX: The Program》这本书。
最初的目标语言是 Pascal;在现代发行版中,一个叫 web2c 的工具把 WEB(经由 Pascal)翻译成 C 再编译。也就是说,今天在你机器上运行的 pdfTeX、XeTeX、LuaTeX,追溯到底仍然出自那一份文学式源码。TeX Live 的目录结构里反复出现 web2c 这个名字,原因就在这里。
收敛于 π 的版本号,与一个被冻结的程序
TeX 的版本号很特别。自版本 3 起,每次更新都在末尾添一位数字,于是这个号码渐近于圆周率 π。在 TeX Live 2024 附带的版本上执行 tex --version,它会回答 TeX 3.141592653。与之相伴的 METAFONT 则朝自然对数底 e 渐近——两件事合起来看,这个命名的玩心才显露出来。
而 Knuth 已经宣告:「绝对最后的一次修改」将在他身后进行,把版本号定为恰好的 π——用他自己的说法,届时所有残余的 bug 都将成为特性(Donald E. Knuth, “The future of TeX and METAFONT”, NTG 期刊 MAPS, 1990;后收入《Digital Typography》第 30 章)。这次冻结不是认输,而是一个设计判断。正因为地基不动,叠在它上面的格式和驱动才能放心演进。几十年前的 .tex 文件今天仍产生相同输出,正是这一判断的直接结果。
METAFONT 与 DVI:为什么 TeX 不直接输出 PDF
TeX 有一位孪生伴侣 METAFONT(命令名 mf):它把字体描述为绘制字形的程序,而不是固定的形状。Knuth 就是用它为 TeX 设计了整套 Computer Modern 字族。分工很清楚——TeX 决定字符放在哪里,METAFONT 生成字符本身的形状。
而 Knuth 的 TeX 直接吐出的不是 PDF,而是 DVI(DeVice Independent) 文件。DVI 里只有与输出设备无关的指令——「把这个字符放在这个位置」——再由 dvips 转成 PostScript,或由 dvipdfmx 转成 PDF。1982 年 PDF 还不存在,这自然是理所当然的设计;反过来看,这意味着输出格式改变时无需重写 TeX 本体。今天广泛使用的 pdfTeX、XeTeX、LuaTeX 的重大差别,就在于把这一步收进了内部,可以直接写出 PDF。
e-TeX 是什么:在不破坏冻结程序的前提下扩展它
e-TeX 是在被冻结的 TeX 之上追加新原语的严格超集扩展。既然 Knuth 冻结了 TeX,新功能就只能出自他人之手。于是在德语区 TeX 用户组 DANTE e.V. 的支持下,1992 年成立了 NTS(New Typesetting System)项目,e-TeX 便是它的第一项具体成果。实现者是慕尼黑马克斯·普朗克物理研究所的 Peter Breitenlohner(2015 年逝世),项目协调人则是 Philip Taylor。TeX Live 附带的 etex.1 手册页把目标表述为:延续并发展 TeX 的精神与理念,同时尊重 Knuth 希望 TeX 保持冻结的意愿。
所谓「严格超集」,是指原有输入一字不改地运行,输出完全相同。为保证这一点,e-TeX 有两种模式。在兼容模式(compatibility mode)下,它的行为与裸 TeX 完全一致;只有进入扩展模式(extended mode),新增的原语才可用。决定采用哪种模式的不是运行时,而是构建格式的时刻:转储时在格式源文件名前加一个 *,就选中了扩展模式。正如下一节所示,这个只有一个字符的约定,支撑着整个现代 LaTeX。
为什么你早已在不知不觉中使用 e-TeX
因为 LaTeX 的格式是在扩展模式下转储的。 这不是猜测,而是白纸黑字写在配置文件里。打开 TeX Live 2024 的 fmtutil.cnf,可以看到这一行:latex pdftex language.dat -translate-file=cp227.tcx *latex.ini,其中 *latex.ini 前面的 * 就是在指定扩展模式;同一个文件开头的注释明确说明,这个 * 本质上等同于 -etex 选项。相比之下,plain TeX 那一行是 tex tex - tex.ini:引擎是 tex,也没有 *。只有裸 TeX 不带 e-TeX 扩展。
从 LaTeX 一侧也能验证同一件事。内核文件 latex.ltx 开头附近有一道守卫:\ifx\eTeXversion\undefined \errmessage{LaTeX requires e-TeX}——没有这些扩展,格式根本无法生成。这一方针正式确立的时点也有据可查:LaTeX News 16(2003 年 12 月)预告说,大约两年内的发行版将转为以 e-TeX 为前提;LaTeX News 26 则宣布,自 2017 年 1 月的发行版起,生成格式必须有 e-TeX。运行 latex 时日志第三行出现的 entering extended mode,正是这项决定留下的可见痕迹。
# Ask each engine whether the e-TeX primitives exist.
$ cat > probe.tex <<'EOF'
\message{[eTeX=\ifx\eTeXversion\undefined none\else\number\eTeXversion\eTeXrevision\fi]}
\count300=7 % register 300 does not exist in Knuth's TeX
\end
EOF
$ tex probe.tex # Knuth's TeX: no extensions
[eTeX=none]
! Bad register code (300).
$ pdftex probe.tex # pdfTeX: e-TeX 2.6 built in, 32768 registers
[eTeX=2.6]
$ latex probe.tex | head -3
This is pdfTeX, Version 3.141592653-2.6-1.40.26 (TeX Live 2024) (preloaded format=latex)
restricted \write18 enabled.
entering extended mode最后那条横幅,正是本页主题浓缩成的一行。版本串 3.141592653-2.6-1.40.26 有三层:Knuth 的 TeX 是 3.141592653,e-TeX 是 2.6,pdfTeX 是 1.40.26——而 (preloaded format=latex) 给出的是格式的名字。于是一行之内同时记下了引擎的血统与格式的选择。养成读日志首行的习惯,协作中「同一条命令,结果却不同」的老难题几秒钟就能定位。
e-TeX 添加的原语
e-TeX 的新增之处,对写宏的人最为受用。其中最大的一项是整数、尺寸与胶的算术。在裸 TeX 中,每次计算都得腾挪临时寄存器;e-TeX 则提供了 \numexpr、\dimexpr、\glueexpr,可以当场求出 (a+b)*c/d 这样的表达式,而且是可展开的形式——所以在 \edef 或 \write 内部它会直接变成数值。
% On any e-TeX engine in extended mode (pdftex, xetex, luatex, euptex).
\count0=\numexpr (3+4)*2/7 \relax % yields 2
% Branch safely on whether a name is defined.
\ifdefined\foo \message{foo exists}\else \message{no foo}\fi
% Test a control sequence without creating it.
\ifcsname chapter\endcsname \message{chapter is defined}\fi
% Negate a conditional directly, instead of swapping the branches.
\unless\ifnum\count0>10 \message{count0 is not greater than 10}\fi第二根支柱是条件判断与记号操作。\ifdefined 检测一个控制序列是否已定义,\ifcsname...\endcsname 则检测由名字拼出的控制序列是否存在。二者的要害在于没有副作用:裸 TeX 里惯用的 \ifx 老手法,有时会悄悄把未定义的控制序列变成 \relax。\unless 可反转任意条件,省去互换两个分支的例行功夫;\detokenize 把记号列变成字符串形式,也就是类别码 12 的字符。
| 原语 | 作用 |
|---|---|
\numexpr | 就地求值整数表达式(可展开):\numexpr (3+4)*2/7 \relax |
\dimexpr / \glueexpr | 对尺寸与胶做同样的事:\dimexpr \textwidth/3 \relax |
\ifdefined | 检测控制序列是否已定义,且无副作用 |
\ifcsname | 检测由名字拼出的控制序列是否存在,而不创建它 |
\unless | 反转紧随其后的条件的真假:\unless\ifnum ... |
\protected | 定义一个不会在 \edef 或 \write 中擅自展开的宏 |
\detokenize / \unexpanded | 把记号列变成字符串/让记号列保持不展开 |
\scantokens / \readline | 把字符串当作输入重新读取/逐字读入一整行输入 |
\middle | 在 \left … \right 中间放置一个可伸缩的定界符 |
\currentgrouplevel | 返回当前所处的分组层数(\interactionmode 用于读写交互模式) |
展开控制也得到加强。\unexpanded 让内容原样留下而不展开,\protected 则定义一个在展开语境中不会擅自展开的宏。后者是在底层正确实现 LaTeX 的 \protect 的关键,使脆弱命令写进 \edef 或 \write 时不再损坏。在数学里,\middle 大显身手:写成 \left( … \middle| … \right),中间那道竖线就会长到与两侧括号同高。裸 TeX 里根本没有「式子中间的定界符」这个概念。
不起眼却决定性的一项,是寄存器的大幅扩容。裸 TeX 的 \count、\dimen、\skip、\toks 之类各自只有 256 个——上面实验里 tex 报出的 ! Bad register code (300). 正是这个缘故。e-TeX 把上限提到 32768,而且以稀疏数组分配,未用到的部分不占内存。今天动辄载入几十个大型文档类与宏包的 LaTeX 文档不会因寄存器耗尽而卡住,靠的就是这项扩展。
此外,e-TeX 还带来了双向排版(自右向左书写体系)的机制。这部分源自 Breitenlohner 的 TeX--XeT,并延伸到后来 XeTeX、LuaTeX 以及日文处理的工作中。而许多现代宏包,离开这些原语根本无从谈起。LaTeX 的编程层 expl3 就是显例:若没有 \ifcsname 和 \protected,那套庞大的宏体系压根写不出来。
懂得这一层之后,你能读懂什么
你能看到错误信息的底层。 LaTeX 发出的许多警告,其实是用下面这一层的词汇写的。Overfull \hbox (12.3pt too wide) 的意思是「内容从行这个水平盒子里溢出了 12.3pt」;只要知道 \hbox 是盒子,就能把原因缩小到长 URL 或无法断行的词。! Missing number, treated as zero. 则表示 TeX 期待一个能读作数字或尺寸的东西却没找到——通常是长度写错了。
不过,读懂这一层与在这一层动手写是两回事。标题、列表、交叉引用、定理环境交给 LaTeX 的结构化命令;低层命令则留给「读懂哪里坏了」和「小范围改动文档类」这两种场合。哪怕是定义新命令,普通文档也更适合用 \newcommand 或 \NewDocumentCommand 而非裸的 \def,因为它们会检查参数个数,并提示与既有命令的冲突。
- 在写正文时 — 用 LaTeX 的结构化命令,尽量少用底层原语。
- 在修文档类或宏包时 —
\ifdefined、\numexpr之类的 e-TeX 工具,比老式 TeX 的小花招更安全、更易读。 - 在读日志时 — 把警告翻译成 TeX 的盒子、胶、寄存器这套词汇,原因就浮现出来。
- 在需要可复现时 — 引擎是冻结的,旧文档仍会产生相同结果。真正会变的是格式与宏包的版本,那才是要钉住的东西。