TeX 引擎如何读取和渲染输入流?

TeX 引擎如何读取和渲染输入流?

我是 TeX 编程新手,还没有读完 TeXBook。我需要一个关于 TeX 引擎如何读取、处理和生成输出的快速教程。

作为新手,当考虑任何接收输入流并产生输出流的黑匣子时,我的思维模型如下。

黑匣子

  1. 占用一段固定长度的字符,
  2. 处理所获取的字符块,
  3. 将处理好的块保存在文件中,
  4. 执行 1、2、3,直到所有输入的字符流都被处理。

TeX 引擎是否使用逐块处理机制?还是像下面这样从头到尾进行多次处理?

TeX 引擎

  1. 从头到尾扫描所有输入流(在输入文件中),
  2. 进行第一次扩展。
  3. 重复 1 然后 2 进行第 n 次扩展,直到无法再进行扩展。
  4. 将渲染的输出保存为文件,例如 PDF 格式。

这个问题的答案确实帮助我比阅读 TeXBook 更快地学习纯 TeX。

编辑 1 (2014 年 9 月 9 日)

我认为,与其产生新的相关问题,不如直接编辑这个问题。

如果没有例子的话,我仍然很难理解 TeX 引擎的算法工作原理。这就是为什么我在这里提供一个简单的例子。

这些评论unit *是特意添加的,以便于引用。

% unit A
\def\aa[#1]{a's value: #1}
\def\bb{[Hi!]}
\def\cc{\bb}
\def\dd{\cc}

% unit B
\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter
\expandafter
\aa\dd


% unit C
\expandafter\expandafter
\expandafter
\aa\cc

% unit D
\expandafter
\aa\bb

\bye 

你能用这个例子详细说明你的答案吗?我想知道的要点:

  1. TeX 如何知道何时进行下一个unit
  2. 我的理解是否正确:扫描从上到下开始,在前一个单元完全扩展和执行后继续到下一个单元?我的意思是,当处理unit B完成其扩展和执行时,其余单元(CD)不受影响。

答案1

TeX 有三种操作模式:(1)将输入流转换为标记,(2)扩展标记,(3)执行完整命令(由标记组成)。

更详细地说,它首先准备一行输入,删除 EOL(取决于操作系统)和行尾的所有空格(通常还有制表符)。然后添加自己的行尾字符( 的值\endlinechar,通常是 ctrl-M)。

然后,它一次读取一行中的一个字符,直到获得一个完整的标记(通常是一个字符或一个控制序列)。该标记被传递到阶段 (2),即宏扩展引擎,后者会对其进行扩展(如果可以的话)或从阶段 (1) 请求更多标记(如果扩展过程需要的话),或者将其传递到阶段 (3),即执行过程。此过程要么执行命令,要么从阶段 (2) 请求更多标记,然后可能必须从阶段 (1) 请求更多标记。

标记化阶段将大多数字符转换为由字符代码/类别代码对组成的标记。由于执行引擎能够为字符分配新的类别代码,因此它可以影响阶段 (1) 的行为。

一个轻微的复杂情况是行内存在换行符。TeX 认为这是行的结束,并忽略行的其余部分。注释字符(通常为%)也会导致行的其余部分被忽略,包括换行符。

请注意,标记化发生在一行内(您不能在一行上开始一个宏标记而在下一行上完成它),但扩展 - 步骤(2) - 可以请求更多标记,并且可以继续到下一行。

我不知道您是否可以将这个过程放在您的两种方案中。“块”要么是行,要么是标记,两者都没有恒定的长度,因此第一个描述是不合适的。当然,TeX 不会一次读取整个文件(除非它可能为了提高效率而将其从磁盘移动到输入缓冲区)。

至于输出结果:TeX 每次将一页发送到输出。它会在处理完每个段落后评估是否已获得整页。当它将段落分成行(以及连字符和边距对齐)时,它会一次对整个段落进行操作。

答案2

以这个例子(按照编辑)为例,我将开始对前几行进行非常详细的介绍,然后再快速地讲解。已经注意到,需要进行一些处理才能使“行”标准化:我假设我们可以继续逐个字符地读取每一行。

这里的底线是 TeX 从头开始​​读取输入,一次处理一个字符以进行标记化。(正如前面所指出的,逐行处理是规范化行尾所必需的,但通常不是“TeX 结尾”的关注点。)然后根据需要进一步处理每个标记。

单元 A

% unit A
\def\aa[#1]{a's value: #1}
\def\bb{[Hi!]}
\def\cc{\bb}
\def\dd{\cc}

第一行以 开头%,TeX 会读取它并将其标记为注释字符。这意味着它会跳过该行的其余部分。

第二行以 开头\,它是转义字符。因此,TeX 继续读取以查找控制序列:单个非字母字符或一个或多个“字母”字符。这实际上是一次完成一个,但是我们再次跳到结果:一个名为 的标记\def。 (后面跟着一个\,暂时保持不变:在这个阶段,重要的是它不是字母,因此终止控制字的形成。)TeX 现在查找 的定义以\def查看要做什么:它可能是可扩展的,可能是可执行的,可能是隐式字符(比如\let\foo=a),或者可能是未定义的。在这种情况下,它是一个被执行的原语。\def原语后面需要跟着一个要定义的标记,可能是一个参数文本,然后是替换文本。因此,TeX 需要找到这些,这意味着更多的标记化。

之后的第一件事\def如前所述\。这将启动一个控制字,因此 TeX 再次构建一个标记,一次一个字符:它发现\aa以 结尾[。因此\def将定义一个名为 的宏\aa。TeX 现在标记[,并且由于它不是起始组标记,因此它成为参数文本的第一个字符。TeX 继续工作,在 之前对#1和进行标记。这]{一个起始组标记,因此参数文本是完整的并且是[#1]。因此\aa将被定义为它必须后跟一个[(catcode 12),然后是一个变量部分(#1),然后是一个](catcode 12)。TeX 现在正在为构建替换文本\aa。它看到了{并且现在开始对内容进行标记。这里有一个括号匹配要求,所以我们必须记住 TeX 读取的每个标记可能是}或等效的,但没有进行扩展。因此\aa最终得到替换文本a's value: #1其中是使用时介于和#1之间的任何内容的占位符。[]\aa

后面}是一个空格(TeX 将行尾转换为空格)。由于我们处于垂直模式,空格不执行任何操作,因此我们可以继续。其余定义行可以以相同的方式进行分析:每次读取一个字符,然后进行标记、执行,ETC。,但希望写得清楚,但写起来相当乏味!

在定义 的行之后\dd,有一个空行,TeX 将其转换为名为 的标记\par,然后将其插入并执行(我假设这里的第一行之前没有代码:\par也可能是要扩展的宏)。我们处于垂直模式,因此\par原语(我假设)不会做任何重要的事情。

B 单元

% unit B
\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter
\expandafter
\aa\dd

单元 B 再次以注释行开始:我将跳过这一行以及未来的注释行。然后 TeX\再次找到 ,经过一番努力后得到了\expandafter它所查找的标记 。这是一个可扩展的标记,它使 TeX 跳过下一个标记并(尝试)扩展下一个标记。要做到这一点,TeX 显然必须找到接下来的两个标记,这就是 所做的:两者都是\expandafter。在这个阶段,生活变得更有趣了。由于要处理的新标记也是可扩展的,因此该过程继续。您有一个链条:

\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter
\expandafter
\aa\dd

如果你仔细阅读,你会发现 TeX 从最开始的那个到\expandafter\dd现在,\dd是一个没有参数但带有替换文本的宏,因此 TeX 会执行恰好一个扩展,替换\dd\cc有效地给我们

\expandafter\expandafter\expandafter\aa\cc

(仔细按照扩展即可看到这一点!)

(注意:在上面我忽略了行尾。正如前面提到的,TeX 已将行尾转换为空格,并且在控制字之后 TeX 会跳过空格。因此,它们在计算下一个标记时不“计算”空格。)

TeX 已经将上述内容作为第一个\expandafter链的一部分进行了标记,因此这里不存在有关标记的问题。我们有另一个\expandafter,因此重复相同的过程,这次发现\cc它又是一个宏,因此被替换,这给了我们有效的

\expandafter\aa\bb

仍然有一个\expandafter,所以再次进行替换以给出

\aa[Hi]

TeX 现在必须扩展\aa,但这次有一个参数 text 允许。因此 TeX 将输入与要求进行匹配:有一个[带有 catcode 12 的,有一个 'some stuff' ( Hi!),然后有一个]带有 catcode 12 的。TeX 现在插入替换文本,替换为\aa,因此我们有#1Hi!

a's value: Hi!

这个字母a只是一个字母:读取它会迫使 TeX 切换到水平模式。然后 TeX 插入\everypar标记参数(此处为空),然后开始收集材料以构建段落。由于现在所有内容都是“文本”,因此在我们到达空行之前不会发生任何“新”事情。这会插入一个\par标记,这又是原始标记。由于我们处于水平模式,这将启动段落构建算法。这是一个独立的主题:目前,我希望我们可以认为 TeX 构建了一个段落,插入了各种跳过,检查段落是否适合当前页面而不会中断,并在这样做时将其添加到“当前页面”材料中。它没有将其添加到 PDF/DVI。

C 和 D 单元

% unit C
\expandafter\expandafter
\expandafter
\aa\cc

% unit D
\expandafter
\aa\bb

这些与单元 A 非常相似。完全相同的分析适用,只是扩展轮数更少。正如我上面提到的,每次\expandafter扩展时,我们都可以将输入视为“重写”和“简化”,因为这基本上就是发生的事情。因此,除了不那么复杂之外,这里没有什么新东西可说。

最后一行

\bye 

这里的最后一条指令是bye。这是完成运行的整理操作。特别是,它将确保在完成 DVI/PDF 文件之前实际发送出“当前页面”的任何材料。因此,对于此处的简短演示,这是唯一会\shipout发生任何操作的地方。纯 TeX 输出例程非常简单,但它确实会向当前页面添加页码等。因此,\bye最好还是单独分析一下。

答案3

所有流程均由约瑟夫·赖特。也许只是 \expandafter需要多解释一下。

的扩展\expandafter <token1><token2>如下:

  • 保存<token1>到 TBRA(=“待再次阅读队列”)。
  • 扩展<token2>(即将其替换为“扩展材料”,如果不可扩展则“扩展材料”= <token2>
  • 回到阅读TBRA+“扩展材料”(按此顺序)并再次处理扩展。

此例程可以递归运行。请注意,TBRA:

  • 队列先进先出, 不
  • 仅在例程的最内层递归级别上才为非空\expandafter
  • 读取时会被清空(当然)。

请注意,TBRA 的内容在出现特殊的 TeX 错误时会显示(但对于这种类型的 TBRA 则不会特别显示\expandafters)。

我会解释我们的B单元从这个角度来看。令牌处理器创建一个令牌队列(没有线结构),这些令牌下面的符号表示:

  • TBRA = 令牌已保存到 TBRA,
  • EX = 令牌已扩展(即,在扩展期间被删除)。

现在,你的例子:

\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\aa\dd
   EX          TBRA         EX          TBRA        EX          TBRA       EX       TBRA EX

expanded material=  \cc
TBRA= \expandafter\expandafter\expandafter\aa

TBRA + expanded material: \expandafter\expandafter\expandafter\aa\cc
                             EX         TBRA         EX       TBRA EX

expanded material=  \bb
TBRA=  \expandafter\aa

TBRA + expanded material= \expandafer\aa\bb
                            EX       TBRA EX

expanded material=  [Hi!]

TBRA + expanded material:  \aa [Hi!]
                            EX

expanded material:  a's value: Hi!   (TBRA is empty now)

整个输入队列由 TBRA + “​​扩展材料” + “来自文件的输入”组成。“扩展材料”在扩展过程中可能会发生变化,只有“来自文件的输入”才是标记处理器实际标记的对象。

在我看来,处理器有四个级别

  • 输入处理器:读取 OS 中定义的行,进行编码转换,通过删除 EOL 和 EOL 之前的空格并添加 来输出独立于 OS 的行缓冲区\endlinechar
  • 令牌处理器:从输入处理器读取行缓冲区并创建标记队列(不进行任何行分段)。标记处理器的行为是另一回事。
  • 扩展处理器:扩展可扩展标记并输出不可扩展标记。
  • 主处理器:执行不可扩展的原始命令及其定义明确的参数,或执行字符标记作为“打印”。但这更复杂,它取决于上下文,也是另一回事。

首先,主处理器启动。它很饿(TeXbook 的术语是胃),所以它向扩展处理器请求第一个不可扩展的标记。此时它的输入端没有数据,所以它向扩展处理器请求标记。标记处理器没有准备好的行缓冲区,所以它向输入处理器请求第一个行缓冲区。输入处理器从输入文件中读取一行。每一级处理器都存储在其输入端只需极少量的数据即可满足其高级处理器的要求. 对内部 TeX 寄存器的分配、控制序列的设置含义等都是在主处理器级别完成的,这些值的改变可能会影响另一个处理器的行为(\endlinechar\catcode\def)。

答案4

这更像是一条评论而不是答案,但它并不合适。

我最近遇到了一个与\expandafters 相关的问题,并试图轻松地了解它是如何工作的,所以我将在这里讲述我的“过程”(尽管它并没有经过很好的测试)。

问题是,您如何“知道”许多 expandafter 会发生什么?好吧,这是我的过程。我在宏后面用数字注释了发生的扩展次数。

这个过程“相对简单”,你删除\expandafters 的矩形列,查看结果展开(并添加数字来表示)。然后你删除\expandafters 的“第一列”并重新开始 :)

D 单元

\expandafter
\aa
  \bb

无需移除任何块,只需在离开并执行之前查看结果是否扩展\bb一次即可。因此\aa

\aa
  \bb1

C 单元

\expandafter\expandafter
\expandafter\aa
  \cc

删除 expandafters 块(1 列乘 2 行)并添加一个数字,然后删除整个块(恰好是一列,因此所有内容都消失了):

\expandafter
\aa
  \cc1

按照上面的解释,结果是

\aa
  \cc2

B 单元

\expandafter\expandafter
\expandafter\expandafter
\expandafter\expandafter
\expandafter\aa
  \dd

第一级:查看块(1 列,4 行)后,您会看到它\dd已扩展,因此删除一列(然后添加数字),因此:

\expandafter
\expandafter
\expandafter
\aa
  \dd1

再次进行同样的工作,重新组织以便更好地查看:

\expandafter\expandafter
\expandafter\aa
  \dd1

最后,

\expandafter
\aa
  \dd2

最后离开

\aa
  \dd3

\dd\aa在执行之前要扩展三次。

如果不清楚,请询问。

添加

这是 Knuth 的一个著名例子。在这种情况下,不是扩展的数量,而是扩展的顺序。所以这里的数字代表顺序:

\def\z{M}
\def\Z{C}
\def\a{\z er}
\def\b{ry }
\def\c{\Z hr}
\def\d{ist}
\def\e{mas}

\expandafter\expandafter
\expandafter\expandafter
\expandafter\expandafter
\expandafter\a
\expandafter\expandafter
\expandafter\b
\expandafter\c
  \d
    \e

在 expandafters 第一行之后

\expandafter
\expandafter
\expandafter
\a
\expandafter
\b
\c
  \d1
    \e

然后(重组)

\expandafter\expandafter
\expandafter\a
\expandafter\b
  \c
    \d1
      \e

\expandafter\a
  \b
    \c2
      \d1
        \e

最后

\a4
  \b3
    \c2
      \d1
        \e

因此,首先,\d然后,,,\c(永远不会被 expandafters 触及)。\b\a\e

相关内容