\noexpand 必须是原始的吗?

\noexpand 必须是原始的吗?

背景:我正在尝试加深对 TeX 及其复杂工作原理的理解(应该说,这个地方给了我一点启发,希望有一天我不再成为一名“Cargo Cult 程序员” - 请参阅何时使用 \edef、\noexpand 和 \expandafter?我对 TeX-by-Topic 很感兴趣,但我不擅长从头到尾阅读,也不擅长断断续续地阅读,所以阅读时并不总是能完全理解。

我觉得我开始理解扩展的概念了,但仍然有一些东西让我困惑。为了试图从中得出一个可以回答的问题,让我把重点放在 上\noexpand。根据 TeX-by-Topic,这是可扩展的,并将下一个标记暂时设置为\relax。我发现很难理解“暂时”这个词。如果想\noexpand模仿,那么我能想到的最好的办法是:

\def\noexpand#1{\let\tempa#1%
\let#1\relax
#1%
\let#1\tempa}

但我怀疑这会在“展开直到没有剩余可展开​​项”的上下文中引起问题(例如在 之后\if)。因此,要明确提出这个问题:可以\noexpand将其实现为宏,还是存在一些黑魔法要求它成为原始的?

(我脑海中浮现的画面\noexpand是,TeX 进入一种贪婪的进食模式,它会将一大堆食物放入嘴中,并尽可能地将其磨碎后再吞咽,但\noexpand会将食物包裹在一层无法咀嚼的涂层中。然而,这层涂层并不能抵御胃酸。不幸的是,有时 TeX(没有得到很好的抚养)会打嗝,它吃掉的一些东西会吐出来重新被吃掉。此时,之前被保护的东西的涂层\noexpand已被胃酸去除,现在可以咀嚼了。)

答案1

(进一步更新答案,深入狮子口在最后一节。)

Joseph 对你的实际问题给出了答案(“不!”);我会试着让你(和我)理解“暂时”。让我们从 TeX-by-Topic 开始,第 129 页底部:

\noexpand命令是可扩展的,其扩展是以下标记。该标记的含义暂时等于\relax,因此无法进一步扩展。

起初,我认为这是夸张的描述,但可以很容易地看出 TeX-by-Topic 是正确的:以下是简短的纯 TeX 文件

\def\foo{bar}
\expandafter\show\noexpand\foo
\bye

印刷

> \foo=\relax.
<recently read> \notexpanded: \foo 

到终端,所以\foo确实是“暂时等于\relax”。我觉得很有趣的是\notexpanded: \foo;这个\notexpanded在 TeXbook 或 TeX-by-Topic 中都没有提到,我只在 Knuth 的TeX 的严酷测试(另一个测试是将其放入您的 TeX 文件中;然后您会在您的日志文件中\tracingcommands=2 \noexpand\foo找到。)\relax

在 TeXbook 中,有一个描述,我觉得比 TeX-by-Topic 中的描述更容易理解(因为它不包含“暂时”一词):第 214 页上写道

\noexpand\relax⟨token⟩。扩展就是标记本身;但是,如果标记是一个通常会按照 TeX 扩展规则进行扩展的控制序列,则该标记将被解释为其含义为 ' '。

那么“暂时”是什么意思呢?

在实践中,“暂时等于\relax”意味着:当 TeX 扩展 时\noexpand\foo,结果具有\foo临时含义\relax,并且只要 TeX继续展开后,\foo恢复了其原有的含义。Tex \noexpand-by-Topic(第 130 页)中的典型用例如下:

\edef\one{\def\noexpand\two{\the\prevdepth}}

在里面\edef,TeX 不会扩展\two,但会继续下一个标记{(也是不可扩展的)。\noexpand是必需的,因为否则 TeX 会尝试扩展。 如果未定义,\two则会导致错误,如果之前已定义,则会导致更多麻烦。(由于不可扩展,因此不需要之前 。)如果您现在,那么您会看到真正的,而不是ed 位于 内:\two\two\def\noexpand\def\show\one\two\relax\one

> \one=macro:
->\def \two {-1000.0pt}.

幕后发生了什么?

首先,让我在 TeX 的嘴和胃的图片中说一下。一般来说,只要第一的令牌是可扩展的。当第一个令牌无法扩展时,它会被吞下并在胃中进一步处理。现在\noexpand确实用某种保护涂层包裹着紧随其后的令牌。然而,这种涂层可咀嚼;认为它是由糖制成的。因此,当 TeX 咀嚼这种涂层标记时,涂层会被去除(并且标记会再次膨胀),但 TeX 会获得感官闪光并认为“哇,这尝起来像\relax,我想吞下它。”果然,如果现在没有涂层的标记是第一的在 TeX 的嘴里(将“首先”想象为“最靠近喉咙”),然后吞咽。

两个例子(第二个例子相当学术):

  1. 例如,

    \edef\bar{\noexpand\foo\anothercs}
    

    然后在 内部\edef\noexpand\foo展开为\relaxed \foo。这会\foo立即被转换回 ,并且 TeX 会继续展开下一个令牌\anothercs

  2. 你扩大\noexpand\foo 两次,例如

    \def\foo{bar}
    \expandafter\expandafter\expandafter\show\noexpand\foo
    \bye
    

    然后在终端上你得到

    > \foo=macro:
    ->bar.
    

    因此,扩大\noexpand\foo 一次给出一个\relaxed (如上所示),而ed\foo的展开又是原来的。这是\relax\foo\foo不是由于它不是第一个令牌,因此被吞噬!

免责声明:以上是我通过观察发现的,有些细节可能不完全正确。

最后几点

由于多种原因,您尝试将其定义\noexpand为宏会失败。一个技术要点是,\let#1\relax如果#1不是控制序列,则这不是一个好主意。另一点是这样做并没有太大帮助\let#1\relax #1;据我所知,这样做具有相同的效果\relax。(并且正如其他答案所指出的那样,\let不可扩展。)

答案2

您的基于宏的方法将失败,因为您使用赋值(\let):这些不能在扩展上下文中使用。虽然 TeX 是图灵竞争的,但这并没有说明扩展。不可能改变某些东西的含义之内扩展(至少没有 LuaTeX)。因此,问题的答案是“是的,这必须是一个原始的”。

答案3

以下答案由两部分组成。

  1. \noexpand的实现。在本部分中,我将概述 TeX 引擎如何实现\noexpand原语。我还将介绍第 2 部分将使用的一些术语。第 1 部分回答了 OP 提出的问题,即如何解释以下语句

    \noexpand将下一个标记暂时设置为 \relax。

    我将介绍的实现是推测性的。换句话说,我并不是说这是任何现有 TeX 引擎的实现方式。我只是说,可以按照本部分描述的思路实现 TeX 引擎,并且就目前而言,最终实现的行为方式将与现有实现一致\noexpand

  2. 分步示例。在本部分中,我将把第一部分的理论应用到四个 TeX 代码片段中,每个片段都演示了如何与\noexpand其他 TeX 基元进行交互。其他四个基元按显示顺序依次为:\edef、、和 e-TeX 。\expandafter\ifx\numexpr

    对于每一个原语,我都会

    (a) 建议如何通过 TeX 引擎实现它。
    (b) 列出一个展示此原语和 之间交互的 TeX 代码片段\noexpand。 (c) 给出第 1 部分中描述的实现和其他原语的建议实现
    ,逐步执行此代码片段。\noexpand


1.\noexpand实施

每个标记t都有一个值。 的值t可以是\let某个控制序列或活动字符,如\let\u=t。我们将最初分配给 TeX 的原始控制序列的值\<cs name>称为<cs 名称>;例如,最初分配给的值\meaning意义\relax,最初分配给的值为放松等等。这些将被称为原语'默认值

TeX 引擎维护一个表,将标记名称(即\string t扩展为的内容)映射到有序对(v,x),其中v是标记的值,x是下面将要描述的布尔标志。我们将称为x标记的“暂时不可扩展”标志。当将条目添加到表中时,x会自动设置为false,但这可以在以后更改,如下所述。

此表仅可通过 API 访问。此 API 包含三个函数,其签名如下:

void temp_nonexpand(string tk_name)
value_obj get_value(string tk_name)
int compare_temp_nonexpand(string tk_name1, string tk_name2)
  • temp_nonexpand以 token 名称作为参数,并将与此名称关联的“暂时不可扩展”标志设置为true。无法明确将此标志重置为;这由和false自动完成,如下所述。get_valuecompare_temp_nonexpand
  • get_value以 token 名称作为参数,并返回(指向)映射到此名称的值的指针。当get_value(...)被调用并传递“暂时不可扩展”标志为 的控制序列的名称时trueget_value返回该值放松,而不管实际映射到此控制序列的值是什么,并将标记的“暂时不可扩展”标志重置为false。因此,下次get_value使用相同名称调用时,将返回实际映射到此名称的值。这就是本答案开头引用的引文的含义。
  • compare_temp_nonexpand以两个标记名称作为参数,如果两个标记的“暂时不可扩展”标志均为true,则返回 1;如果两个标记的“暂时不可扩展”标志均为false,则返回 -1;否则返回 0(即其中一个标志为真,另一个为假,不一定按此顺序)。与 类似get_valuecompare_temp_nonexpand将两个标记的“暂时不可扩展”标志重置为false。此函数仅由 的实现使用伊夫克斯;详情请参阅下文第 2 节。

每个值要么可扩展,要么不可扩展。用户无法控制此属性;TeXbook(第 20 次印刷,Addison-Wesley 1991 年)规定如下:可扩展值是宏,以及 TeX 的一些原始控制序列的默认值。TeXbook 的第 212-215 页提供了可扩展原始值的完整列表,但这里是一些示例。

  • 可扩展的值包括以下原语的默认值:\expandafter\ifx\meaning\noexpand\string\the
  • 不可扩展值包括以下原语的默认值:\def\edef\relax\show

当 token 的值可扩展时,我们称 token 本身是可扩展的。扩展 token 就是扩展其值。“扩展值”的含义取决于值。例如,扩展不扩展u是这样的。用输入流上的下一个标记表示。如果u是可扩展的,则通过执行将u的“暂时不可扩展”标志设置为。truetemp_nonexpand(\string u)

一些不可扩展的原语是可执行命令(例如,上一个要点中列出的所有原语),而一些则不是(例如,寄存器、参数)。“执行”和“扩展”之间没有真正的区别,只是我们将这些动词应用于不同的对象:前者应用于不可扩展的命令原语,而后者应用于宏和可扩展原语。我们还将把“执行”一词应用于内部函数,例如get_value,就像我们在上一段末尾所做的那样。


2. 步骤示例

现在让我们通过四个示例将所有这些理论付诸实践。每个示例都展示了如何\noexpand与其他 TeX 基元交互,即

  1. \edef
  2. \expandafter
  3. \ifx
  4. \numexpr(e-TeX 原语)

所有示例均假设其中的所有原语都有其默认值。

示例 1:\edef

我们只关心如何埃德夫根据输入流中花括号内指定的标记构造定义宏的替换文本的内部表示。埃德夫实现以下算法来构造替换文本。

  1. 将令牌列表初始化replacement_text为空列表。
  2. 从输入流中删除第一个标记,并将其分配给变量t
  3. 只要t不是右括号,就重复:
    1. 获取t的值:v := get_value(\string t)
    2. 如果v不可扩展,则将标记附加到替换文本:replacement_text.append(t )
    3. 如果v可以扩展,则扩展v
    4. 从输入流中删除第一个标记,并将其分配给t

现在考虑以下 TeX 代码(取自Hendrik Vogt 的回答):

\edef\bar{\noexpand\foo\anothercs}

让我们逐步完成替换文本的构建。

  1. 标记replacement_text列表被初始化为空列表。
  2. \noexpand从输入流中移除并分配给t
  3. \noexpand的值被赋给vv := get_value("noexpand"),所以现在v ==非扩展
  4. 自从不扩展是可扩展的,通过执行 ,它被扩展temp_nonexpand("foo")。这将打开 的\foo“暂时不可扩展”标志。
  5. \foo从输入流中移除,并分配给t
  6. 我们设置v := get_value("foo")。由于 的\foo“暂时不可扩展”标志是在调用true时,get_valuev==放松,并且标志被关闭。
  7. 自从放松不可扩展,\foo附加到replacement_text列表中。
  8. \anothercs从输入流中移除,并分配给t
  9. 如果\anothercs的值不可扩展,\anothercs则将其附加到replacement_text列表中;否则,它将被扩展,依此类推。

示例 2:\expandafter

展开后实现如下。假设有一个专用的全局令牌堆栈s用于展开后的用途。s可以假设最初是空的,尽管这里不会用到这个事实。

  1. 从输入流中移除第一个标记,并将其压入堆栈s
  2. 从输入流中删除第一个标记,并将其分配给变量t
  3. 检索t的值:v := get_value(t)
  4. 如果v不可扩展,则将其推t回到输入流的前面。否则,将其扩展。
  5. 弹出 顶部的标记s,并将其添加到输入流的前面。

现在考虑下面的 TeX 手稿(取自Hendrik Vogt 的回答):

\def\foo{bar}%
\expandafter\expandafter\expandafter\show\noexpand\foo%
\bye

让我们逐步地展开第二行。

  1. \expandafter从输入流中删除,以及它的值,展开后,被检索。由于展开后是可扩展的,它被扩展了,如下所示。

    1. 第二次出现的\expandafter被从输入流中删除,并被推送到s,因此堆栈现在只保存这一个标记。
    2. 将第 3 次出现的\expandafter从输入流中删除,并分配给t
    3. 我们设置v := get_value("expandafter")。现在v ==展开后
    4. 自从展开后是可扩展的,它被扩展,如下所示。1
      )\show从输入流中移除,并推入s,因此s现在保持\show\expandafter,其中堆栈顶部在左侧。2
      )\noexpand从输入流中移除,并分配给t。3
      ) 我们设置v := get_value("noexpand")。现在v ==不扩展.
      4) 由于非扩展是可扩展的,则通过执行 来扩展它temp_nonexpand("foo")。这将打开 的\foo“暂时不可扩展”标志。5
      )s弹出,弹出的项目,即\show,被添加到输入流的前面,现在看起来如下:

      \show\foo
      
    5. s被弹出,并且弹出的项目,即\expandafter,被添加到输入流的前面,现在如下所示:

      \expandafter\show\foo
      
  2. \expandafter从输入流中删除,以及它的值,展开后,被检索。由于展开后是可扩展的,它被扩展了,如下所示。

    1. \show从输入流中移除,并被推送s
    2. \foo从输入流中移除,并分配给t
    3. 我们设置。由于在调用v := get_value("foo")时,的“暂时不可扩展”标志自步骤 1.4.4 以来一直处于此状态,get_value\footruev ==放松,并且\foo的“暂时不可扩展”标志被关闭。
    4. 自从放松是不可扩展的,\foo被推回到输入流的前面。
    5. s弹出,弹出的项目,即\show,被添加到输入流的开头。输入流现在是:

      \show\foo
      
  3. \show从输入流中删除,以及它的值,展示,被检索。由于展示是一个命令,它被执行:\foo检索 的值,如下所示。首先我们设置v := get_value("foo"),然后将 的表示v写入日志文件。由于get_value步骤 2.3 中的调用已关闭 的\foo“暂时不可扩展”标志,v == (macro:->bar)

示例 3:\ifx

伊夫克斯实现如下。

  1. 从输入流中删除前两个标记,并将它们分别分配给变量tu
  2. 设置c := compare_temp_nonexpand(\string t, \string u)。作为副作用,此操作将两个标记的“暂时不可扩展”标志设置为false
  3. 如果c == ...
    • 1,即,如果两个标记暂时不可扩展,则测试评估为true
    • 0,即,如果恰好有一个标记暂时不可扩展,则测试评估为false
    • -1,即,如果两个标记都不是暂时不可扩展的,则测试将根据通常的规则进行评估(参见 TeXbook,第 210 页)。
  4. 一旦知道了测试值,伊夫克斯的扩展照常进行(参见 TeXbook,第 213 页)。

例如以下 TeX 手稿:

\def\foo{foo}%
\def\bar{bar}%
\expandafter\expandafter%
    \expandafter\ifx%
        \expandafter\noexpand\expandafter\foo\noexpand\bar yes%
    \else no%
    \fi%
\bye

排版“是”,即使\foo\bar不具有相同的\meaning,因为在执行测试时,两个标记都暂时不可扩展。

然而,以下手稿:

\def\foo{foo}%
\expandafter\ifx\noexpand\foo\relax yes\else no\fi%
\bye

排版为“no”,尽管\noexpand\foo\relax具有相同的含义(尝试\expandafter\meaning\noexpand\foo),因为在执行测试时,\foo的“暂时不可扩展”标志是true,而\relax的“暂时不可扩展”标志是false

应注意以下警告。伊夫克斯是唯一一个在实现中调用 的 TeX 基元compare_temp_nonexpand。特别是\if, 和任何其他条件(例如\ifcat)都不会这样做。因此,如果您用 替换\if上面\ifx的 ,则两个测试都将评估为true,因为暂时不可扩展的标记将被解释为\relax

示例 4:\numexpr

数字表达式是一个不可扩展的原始命令,它不是核心 TeX 的一部分,而是e-TeX 扩展,由 实现pdftex。我将描述此算法的一个简化版本,其行为与原始算法略有不同,但足够接近原始算法,以说明如何数字表达式不扩展真实互动。

版本数字表达式下面将要介绍的表达式将一系列非负整数作为输入,这些整数中穿插着二进制算术运算符,例如1+20*3,并将表达式的结果赋给新的整数寄存器\return。表达式中不允许有空格。任何既不是数字也不是二进制算术运算符的标记都标志着数字表达式的输入。操作按文本顺序执行,因此,例如,上面的表达式将计算为 63 (= (1 + 20) * 3),而不是 61。数字表达式从输入流中移除其输入。如果表达式后面跟着一个值为放松,这个标记也会从输入流中删除(这就是真正的数字表达式原始行为;参见 e-Tex 手册第 8 页)。

数字表达式算法利用了两个辅助函数:get_next_nonexpandableparse_nonneg_int

get_next_nonexpandable

get_next_nonexpandable如果有必要,通过反复扩展第一个项从输入流中获取下一个不可扩展的标记。还返回标记的值(如果下面的示例要按照实际操作,这一点至关重要)。它的签名是

(token, value) get_next_nonexpandable()

其实现方式如下:

  1. 从输入流中删除第一个标记,并将其分配给变量t
  2. 获取t的值:v := get_value(\string t)
  3. 重复直至v可扩展:
    1. 扩张v
    2. 从输入流中删除第一个标记,并将其分配给变量t
    3. v := get_value(\string t)
  4. 返回(t, v)

parse_nonneg_int

parse_nonneg_int从输入流的开头删除最长的数字序列,并返回此数字串所表示的非负整数。其签名为

int parse_nonneg_int()

其实现方式如下:

  1. 将非负整数变量初始化D0。此变量将累积中间计算,并最终保存要返回的数字。
  2. 从输入流的开头获取下一个不可扩展标记:(t, v):= get_next_nonexpandable()
  3. 重复,只要v形式为(character, catcode) 该字符是一个数字。
    1. 用 来表示v的性格c
    2. 转换c为整数d
    3. D := D*10 + d
    4. 从输入流的开头获取下一个不可扩展标记:(t, v):= get_next_nonexpandable()
  4. t回到输入流的开头。
  5. 返回D

numexpr

使用get_next_nonexpandableparse_nonneg_int,简化版的数字表达式的算法是:

  1. 初始化整型变量i := 0。该变量将累积中间计算,并最终保存整个算术表达式的结果。
  2. 初始化字符变量op:='+'。此变量将保存最近遇到的算术运算符。
  3. 从输入流中获取下一个不可扩展标记:(t,v) := get_next_nonexpandable()
  4. 重复,只要v是形式(character, catcode)该字符可以是数字或二进制算术运算符。
    1. 用 来表示v的性格c
    2. 如果c是数字:
      1) 将其推t回到输入流的开头。2
      ) 从输入流的开头解析最长可能的非负整数:j := parse_nonneg_int()。3 )对参数和
      执行指定的操作。4 ) 设置为。opij
      opnull
    3. 如果c是二元算术运算符:
      1) 如果op != null,则报错(“2个连续的运算符!”),并退出 TeX 引擎。2
      ) 如果op == null,则赋值op := c
    4. 从输入流中获取下一个不可扩展标记:(t,v) := get_next_nonexpandable()
  5. 如果op != null,则报告错误(“悬垂运算符!”),并退出 TeX 引擎。
  6. 如果v !=放松,推t回到输入流的开头。
  7. 分配一个新的整数寄存器:\newcount\result
  8. 分配i给整数寄存器:\result{<tokenized representation of i>}
  9. 将标记添加\result到输入流的开头。

现在考虑下面的 TeX 代码(这是 Martin Scharrer 在一条评论对于亨德里克的回答):

\the\numexpr 1+1\noexpand\empty\relax

让我们逐步处理这一行,从 开始\numexpr。换句话说,TeX 引擎正在扩展并刚刚开始执行数字表达式,因此输入流当前为1+1\noexpand\empty\relax。我们假设\empty是一个宏,其定义如下:\def\empty{}

  1. 初始化整型变量i:=0和字符变量op:='+'
  2. (t, v) := get_next_nonexpandable().现在是具有值的t标记(12 是数字的默认类别代码)。1('1', 12)
  3. 因为1是数字,所以t被推回到输入流的前面,并且我们设置j := parse_nonneg_int()j现在等于整数1,并且输入流为+1\noexpand\empty\relax
  4. 分配i := i + j.i现在等于1
  5. (t, v) := get_next_nonexpandable().现在是具有值的t标记(12 是字符 '+' 的默认 catcode)。+('+',12)
  6. 由于+是二元算术运算符,我们分配op:='+'
  7. (t, v) := get_next_nonexpandable().现在是具有价值 的t令牌。1('1', 12)
  8. 由于1是数字,t所以被推回到输入流的前面,并且我们设置j := parse_nonneg_int()。让我们逐步执行parse_nonneg_int

    1. 非负整数变量D初始化为0
    2. (t, v) := get_next_nonexpandable().t现在是令牌1,并且v('1', 12)
    3. 由于1是数字,因此将其转换为相应的整数d:=1,并且我们设D:=D*10+dD现在等于1
    4. (t, v) := get_next_nonexpandable()。让我们逐步执行get_next_nonexpandable。1
      ) 从输入流中删除第一个标记并将其分配给变量t。所以现在t == \noexpand。2
      )t检索 的值并将其分配给变量vv:=get_value("noexpand")。所以现在v ==不扩展
      3)由于不扩展是可扩展的,它通过执行 进行扩展set_temp_nonexpand("empty")。所以现在\empty被标记为暂时不可扩展。4
      ) 从输入流中删除输入流的第一个标记并将其分配给t。所以现在t == \empty。5
      )t的值被检索并分配给v: 。由于在调用v:=get_value("empty")时,被标记为暂时不可扩展,get_value\emptyget_value因此返回放松,并关闭\empty“暂时不可扩展”标志。所以现在v ==放松,并且\empty不再暂时不可扩展。
      ) 由于放松不可扩展,(t, v)(\empty,放松),返回。
    5. 自从v==放松不是 的形式(character, catcode)t被推回到输入流的开头,并parse_nonneg_int返回D,即整数1。输入流现在是

      \empty\relax
      
  9. 现在i == 1,,j == 1op == '+',并且我们设置i := i + j,所以现在i == 2,并且我们设置op := null

  10. (t, v) := get_next_nonexpandable(). 让我们逐步执行get_next_nonexpandable
    1. 从输入流中移除输入流的第一个标记,并将其分配给t,使得t == \empty
    2. t的值被检索并赋值给vv := get_value("empty")。回想一下,自步骤 8.4.5 结束以来,\empty的“暂时不可扩展”标志一直存在。因此返回的实际值,即空宏。falseget_value\empty
    3. 由于v是宏,因此它是可扩展的,因此 也\empty被扩展。但是,由于\empty的主体为空,因此输入流保持不变(当然,TeX 引擎的内部状态也是如此)。因此,在\empty扩展之后,输入流由单个标记组成:\relax
    4. 第一个标记从输入流中删除,并分配给t,以便t == \relax
    5. t的值被检索并分配给vv := get_value("relax")。所以现在v ==放松
    6. 自从放松不可扩展,get_next_nonexpandable返回(t, v),即(\relax,放松)
  11. 自从v ==放松数字表达式已到达循环末尾(步骤 4数字表达式的算法)。因此我们分配一个新的整数寄存器\result\newcount\result
  12. 我们分配i\result\result{2}
  13. 我们将标记添加\result到输入流的开头,这样现在输入流由一个标记组成:\result,并且此输入流被“移交”给原始的操作。因此最终结果将与 TeX 引擎最初呈现的输入流一样

    \the\result
    

    \result一个保存值 2 的整数寄存器。

  14. 文本“2”已排版。

答案4

在 LuaTeX 背景下的一些想法。(公平地说,这有点“作弊”,因为当你赋予 Lua 越来越多的功能时,更多的事情变得可以实现)

OP 的实现(经过一些修改)有效吗?

原来的实现可以改成这样... (假设这是\ExplSyntaxOn环境,所以没有虚假空间)

\def\noexpand #1{
 \immediateassignment \let \tmpa #1 
 \immediateassignment \let #1 \relax
 #1
 \immediateassignment \let #1 \tmpa
}

\noexpand它满足实数满足的这两个属性

  1. edef上下文中,token 被收集到\edef结果中
  2. 在正常执行环境中,它不执行任何操作。

问题(首先是显而易见的)

  • 如果下一个标记是{或怎么办}
  • 如果下一个标记是\let它自己怎么办?
  • (更一般地说,如果下一个标记不可扩展,\noexpand则不应该对其执行任何操作)

假设这些问题已经解决。还有哪些问题?

除了这两个属性之外:

  1. 真实\noexpand也仅需 1 个扩展步骤即可扩展为\notexpanded:标记。

作为一个普通宏,它至少需要 2 个。

如果我不关心扩展步骤的具体数量怎么办?

这会严重限制您使用 TeX 的功能。特别是,因为\expandafter只扩展以下标记一次。

好的,如果用 解决了该问题会怎样luadef

在 LuaTeX 中,目前您还可以让任何标记的单次扩展执行任何您想要的操作;并且您可以滥用token.scan_toks()它来进行任意扩展;结合起来\immediateassignmenttex.runtoks您可以以 1 步扩展的方式执行(几乎)任意 TeX 代码。

因此,您将\noexpand \__a分配\relax\__a,然后分 1 步扩展为\__a \immediateassignment \let \__a \relax。这样可行吗?

问题:实际\noexpand只扩展为 1 个标记。

找出导致问题的情况并不太难。例如,如果你执行\futurelet,你将得到\immediateassignment而不是真正的下一个标记。

如果该问题解决了会怎样?

我能看到解决这个问题的一种方法,即将\noexpand \__a扩展 1 步变成新代币(与token.new())具有与 相同的字符串表示形式\__a;但尽管如此,它是一个不同的标记,并且在扩展/执行时会相应地表现。

附注:这里的“行为相应”在\noexpand上下文中也意味着

  1. 当它被扩展一次(例如使用\expandafter、 或“数字扫描仪”,如\number/\ifnum等或 dimen 扫描仪等)时,它“似乎”是不可扩展的扩展为原始标记。
  2. 其他 \__a令牌不得改变。换句话说,只有新的令牌才有意义\relax,因此您实际上不能这样做\let \__a \relax
  • (当它位于\edef或处于正常执行上下文中时,请参见上文。)

请注意,这要求新令牌能够以某种方式区分它是否在 内\edef。据我所知即使在当前的 LuaTeX 中,这也是不可能的,没有任何副作用;尽管修补\edef等是一种选择。

好的,假设以上所有情况都成立。那么,它有效吗?

问题:令牌还有最后一个特殊属性\notexpanded:

  1. 当它被扫描为命令参数时,它会失去其\notexpanded:属性。

例如

\def\showwrapper #1{\show #1}
\expandafter \show \noexpand \__a  % shows \notexpanded
\expandafter \showwrapper \noexpand \__a  % shows the meaning of the original token

只有将其不受影响地放在输入流中,它才能保留该属性。(并且只要它不受影响,它就会保持不扩展,无论你之后对令牌做什么)

即使使用当前的 Lua 引擎,我也没有找到解决这个问题的方法。


其他一些有趣的属性(用 LuaTeX 测试,其含义是[unknown command code! (0, 1)]而不是通常的\relax

  • 即使用 跳过它\expandafter,它也会失去其\notexpanded:属性。
  • 同样,当它的意义被赋予时,\futurelet它就失去了它的\notexpanded:属性;但重要的是,任务目标得到了\notexpanded意义,它将遗迹所以。
  • 如果它留在输入流中,然后它被设置为一些不可扩展的标记,例如\let,那么它的含义将变成\let;但是如果它被设置为某个可扩展的东西,那么它的含义将返回到未扩展。
  • 另一方面,如果它原本是不可扩展的东西,被设为不可扩展,然后被重新定义为可扩展的东西,它的含义就不会扩展。
  • Luatoken.put_next(token.get_next())也会使其失去该属性。

相关内容