> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/53-what-is-a-tex-token.md).

# 什么是“TeX token”？

## 关于 TeX 记号及相关概念系列文章的写作动机

本文讨论撰写一系列关于 TeX 记号及相关概念文章的动机和所采用的方法。 [新系列文章：TeX 记号及相关概念——但为什么（以及如何）？](https://www.overleaf.com/blog/521-a-new-series-of-articles-tex-tokens-and-related-concepts-but-why-and-how) 正如那篇文章所指出的，在整个系列中，我们的讨论和解释都基于对 Knuth 原始 TeX 程序的自定义构建所获得的见解——用它来生成一系列文章，旨在对关键的 TeX 概念提供简单的描述和易于理解的解释。

## 引言：我们的目标是什么？

在本文中，我们通过追踪从输入文件中的字符到 TeX 记号实际生成的处理过程，准确地弄清楚什么是 TeX 记号。实际上，这个过程相当复杂，因此我们把它简化到核心要点，力求在保持技术准确性的同时，让它更容易跟随和理解。

我们先介绍一些重要的 TeX 内部概念： *原语*, *命令代码* 和 *命令修饰符*。然后，我们使用一个非常简单的宏示例，来准确查看 TeX 如何处理该命令 `\def` 以及 TeX 为表示该命令而创建的相应记号。

最后，我们简要看看 TeX 如何创建用于表示字符的记号，以及字符的 `\catcode` 确实会永久附着到字符记号上——这一点在关于 TeX 的书中经常被提到，但这里我们将准确看到它是如何实现的。

下图展示了我们将要概述的旅程——从输入文本到 TeX 记号：

![从 TeX 输入到 TeX 记号的旅程。](/files/ff2789dc3c98fb880dad923724f23b10bb12db86)

## 但首先：原语和命令代码

每个 TeX 引擎（Knuthian TeX、pdfTeX、XeTeX、LuaTeX）都能理解若干内建命令：所谓的 *原语*——TeX 可编程性的基础构件命令。它们之所以被称为“原语”，是因为与用户定义的宏不同，它们不是由其他命令构造出来的，也不能进一步分解为更简单的指令。对于 Knuth 的 TeX 来说，大约有 320 个原语——不过需要注意的是，pdfTeX、XeTeX 和 LuaTeX 等其他 TeX 引擎都在 Knuth 的原始程序之上添加了新命令，因此会包含 Knuth 的 TeX 软件中没有的原语。

在内部，TeX 会为所有命令分配一个数值 *命令码* ——无论它们是用户定义的宏还是内建原语。这些命令代码对 TeX 用户不可见，它们只是 TeX 处理过程内部机制的一部分，但为了后面讨论 TeX 记号，了解它们是有用的。

具有相关功能的一组命令会共享相同的命令代码。例如， `\def`, `\gdef`, `\edef` 和 `\xdef` 这些原语都用于定义宏，并共享 97 这个命令代码（在 Knuth 的 TeX 中）。显然，这 4 个宏定义命令各自以略有不同的方式创建宏；因此，在处理过程中，TeX 需要一种方法来区分它们。

单独一个命令代码（如 97）并不能告诉你正在讨论的是哪一个宏创建命令；因此，正如你所预期的，每个 TeX 命令还会被分配另一条附加信息，称为它的 *命令修饰符* （见下例）。

### 命令修饰符：两种类型

命令修饰符分为两类，我们将其称为“类型 1”和“类型 2”——TeX 并不使用这种术语，这里只是为了方便：

* **类型 1**：简单的整数值，TeX 在需要时可以用它来区分共享相同命令代码的命令。
* **类型 2**：一个整数值，它是 TeX 内存中的一个数字位置，告诉 TeX 需要去哪里查找该命令的信息。例如，这适用于用户定义的命令（宏），其中命令修饰符告诉 TeX 宏定义存储在内存中的位置。

#### 类型 1 命令修饰符（示例）

如前所述，在 Knuth 的 TeX 中，用于定义宏的四个原语命令： `\def`, `\gdef`, `\edef`, `\xdef` 都共享 97 这个命令代码：它们通过命令修饰符来区分，下面的表格列出了这些修饰符：

| 命令      | <p>命令<br>代码</p> | <p>命令<br>修饰符</p> |
| ------- | --------------- | ---------------- |
| `\def`  | 97              | 0                |
| `\gdef` | 97              | 1                |
| `\edef` | 97              | 2                |
| `\xdef` | 97              | 3                |

作为第二个示例，Knuth 决定把命令 `\openout`, `\write`, `\closeout`, `\special`, `\immediate` 和 `\setlanguage` 实现为 TeX 的“扩展”，纯粹是为了展示如何向 TeX 添加新的原语。在这种情况下，这些命令其实并没有“相似功能”，只是 Knuth 为了解释如何扩展 TeX，才把它们分在一起。这 6 个命令被归类为“扩展”，并归为命令代码值 59，但每一个都具有合适的命令修饰符以区别于其他命令：

| 命令             | <p>命令<br>代码</p> | <p>命令<br>修饰符</p> |
| -------------- | --------------- | ---------------- |
| `\openout`     | 59              | 0                |
| `\write`       | 59              | 1                |
| `\closeout`    | 59              | 2                |
| `\special`     | 59              | 3                |
| `\immediate`   | 59              | 4                |
| `\setlanguage` | 59              | 5                |

#### 类型 2 命令修饰符（简要说明）

虽然所有命令修饰符都是整数，但类型 2 修饰符需要多做一些说明。在 TeX 中，这些命令修饰符被称为“指针”，因为它们指向内存中的一个位置，TeX 可以在那里找到该命令的附加信息。这听起来可能有点模糊，但 TeX 使用这些指针查找信息的方式相当多样，若做更全面的解释会偏离本文的核心目标。一个例子就能说明问题：宏。当一个宏命令被定义时，TeX 需要把替换文本存放在内存中的某处。如下文所示，用户定义的宏具有 111 到 114 之间的命令代码，并带有一个指向内存的命令修饰符，告诉 TeX 其替换文本（宏定义）存储在哪里。

### 命令代码：可展开和不可展开

在 Knuth 的 TeX 源代码中，命令代码的范围从 0 到 120——注意，该范围内的一些代码纯粹用于特定的内部用途，并不会分配给用户可访问的命令。值得注意的是，pdfTeX、XeTeX 和 LuaTeX 等其他 TeX 引擎都在 Knuth 的原始集合上增加了新命令，因此会包含更多原语及相应的命令代码；不过，这里概述的原理是所有源自 Knuth 源代码的 TeX 基础引擎的核心。

命令代码集合可分为两大类：

* *不可展开命令*：命令代码小于或等于 100；
* *可展开命令*：命令代码大于 100，最大到 120。101 到 120 的范围包括用户定义的宏，以及诸如 `\csname`, `\expandafter` 和 `\the`.

不可展开命令通常会对内部参数进行赋值，或者直接生成可用于排版的内容。可展开命令通常会将一串记号“注入”到 TeX 当前的处理活动中，或者改变记号处理的顺序。

如上所述，所有宏（用户定义的命令）都被赋予 111 到 114 之间的命令代码：不同的值反映了该宏被定义为 `\long`, `\outer`、二者兼有，或二者皆非。以下是一个示例：

| 宏类型            | 示例                             | 说明                 |
| -------------- | ------------------------------ | ------------------ |
| 非 long、非 outer | `\def\ohyeah{....}`            | `\ohyeah` 命令代码=111 |
| long、非 outer   | `\long\def\ohyeah{....}`       | `\ohyeah` 命令代码=112 |
| 非 long、outer   | `\outer\def\ohyeah{....}`      | `\ohyeah` 命令代码=113 |
| long、outer     | `\long\outer\def\ohyeah{....}` | `\ohyeah` 命令代码=114 |

提醒一下命令修饰符：当宏被定义时，TeX 会把宏的定义存储在内存中的某个位置：那个位置（一个指针）将成为该宏命令的命令修饰符，而该宏命令将根据其定义方式被存储为 111 到 114 之间的某个命令。分配给用户定义宏的实际名称其实并不重要：在处理输入之后，它们都会被分配一个 111–114 之间的命令代码，最终，TeX 从你的输入中读取的所有命令，无论是原语还是用户定义宏，都会被转换为一种数值表示，称为一个 *记号*.

## 从输入文本到 TeX 记号的旅程

在这一节中，我们将使用一个非常简单的宏示例，准确查看 TeX 如何处理命令 `\def` 以创建一个表示该 `\def` 命令的记号。TeX 的详细处理活动可能极其复杂，因此我们没有使用宏参数或分隔符，因为那会增加复杂性并分散我们对这段旅程的关注。

假设你的 TeX 输入文件包含以下一行：

```latex
\def\ohyeah{Overleaf is cool!}
```

当 TeX 开始处理这一行输入时，它会检查 `\catcode` 中每个字符的类别码，并发现第一个字符是 `\` （……的第一个字符 `\def`）。它检测到（在内部表中查找）该 `\` 具有 `\catcode` 0，这意味着它表示一个 *控制序列*的开始。当然，你可以把任何字符重新定义为具有 `\catcode` 0，但我们这里假定使用的是 plain TeX 或 LaTeX 的常规定义。

严格来说，术语 *控制序列* 有两个子类别： *控制词* 和 *控制符号*:

* *控制词*：一串字符，其 `\catcode` 类别码为 letter（11）；
* *控制符号*：一个单个字符，其 `\catcode` 是 *不* 类别码为 letter（11）。

此时， `\` 转义字符已经完成了它的任务，现在不再继续。检测到转义字符后，TeX 的响应是开始读取输入中后续的所有字符，以检测控制词或控制符号。

在最初的 `\`，TeX 立刻检测到 `d`：一个字符，其 `\catcode` 为 11，这告诉 TeX 它已找到一个 *控制词*的第一个字母。它继续扫描后续字符，直到最终检测到一个字符，该字符 *不会* 的用户已 `\catcode` 类别码为 letter（11）。初始 `\`）之后的所有字符其 `\catcode` 11（letter）被视为构成控制词的名称：也就是一个命令的名称——可能是宏，也可能是原语，但 TeX 目前还不知道它是哪种命令。此时它只是一串字符。

所以，在我们的示例中，TeX 会愉快地继续扫描，检查每个字符，直到它到达最初的 `\` 的记号值 `\ohyeah` 它也具有 `\catcode` 0。TeX 识别到自己扫描过头了，并礼貌地将那个 `\` 放回文本流中，使其成为后续文本扫描中将要看到的下一个字符。此时，TeX 已识别出一个字符串（`def`）——它知道这构成了一个由三个字符组成的控制词文本，每个字符的 `\catcode` 11 (`d`, `e` 和 `f`）。TeX 现在需要做的是弄清楚 `def` 意味着什么：它要做什么？正如你可能已经猜到的，TeX 需要找到 `def` 的命令代码和命令标识符，以便它能弄清楚该如何处理这个命令。

## 把它搞成哈希

在检测到一个控制词（`def`）后，TeX 做的第一件事就是使用所谓的哈希函数，把这串字符（`def` 在我们的示例中）转换为一个整数。我们不必过于纠结细节，知道大致过程就够了。本质上，TeX 会查看它刚刚检测到的控制词中的每个字符，并使用每个字符的 ASCII 码值（或 XeTeX/LuaTeX 的 Unicode 值）来计算一个称为哈希值的数字：它只是一个简单的整数。

作为这个哈希计算过程的一部分，TeX 还会检查新检测到的控制词中的字符序列是否已为它所知。所有命令的人类可读文本，无论它们是原语还是用户定义的宏，都会存放在一个内部存储区域中，称为 *字符串池*。TeX 之所以必须这样做，是因为它可能需要输出一个命令的人类可读名称——例如，当 TeX 需要报告错误并给出出错命令的名称时。比如，我们的宏 `\def\ohyeah{Overleaf is cool!}` 正在定义一个名为 `\ohyeah` 而 TeX（在后续阶段）不仅需要为 `ohyeah` (*没有* 最初的 `\` 字符）进行哈希值计算，而且还要存储其文本字符串（可读形式），以便在需要时用于错误报告（或其他任务）。

如果你想了解更多关于 TeX 字符串处理过程的细节，我已在我的 [个人博客网站](http://www.readytext.co.uk/?p=3590).

最终结果是，表示该命令的字符串 `def` 被转换为数值 1218（这是 TeX 实际计算出的值）。此时，各个字符 `d`, `e` 和 `f` 已经不再是主角——它们已从输入中读取并完成了任务：从现在起，一切都围绕整数和 *记号*——我们很快就会看到 token 究竟是什么！在内部，TeX 将这些哈希值称为 *当前控制序列* 但在源代码中，这个术语被缩写为一个名为 `curcs`。TeX 的源代码里充满了非常短、往往相当晦涩的变量名。

但是 TeX *到底做什么* 面对这个新鲜出炉的整数值 1218？TeX 如何发现原始字符串 `def`，现在由整数 1218 表示，实际上对应的是一条定义宏的指令？答案是，TeX 有一种内部的“文件柜”，用于存储它当前已知的每个命令的含义和值——无论该命令是用户定义的宏还是内建原语。TeX 之所以费力将 `def` 转换为哈希值 1218（现在存储在名为 `curcs`）的变量中），是为了用它查找 *和含义* 的记号值 `def`。当然，TeX 会对输入中检测到的所有控制词重复这一哈希计算过程——不过，不同的控制词会从哈希函数得到不同的整数值：这正是其核心所在。

TeX 内部的“文件柜”被称为 *等价表* ，这也是下一节的主题。

### 查阅等价表

简单回顾一下，看看我们到目前为止学到了什么：

* `\` 表示控制序列的开始（无论是一个 *控制符号* 或一个 *控制词*).
* 如果在 `\` 具有 `\catcode` 11（letter），那么它就是一个 *控制词*.
* 对于 *控制词* TeX 会扫描检查后续所有类别码为 `\catcode` 11 的输入字符，并在找到第一个不 *不* 具有一个 `\catcode` 11。
* 输入字符串（在 `\`）之后的字符，其 `\catcode` 11 会被视为一个 *控制词* 用户键入的：一个请求 TeX “做某事”的命令。
* 为了开始“做某事”的过程，TeX 会把控制词中的字符序列转换成一个整数。它通过一个所谓的哈希函数来完成，这个函数会输出一个整数。
* 这个整数（计算得到的哈希值）被称为 *当前控制序列*，但 TeX 给它起了更短的名字 `curcs`.
* 在我们的示例中，控制词 `def` 被转换为数值 1218——该值存储在一个名为 `curcs`：即， `curcs=1218`.

TeX 现在需要弄清新检测到的 *当前控制序列* 究竟意味着什么——TeX 如何处理它？

#### 关于分组的说明：保存和恢复信息的需要

这里，我们稍微绕一下，提醒自己 TeX 具有保存和恢复信息的能力：也就是说，它有某种内建“记忆”。

哪怕只写过最简单宏的人，都应该了解 TeX 的分组机制——例如，使用 `\def` 在组内创建宏。除非你对 `\global` 前缀应用于 `\def`在组内定义的宏，否则该宏的值或含义只会在该组（及其子组）中持续存在：当该组结束时，它的定义就会丢失。例如，如果你在一个组内定义一个简单宏，如下所示：

```latex
{\def\foo{Hello}}
```

并尝试在 `\foo` 组外使用

```latex
{\def\foo{Hello}}% \foo defined within a group (note: no use of \global)
\foo %<--- no longer defined, now undefined
```

那么我们就会得到令人熟悉的错误： `未定义的控制序列`. `\foo` 只在其定义所在的组（及其子组）中才有意义。此外，当你在一个组内重新定义宏时，新的值可能会在该组结束时丢失，而之前在组外存在的含义会被恢复。

```latex
\def\foo{Goodbye}
\foo\par% Outputs Goodbye
{\def\foo{Hello}% Redefined inside a group:
{Inside 2nd level group: \foo\par}}% Used inside 2nd level group: \foo outputs Hello
Outside group old value restored: \foo\par% Outputs Goodbye
```

这些简单示例的目的，是指向 TeX 拥有某种“存储机制”或“记忆”，它能够保存/恢复命令的“含义”——当然，它确实如此。我们在上一节已经暗示过这一点：那个“存储机制”或“文件柜”是一个大型内部表，称为 *等价表*。TeX 就是在那里存储它当前所知道的所有命令的当前含义或值——内建原语和用户定义的宏。

### 等价表：类比说明

为了说明等价表，我们采用类比的方式。我们继续使用带有成千上万个小抽屉的文件柜这一概念，每个抽屉都用一个唯一整数标号。此时在处理过程中，TeX 实际上是在说：

“好吧，我有一个刚刚计算并保存在名为 `curcs`的变量中的整数值 1218。我现在需要弄清楚它意味着什么：为此，我要去查看我文件柜中编号 1218 的抽屉，看看里面写了什么。”

TeX 用 1218 来定位正确的抽屉，然后在那里找到一张小纸条，其中包含三条信息，它们的名称是 TeX 源代码中使用的那些名称：

* **`eq_level:`** 该条目定义时的分组层级（层级 1 = 全局定义）。我们在上面已经看到分组的效果：在等价表中，这里存储了分组层级信息；
* **`eq_type:`** 该条目的命令代码；
* **`equiv:`** 该条目的当前“值”——它可以是一个简单整数，比如前面提到的命令修饰符，或者是一个指向内存区域的指针；例如，代表宏定义的一组记号所在的内存位置。

因此，我们的哈希值 1218（保存在变量 `curcs`）实际上已被用作 *键* ，用于访问一个抽屉，该抽屉包含我们最初以字母串输入的命令的当前含义和值 `\def`.

在 TeX 程序的源代码中， `eq_type` 对于任意命令，其存储在一个名为 `curcmd` 而 `equiv` 的值存储在一个名为 `curchr`.

### 等价表对 def 说了什么？

如前所述，为任何命令计算得到的哈希值会存储在一个名为 `curcs`；因此对于 `def` 我们有 `curcs=1218`。在等价表中的位置 1218 处，TeX 将找到以下信息：

* `curcmd`=97。这是 `\def`;
* `curchr`=0。这是 `\def`.

`\def` 是一个原语（内建）TeX 命令，除非它在某处被重新定义，否则第三也是最后一条信息应为 `eq_level=1` 这表明 `\def` 是全局定义的，不受某个较低分组层级的限制。在内部， `eq_level` 附加到某个命令上的值在 TeX 的分组机制中起着极其重要的作用，但这里我们不再进一步讨论。

下图总结了我们刚才讲解的内容：

![从 TeX 输入到 TeX 记号的旅程。](/files/ff2789dc3c98fb880dad923724f23b10bb12db86)

## 命令的 TeX 记号

在经历了上面的解释之后，控制序列的 TeX 记号实际计算起来其实非常简单。TeX 使用 `curcs` 哈希函数得到的值（1218）来创建一个简单的整数，TeX 将其称为一个 *记号*。由……的值生成记号的计算 `curcs` 的一般形式是：

```c
curtok = 4095 + curcs
```

TeX 将当前记号（最近计算得到的）的值存储在一个名为 `curtok`.

因此，最后，表示该 `\def` 命令是 `4095 + 1218 = 5313`。这就是表示命令序列的 TeX 记号：它们其实只是一个由哈希表值加 4095 计算得到的整数。

## 字符的 TeX 记号

当 TeX 需要创建一个表示字符的记号时，它使用下面同样简单的计算：

```c
curtok = 256*catcode + (ASCII value of character)
```

请注意，对于 LuaTeX 这类支持 Unicode 的引擎，会使用略有不同的计算方式。

例如，表示一个空格字符且其 `\catcode` 类别码为 10、ASCII 值为 32 的 TeX 记号是：

```c
256*10 + 32 = 2592
```

### 包含字符的记号列表

当你创建一个简单的记号列表时，例如

```latex
\toks100={Hello}
```

TeX 将创建下面的记号列表，并将其存入内存以供后续使用：

* H→ 256 × 11 + 72 = 2888
* e→ 256 × 11 + 101 = 2917
* l→ 256 × 11 +108 = 2924
* l→ 256 × 11 +108 = 2924
* o→256 × 11 + 111 = 2927

在 TeX 内存深处，记号寄存器 100 可以访问“Hello”的存储位置，该内容被保存为 5 个记号值：2888、2917、2924、2924、2927。请注意，这些记号将每个字符的 ASCII 代码与其 `\catcode`在这一时刻，它们会被转换为记号（tokenized）。一旦字符被转换为字符记号， `\catcode` 附加在它们上的值就是永久性的，并会存储在这些记号中，以便用户例如在输入 `\the\toks100`.

如前所述，字符记号是根据 `256*catcode + (ASCII value)` 而控制序列记号则是根据 `4095 + curcs` 其中 `curcs` 是 TeX 在输入中检测到的控制词（用户键入的命令文本串）的哈希值。值得注意的是，字符记号始终小于 4095。因此，TeX 可以轻松判断某个记号表示的是控制序列（命令）还是字符，然后弄清楚是哪一个控制序列或字符，以及 `\catcode` 该对是如何被编码进该记号中的。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/53-what-is-a-tex-token.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
