我开始阅读TeX 按主题分类作者 Victor Eijkhout他首先描述了四种引擎:
输入处理器。这是 TeX 的一部分,它从运行 TeX 的任何计算机的文件系统中接受输入行,并将它们转换为标记。标记是 TeX 的内部对象:有构成排版文本的字符标记,以及作为下两级要处理的命令的控制序列标记。
扩展处理器。第一级生成的部分(但不是全部)标记(宏、条件和一些原始 TeX 命令)需要进行扩展。扩展是用其他(或不)标记替换部分(标记序列)标记的过程。
执行处理器。不可扩展的控制序列是可执行的,并且此执行发生在 TeX 处理器的第三级。此处活动的一部分涉及对 TeX 内部状态的更改:分配(包括宏定义)是此类别中的典型活动。此级别上发生的另一件大事是水平、垂直和数学列表的构建。
视觉处理器。在最终的处理级别,执行 TeX 处理的视觉部分。在这里,水平列表被分成段落,垂直列表被分成页面,并且公式由数学列表构建而成。此外,dvi 文件的输出也在此级别进行。此处运行的算法对用户来说是不可访问的,但它们可能会受到许多参数的影响。
作者作出了如下陈述:“对于许多目的来说,最方便、最有洞察力的做法是将这四个层次的处理视为一个接一个地发生,每个层次都接受完全的前一级别的输出”。
我不明白“每个人接受完全的扩展处理器(第 2 级)需要由执行处理器(第 3 级)分配的宏定义,因此“扩展处理器(第 2 级)需要上一级的输出”是可能的。我给本书的作者发了一封电子邮件,询问这是如何实现的,但没有得到回复。有人能解释一下吗?
更新
我认为我可以接受这本书的模式,如果我们重新考虑一下完全的意思。作者后面举了一个例子:
\def\DoAssign{\count42=800}
鉴于上述呼吁
\DoAssign 0
生产
\count42=8000
发生这种情况是因为空格\DoAssign
被输入处理器删除了。从这个意义上说,完全的在下一阶段之前。而不是:“\DoAssign
得到处理,\count42
被分配800
,然后输入从最后一个继续0
。”
答案1
评论太长了。这是一项教学资产,使理解更容易。所有四个处理器在 TeX 中共存,它们的执行是交错的,而且很难一步一步解释,因此更简单的“先读取,然后展开,然后执行,然后排版”模型对于理解事物的工作原理非常有用。
例如,简单的一行
\edef\x{\if\string}!{\else a\fi}\x
练习所有四个处理器。只要具备最基本的 TeX 编程知识,您就可以将该行拆分为 5 个步骤(具体步骤多少取决于您希望解释的精细程度):
- (3)执行处理器启动
\edef
并开始扩展内部内容; - 扩展 (2) 处理器扩展
\if
,为假,跳至\else
并返回a
; - 执行(3)处理器分配
a
给\x
; - 扩展(2)处理器扩展
\x
为a
; - 视觉(4)处理器排版
a
。
您已经看到,这已经证明了“完成输出”的说法是不准确的,因为输入处理器没有发挥作用(至少这是我的有偏见的列表告诉您的),并且 (2) 和 (3) 是交错的。尽管如此,它在某种程度上是正确的,并总结了发生的事情。
该过程更准确(但仍缺少一些步骤)的描述是:
- (1)扫描
\edef
; - (3)开始分配,并再次调用(1)来扫描控制序列;
- (1)扫描
\x
; - (1)扫描
{
; - (2)开始向前扩张;
- (1)扫描
\if
; - (2)扩展
\if
,开始扩展寻找两个标记; - (1)扫描
\string
; - (2)扩展
\string
将下一个标记变成 catcode 12 标记; - (1)扫描
}
12(\string
使其成为 catcode 12,因此输入处理器永远不会看到 1}
); - (1)扫描
!
; - (2)
\if
看到}!
并给出错误,因此 TeX 的扫描仪跳过直到出现\else
或\fi
; - (1)看到
{
1并跳过; - (1)
\else
被看见; - (2)
\else
被删除; - (2)
a
被看到并传递给(3); - (2)
\fi
被看见; - (2)
\fi
被删除; - (1)看到
}
1,表示 的结束\edef
; - (3)相当于
\def\x{a}
执行; - (1)扫描
\x
; - (2)扩展
\x
为a
; - (3)执行
a
; - (4)添加
a
到当前hlist;
这是一个很多比较简单的版本更难理解(并且要花更长的时间来阅读),证明简单的模型“最方便,最有见地”。
答案2
举一个简单的例子,考虑仅包含以下内容的行:
a~b
现在我们可以想象事情是这样的:
输入处理器将这三个字符转换为三个标记:(11,'a')、(13,'~') 和 (11,'b')。(回想一下,11 是“字母”的类别代码,13 是“活动字符”的类别代码。)
扩展处理器将“~”扩展为
\penalty 10000 \
(比如说),这样我们现在就有了对应于的标记a\penalty 10000\ b
。执行处理器将其转换为水平列表。
视觉处理器将此段落(假设它到此结束)分成几行,并输出到DVI等。
在这里,我们假设每个阶段都得到前一阶段的完整输出:输入处理器对整行进行工作,然后扩展对输入处理器生成的标记进行工作,等等。对于许多简单的情况,这种理解就足够了。
(显然,遇到的宏定义在使用之前需要进行“赋值”,但即使在这种情况下,对每行或“块”按顺序发生的所有这四个处理器的简单描述就足够了。)
实际上,该程序内部仅具有一个主控制循环,类似(以类似 Python 的伪代码):
while True:
t = get_x_token()
...
它一次“请求”一个扩展标记,因此在需要时从输入文件读入行(当当前行结束并请求另一个标记时,读取下一行),并维护输入堆栈,并在必要时进行扩展,并且当遇到“~”时,字符(来自当前字体)“a”已经被放入水平列表中并且尚未扩展,并由\penalty
操作过程处理等 - 一切都是交错的,但这是一个复杂得多的图景。
答案3
好吧,我觉得有必要发表自己的答案,因为我不明白为什么人们要向我解释解析的工作原理以及简化的强大功能。我当了几十年的软件工程师,我知道。
问题是关于这个词的意思完全的以及为什么以这种方式思考解析的阶段是很有见地的。我在作者在书中后面给出的一个例子中找到了答案。我的困惑源于这样一个事实:我考虑了完全的这意味着已完全处理全部输入。这似乎不是作者的意思。请考虑以下内容(书中的一个例子):
\def\DoAssign{\count42=800}
鉴于此定义,以下输入
\DoAssign 0
将产生以下分配
\count42=8000
出现这种违反直觉的结果是因为空格被\DoAssign
输入处理器删除了。从这个意义上说输入处理器有完全的在下一阶段之前完成其工作。而不是:“\DoAssign
得到处理,\count42
被分配800
,然后输入从空间和后续部分继续0
。”