TeX言語のトークンと値 その2:\noexpandと、挿入された\relax

我々はTeX言語を完全に理解しなければならない

我々はTeX言語を完全に理解するであろう

— David Hilbert

TeX言語をわからないまま書くのと、TeX言語を完全に理解した上で忘れるのは違いますからねぇ

— とある脚本家

前回はTeX言語について概略を説明したので、今回は堂々と(The TeXbookやTeX by Topicにも載らないような)重箱の隅をつつくことにする。記法、用語は基本的に前回に準じるが、面倒なので制御綴 \foo を表す際に四角で囲っていなかったり、文字トークンのカテゴリーコードを省略している場合がある。

一部、以前の TeXっぽいものを実装するにあたっての雑記 と内容が被っている。

\csname(復習)

\csnameの副作用は有名だし、以前の記事でも取り上げた。つまり、\csnameは対象とするコマンド名(制御綴)が未定義の場合、その意味を\relaxとする。

つまり、\(\fbox{foo}\) が未定義(\(\mathsf{definition}(\fbox{foo})=\mathsf{undefined}\))の状態で

\csname foo\endcsname

を実行すると \(\fbox{foo}\) の意味(値)が \relax となる。実際、 \csname foo\endcsname の後に

\foo

を実行しても \foo が未定義だというエラー(Undefined control sequence.)は出ないし、

\ifx\foo\relax \message{Y}\else\message{N}\fi

を実行するとYが出力される(つまり、コマンド名 \foo の値は \ifx の意味で \relax と等価である)

\noexpand

\noexpandとは

まずは標準的な\noexpandの説明をしよう。

\noexpandを前置した展開可能トークンまたは未定義トークンは一時的に\relaxの意味を持ち、「展開不能」とみなされる。

これは\edef(等の引数を完全展開するコマンド)と組み合わせて使われることが多い。つまり、\fooを

\edef\foo{\bar\noexpand\baz\bazz}

と定義した時に、\fooの定義において\bar,\bazzは展開されるが、\noexpandで前置された\bazの展開は抑制される。

単独で使った場合、後続するものが展開可能トークン(マクロ等)の場合はそれを \relax 化し、展開不能トークン(ここでの例は \message)の場合はそのままにする。つまり、

\def\baz{\message{Hello}}
\noexpand\baz
\noexpand\message{Goodbye}

を実行した場合に、\bazは展開可能トークンなので\relax化されてHelloは表示されず、\messageは展開不能なので意味は変わらずGoodbyeが表示される。

\noexpandの展開結果

さて、\noexpandは展開可能コマンドである。展開可能コマンドということは、何かしらの引数を読み込んで、何かしらのトークン列を生み出すはずである。\noexpandが生み出すトークン列とはどのようなものだろうか?

\csnameにも「\relax化する」と呼べる作用があったが、あれはコマンド名から値(意味)への対応 \(\mathsf{definition}\colon\mathsf{CommandName}\to\mathsf{Value}\) をいじって(TeXコードで言えば、\let\foo=\relax )\relax化するのであって、\csnameの展開結果自体は普通のトークン(コマンド名)である。

一方、\noexpandは\csnameとは違い、同じコマンド名の別の出現には影響しない。つまり \noexpand\baz \baz と書いたとしても、\relax化されるのは最初の\bazだけである。

\noexpandがコマンド名から意味への対応 (\(\mathsf{definition}\colon\mathsf{CommandName}\to\mathsf{Value}\)) をいじるものではない以上、展開結果のトークンに細工があると考えた方が良さそうである。

つまり、前回はトークンの集合 \(\mathsf{Token}\) を

\[\begin{aligned}
\mathsf{Token}:=&\mathsf{CommandName}\cup\{x_\mathit{cc}\mid x\in\mathsf{Char},\mathit{cc}\in\mathsf{TCatCode}\}\\
=&\mathsf{CommandName}\cup\{\mathtt{a}_{11},\mathtt{b}_{11},\ldots,\mathtt{a}_{12},\ldots,\mathtt{=}_{12},\ldots\}
\end{aligned}\]

として定義したが、この定義は実は不完全で、「noexpandマークがついたコマンド名」の集合 \(\mathsf{CommandName}^{\mathsf{noexpand}}=\{x^{\mathsf{noexpand}}\mid x\in\mathsf{CommandName}\}\) を加えて

\[\begin{aligned}
\mathsf{Token}:=&\mathsf{CommandName}\cup\mathsf{CommandName}^{\mathsf{noexpand}}\cup\{x_\mathit{cc}\mid x\in\mathsf{Char},\mathit{cc}\in\mathsf{TCatCode}\}\\
=&\mathsf{CommandName}\cup\{x^{\mathsf{noexpand}}\mid x\in\mathsf{CommandName}\}\cup\{\mathtt{a}_{11},\mathtt{b}_{11},\ldots,\mathtt{a}_{12},\ldots,\mathtt{=}_{12},\ldots\}
\end{aligned}\]

とすべきだったのだ。この定義の下で、\noexpandの動作は次のようになる:

  • \noexpandは、直後のトークン(の値)が展開可能もしくは未定義であれば、そのトークンに「noexpandマーク」をつけて展開結果とする。
  • 「noexpandマーク」がついたトークンの値(意味)は、展開不能プリミティブ \relax である。
  • 「noexpandマーク」は、多くの文脈で無視されたり、削ぎ落とされたりする。例えば、
    • \def\edef で定義するマクロの本文中にnoexpandマークがついたトークンがあっても、定義されるマクロには影響しない。例: \expandafter\def\expandafter\foo\expandafter{\noexpand\barbaz} で定義される \foo\def\foox{\barbaz} で定義される \foox\ifx の意味で等しい。
    • マクロのdelimited引数の検査ではnoexpandマークは無視される。例:\def\foo\barbaz{} \expandafter\foo\noexpand\barbaz を実行してもエラーが起きない
    • \uppercase\lowercase を通すとnoexpandマークは消滅する。例: \def\barbaz{nyaa}\uppercase\expandafter{\expandafter\edef\expandafter\foo\expandafter{\noexpand\barbaz}} で定義される\fooの中身はnyaaである。対比:\expandafter\edef\expandafter\foo\expandafter{\noexpand\barbaz} で定義される\fooの中身は\barbazのままである。
    • 【追記】noexpandマークのついたトークンを\expandafterで展開しようとすると、noexpandマークが取り除かれる。詳しくは次の記事を参照。

これで\noexpandの挙動をうまく説明できるのではないか。

…と、これで済めばよかったのだが、現実のTeX処理系の動作はもう少し複雑イミフなようである。

\noexpandの展開結果の意味

\noexpandの展開結果を\letの右辺に置くとどうなるか。つまり、 \barbaz が展開可能あるいは未定義の状態で

\expandafter\let\expandafter\foo\noexpand\barbaz

を実行した時に制御綴 \foo の指す値は何になるのか。

(一応解説しておくと、

  1. まず、 \expandafter\let\expandafter\foo\noexpand\barbaz の最初の \expandafter が展開される。最初の \expandafter\letを脇に置いて2番目の \expandafter を展開しようとする。
  2. 2番目の \expandafter\foo\noexpand\barbaz\foo を脇に置いて \noexpand を展開しようとする。
  3. \noexpand の展開結果のトークンとして〈noexpandマークのついたbarbaz〉が生まれる。
  4. 2番目の \expandafter が脇に置いていた \foo を戻し、「先読み済みのトークン列」は \foo, 〈noexpandマークのついたbarbaz〉 の2つとなる。
  5. 1番目の \expandafter が脇に置いていた \let を戻し、「先読み済みのトークン列」は \let, \foo, 〈noexpandマークのついたbarbaz〉 の3つとなる。
  6. \let が実行され、コマンド名 \foo に〈noexpandマークのついたbarbaz〉の意味が代入される。

という流れである。)

先ほどの仮説によれば、「〈noexpandマークのついたbarbaz〉の意味」は展開不能プリミティブ \relax なので、コマンド名 \foo には値 \relax が代入されるはずである。

実際、(LuaTeX以外のTeX処理系では) \show\foo とすると > \foo=\relax. と表示される。\fooを実行しても何も起こらないし、TeXの文法で\relaxが置ける部分(〈filler〉など)に\fooを置いてもエラーが出ず期待通りに\relaxとして振る舞う:

% \message と直後の { の間には \relax を置ける:
\message\relax{Hello} % -> Hello
% \foo は \relax と等価なはずなので、 \foo を置くこともできる:
\message\foo{Hello} % -> Hello
% 比較:空白と\relax以外の展開不能トークンを置くことはできない(エラーになる):
\message a{Hello} % -> ! Missing { inserted.

こうなると当然、 \ifx で \foo と \relax を比較すれば、結果は真のはずだ。やってみよう:

\ifx\foo\relax
  \message{Yes}
\else
  \message{No}
\fi

結果は、まさかの、No、である…!

つまり、「〈noexpandマークのついたトークン〉の意味」は、内部的には \relax そのものではなく、「擬似的な\relax」〈quasi \relax〉とでも呼ぶべき何かなのである。

\[\mathsf{NValue}=\{\mathtt{\backslash relax},\mathtt{\backslash let},\ldots,\langle\text{quasi }\mathtt{\backslash relax}\rangle,\ldots\}\]

\noexpandの動作に関して、先のセクションの箇条書きの該当部分は次のように加筆訂正される:

  • 「noexpandマーク」がついたトークンの値(意味)は、展開不能な値 〈quasi \relax〉 である。
  • 〈quasi \relax〉は\ifxを除くほとんどの文脈で\relaxと同様に扱われる。

この動作(\noexpandされたトークンの意味が\ifxの意味で\relaxと異なる)はおそらく実装の詳細もいいところで、TeXに詳しい人でも知らない人が多いだろう。TeXと100%互換な処理系を作りたい偏執狂であればこの挙動を正確に再現したいかもしれないが、そうでない場合はわざわざ作り込まずに、\noexpandされたトークンの意味が完全に(\ifxの意味でも)\relaxと同じようにしてしまって良いだろう。

(筆者が作っているYuruMathでは当初はこの挙動を再現していたが、流石にあほらしいと思い、やめた)

おまけ:\unexpanded

e-TeXでは\noexpandの複数トークン版とも言える\unexpandedが追加された。

\unexpandedは\edef(等の引数を完全展開するコマンド)の文脈で展開された際に、引数を完全展開させない。つまり\fooを\edefで

\edef\foo{\bar\unexpanded{\baz\bazz}\meow}

と定義した時に、\bar,\meowは展開されるが、\unexpanded中の\baz,\bazzは展開されない。

\unexpandedは\noexpandと異なり、通常の展開の際には中身をそのまま返すし、\relax化の作用を持たない。例えば

\def\greet{\message{Hello}}
\unexpanded{\greet}

を実行するとマクロ\greetは無効化されずにHelloと表示されるし、

\expandafter\let\expandafter\foo\unexpanded{\barbaz}

の実行結果は

\let\foo\barbaz

と同じである。

つまり、\unexpandedが特殊なのは(\protectedなマクロと同じく)、通常の展開の文脈と\edefの文脈で動作が変わる点であり、それ以外の点は普通である。むしろ\edefが\unexpandedを特別扱いしていると言って良いかもしれない。

挿入された\relax

\ifナントカ の潜在的な注意点

\ifナントカ系のコマンドは多くの場合引数をとるわけだが、注意しないと奇妙な結果が起こる可能性がある。

例えば、 \ifnum\count0=1 \message{Yes} \else \message{No} \fi とすると整数レジスターの0番目の値が1かどうかをテストし、条件が真の場合はYesを、偽の場合はNoを出力できる。ここで、条件が真の場合は何もせず、偽の場合のみ処理を行いたいとしよう。例えば、

\ifnum\count0=1\else \message{No} \fi

とするとどうなるか。

\ifnumは〈整数〉〈関係演算子〉〈整数〉の順番でトークン列を解釈しようとするわけだが、TeXの(文字列で記述する)整数の文法には注意すべき点があって、数字の列の終わりを確認するために、数字以外の展開不能トークンに遭遇するまで展開を続けようとする(もちろん、遭遇した(空白以外の)展開不能トークンが行方不明になると困るので、それは「先読みされたトークン列」にちゃんと戻す)。

例えば、

\def\foo{1}
\ifnum1=0\foo \message{1=0} \else \message{1/=0} \fi

を実行すると、\ifnumは0の直後のトークンを確認するまで展開を続けようとするので、\fooを展開してカテゴリーコード \(\mathsf{Other}\) の 1 を得る。これは数字なので\ifnumはさらに数字を読み取ろうとし、\messageに行き着く。\messageは展開不能トークンなので、\ifnumはこれで整数の表記が終わったと判断して、(トークン \message を「先読みされたトークン列」に積みつつ)条件判断に移る。

(整数の文法に関するこの性質を利用するのがいわゆる\romannumeralトリックだったりするわけだが、今回は関係ない)

話を戻して、

\ifnum\count0=1\else \message{No} \fi

を考えよう。

\ifnumは=の右辺の1を読み取り、数字列がまだ続くのか、それとも終わるのかを確認するために、次のトークン \else を展開しようとする。\elseは展開可能トークンなので、前回説明したように、次の\fiまで読み飛ばして空トークン列に展開される…のではなくて、展開器が状態として持つスタック \(\mathsf{condStack}\) の先頭が \(\mathsf{Then}\) ではないのでエラーが出る…と考えられる。

しかしこの仕様は不便(空白を1つ忘れただけでエラーが出る)である…と考えられたのかはわからないが、TeX by Topicの 13.8.2 The test wants to gobble up the \else or \fi によると、このような状況(条件の展開中)において\else, \or, \fiに遭遇した場合は、\relaxが挿入される。\relaxであれば整数など、大抵の読み取り操作は中断される。

このような仕組みにより、\ifナントカ系のコマンドが奇妙な動作をすることは避けられたのだ。めでたしめでたし。

この「\relaxの挿入」が言葉遊びではなく、観測可能な形で実在することを確かめるには、

\ifodd1\fi

を展開すると良い。 \message{\ifodd1\fi} を実行すると確かに\relaxと表示される。

挿入された\relaxの正体

だがちょっと待ってほしい。\relaxが挿入されるとはどういうことか。いくつか仮説を立てて検証してみよう。

仮説1. 文字通りの制御綴 \relaxが挿入される

仮に文字通りの制御綴 \relax が挿入されるとすると、 \let\relax=ナニカ によって制御綴 \relax の指す値が上書きされた場合に本来の挙動(整数等の読み取り操作を中止する)を果たさなくなってしまう。例えば

\def\relax{0} \ifodd1\fi

を実行すると、\fiの展開として \(\fbox{relax}\) が挿入され、 \(\fbox{relax}\) は展開可能なので展開すると整数0が出てくるので数字列はまだ続くと判断され、\fi がまた展開され、また \(\fbox{relax}\) が挿入され、…の繰り返しで、整数の読み取り操作を中止するどころか、無限ループが発生してしまう!

(制御綴 \relax の意味を上書きする行為は限りなく迷惑なので、例えばLaTeXの\newcommand系では禁止されているが、TeXプリミティブの\letや\defにはそのような安全装置は付いておらず、上書きし放題である)

幸い、 \def\relax{0} \ifodd1\fi を実際に試しても無限ループは起こらない。なので、\else/\or/\fiが「文字通りの制御綴 \relax を挿入する」動作ではないことは確認できる。

仮説2. noexpandマークのついたrelaxである

先ほど\noexpandの仕様を記述するために「noexpandマーク」を導入したが、ここで挿入された\relaxにもそういうマークがついているのではないか?そうすれば制御綴 \relax の意味が上書きされていても関係ない。

仮に先ほどのnoexpandマークと同じものであれば、トークンが指す値(意味)は \relax でではなく〈quasi \relax〉であり、\ifxで区別できるはずである。

やってみよう。

\expandafter\let\expandafter\foo\ifodd1\fi
\ifx\foo\relax \message{Yes} \else \message{No} \fi

実行結果は… Yes である!つまり、「挿入された\relax」の意味は\ifxでも\relaxと等価と判断される。ここで働いている機構はnoexpandマークとは別物であることがわかった。

挿入された\relaxについていると考えられるマークを仮に「insertedマーク」と呼ぶことにしよう。insertedマークは、次の性質を持つと考えられる:

  • \ifナントカ の条件の部分で展開可能プリミティブ\else,\or,\fiが展開された場合、展開結果のトークン列は「insertedマークのついた制御綴 \relax」および「展開されそうになったトークンそのもの」となる。
    • noexpandマークは任意のコマンド名につく可能性があったが、insertedマークがつきうるのは \relax のみである。
    • 普通の展開可能コマンドの展開結果は「展開時についていた名前」には依存しないが、 \else, \or, \fi は展開結果が展開時の名前に依存する。つまり、 \let\endif\fiとした状態で \ifodd\endif を展開したら、 \endif の展開結果は \relax \endif  という風に、名前 \endif が展開結果に含まれる。筆者の把握している限り、この性質を持つのは \else, \or, \fi のみである(\edefの文脈であれば\protectedマクロもそういうものと言って良いかもしれない)
  • insertedマークのついた制御綴 \relax の意味は、常に展開不能プリミティブ \relax である。

他の状況ではどう振る舞うか?検証してみよう:

  • noexpandマークが削ぎ落とされるような状況でも、insertedマークは生き残る。
    • \def や \edef で定義するマクロの本文中であっても、insertedマークのついた\relaxは通常の制御綴 \relax と区別される。。例: \expandafter\def\expandafter\foo\expandafter{\ifodd1\fi} で定義される \foo\def\foox{\relax\fi} で定義される \foox はどちらも\showすると macro:->\relax \fi . となるが、 \ifx の意味では異なる。
    • マクロのdelimited引数の検査ではinsertedマークは区別される。例:\def\foo\relax{} \expandafter\foo\ifodd1\fi を実行するとエラー ! Use of \foo doesn't match its definition. が起きる。
    • \uppercase や \lowercase を通してもinsertedマークは消滅しない。例: \def\relax{nyaa}\uppercase\expandafter{\expandafter\edef\expandafter\foo\expandafter{\ifodd1\fi}} で定義される\fooの中身は nyaa ではなく \relax である。

こうしてみると、noexpandマークとは大幅に様子が異なるようだ。トークンにマークをつけるというよりも、そもそもコマンド名のレベルで違う(見た目は\relaxというコマンド名だが、内部的には異なる)と言った方が良いのではないか?

そういうわけで、仮説3に辿り着く:

仮説3. \relaxのようでrelaxではないコマンド名である

前回の定義ではコマンド名の集合 \(\mathsf{CommandName}\) は

\[\begin{aligned}
\mathsf{CommandName}:=&\{\text{control sequence}\}\cup\{\text{active char}\}\\
=&\{\fbox{foo},\fbox{bar},\fbox{relax},\ldots\}\cup\{\mathtt{a}_{13},\mathtt{b}_{13},\ldots,\verb|~|_{13},\ldots\}
\end{aligned}\]

としたが、「挿入された\relax」を考慮すると、より精密には

\[\begin{aligned}
\mathsf{CommandName}:=&\{\text{control sequence}\}\cup\{\text{active char}\}\cup\{\langle\text{inserted \relax}\rangle\}\\
=&\{\fbox{foo},\fbox{bar},\fbox{relax},\ldots\}\cup\{\mathtt{a}_{13},\mathtt{b}_{13},\ldots,\verb|~|_{13},\ldots\}\cup\{\langle\text{inserted \relax}\rangle\}
\end{aligned}\]

とするべきだったのだ。

コマンド名 \(\langle\text{inserted \relax}\rangle\) に対応する値は常に(本物の)展開不能プリミティブ \relax であり、\letや\defによって上書きすることはできない。実際、 \let による上書きを目論んで

\edef\foo{\let\ifodd1\fi=0 }
% \show\foo すると macro:->\let \relax =0 . となる
\foo

を実行すると ! Missing control sequence inserted. というエラーが出る。

おまけ:無限に\relaxを挿入する

説明したように、\ifナントカの条件の部分では

\fi

の展開結果は

〈inserted \relax〉 \fi

となり、〈inserted \relax〉は展開不能なので、展開は普通はここで終了する。

もしもここで展開が終了せず、さらに \fi が展開されることがあったらどうなるか?

当然、さらに 〈inserted \relax〉 が挿入されるはずである。やってみよう。

\message{\ifodd1\expandafter\expandafter\fi} % --> \relax \relax

解説しておくと、

  1. \ifoddが 1 を読み込み、次のトークン \expandafter を展開しようとする。
    1. 最初の \expandafter は後続のトークン2つ(\expandafter \fi )を読み込み、1番目のトークン \expandafter を脇に置いて2番目のトークン \fi を展開しようとする。
    2. \ifの条件部での \fi の展開なので、展開結果は 〈inserted \relax〉 \fi となる。
    3. つまり、最初の \expandafter の展開結果は \expandafter 〈inserted \relax〉 \fi となる。
  2. \ifodd が数字を読み取るには、得られたトークン列 \expandafter 〈inserted \relax〉 \fi をさらに展開する必要がある。
    1. \expandafter は後続のトークン2つを読み込み、1番目のトークン 〈inserted \relax〉 を脇に置いて \fi を展開しようとする。
    2. \fi の展開結果は 〈inserted \relax〉 \fi となる。
    3. 脇に置いた 〈inserted \relax〉 を戻し、展開結果は 〈inserted \relax〉〈inserted \relax〉 \fi となる。
  3. 展開の結果として展開不能トークン 〈inserted \relax〉 が得られたので、 \ifodd は展開をやめる。

という流れで、 \ifodd1\expandafter\expandafter\fi の完全展開の結果は2個の\relaxとなり、 \message はそれを表示する。

というわけで、2個の \relax を挿入させることはできたが、 \fi の前に \relax が2つ並んでしまうと \expandafter でさらに \fi を展開させるということができないので、3個以上の \relax を挿入させるのは難しそうである。

しかし、LuaTeXにある \expanded プリミティブがあると話は変わってくる。

\expandedプリミティブはググっても情報がほとんど出てこない。pdfTeXに入る予定だったが(まだ?)入っていないという話がある。参考: http://tug.org/pipermail/lualatex-dev/2011-May/001227.html

【追記】\expandedは最近pdfTeX(や他のエンジン)にも実装されたようだ。詳しくは次の記事を参照。

\expandedプリミティブについて軽く説明しておこう。\expanded プリミティブは展開可能プリミティブであり、通常の展開の中で \edef と同等の完全展開を引き起こす。例えば、

\def\foo{\bar}\def\bar{baz}

と定義した状況で

\message{\unexpanded{\foo\foo}}

を実行すると \foo \foo が表示されるが、

\message{\unexpanded\expandafter{\expanded{\foo\foo}}}

を実行すると、中身が完全展開されて bazbaz が表示される。

この \expanded プリミティブを使うと、\else, \or, \fi に無限に \relax を挿入させることができる。

例えば、

\if\expanded{\fi}

を展開すると、 \if は引数を展開しようとして \expanded を展開しようとする。\expanded は引数 \fi を完全展開しようとするが、 \fi の展開結果には再び \fi が含まれるので、展開が無限ループして停止しなくなる。もっと悪いことに、LuaTeXには \expanded が展開するトークン列の長さを制限する機構はないようで、 ! TeX capacity exceeded の類のエラーが出ることもなくメモリを食いつぶす。

ちなみに、筆者のYuruMathにはこの手の無限ループを抑止する機構が備わっている。

我々はどうするか

isrelaxマーク

このように、伝統的なTeXのコマンド名と値に関する挙動を厳密に説明しようとすると、特殊な値 〈quasi \relax〉 やら特殊なコマンド名 〈inserted \relax〉 やらが登場する羽目になる。クソ

新たにTeX処理系を作る際は、このような実装の詳細まで再現する必要はないだろう。実際、現在のYuruMathにはこのような特殊な値や特殊なコマンド名はない。

もちろん\noexpandを実装するためのnoexpandマークに相当するものはあるが、「挿入された\relax」と統合して「isrelaxマーク」と呼んでいる。

つまり、YuruMathで展開結果としてありうるトークンの集合は

\[\begin{aligned}
\mathsf{Token}:=&\mathsf{CommandName}\cup\mathsf{CommandName}^{\mathsf{isrelax}}\cup\{x_\mathit{cc}\mid x\in\mathsf{Char},\mathit{cc}\in\mathsf{TCatCode}\}\\
=&\mathsf{CommandName}\cup\{x^{\mathsf{isrelax}}\mid x\in\mathsf{CommandName}\}\cup\{\mathtt{a}_{11},\mathtt{b}_{11},\ldots,\mathtt{a}_{12},\ldots,\mathtt{=}_{12},\ldots\}
\end{aligned}\]

であり、isrelaxマークがついたトークンの意味は、本物の展開不能プリミティブ \relax である。

\else,\or,\fiで挿入される\relaxは「isrelaxマーク」のついた\relaxトークンであり、マクロ本文や\uppercaseを通すとそのマークは失われる。

YuruMathは、組版の部分を除いてTeX言語をなるべく忠実に再現することを目指しているが、この程度の非互換は誰も気にしないだろう。

(ちなみに、YuruMathでは「字句解析の結果生じるトークン」の型と、isrelaxマークがつく可能性のある「展開結果としてありうるトークン」の型を区別している)

【追記】\expandafterとnoexpandマークは\expandafterとの関係でアレコレあるので、やっぱり「isrelaxマーク」に統合してはダメだった。詳しくは次の記事を参照。

\csnameの仕様改変案

isrelaxマークの応用として、「代入を引き起さず、展開結果のみを\relaxにする\csname」が考えられる。

普通は、\csnameの仕様を改変して代入という副作用をなくすとしたら、

  • 生成される制御綴の値が未定義の場合でも何もしない

という仕様が思い浮かぶ(LuaTeXにはこの仕様の\begincsnameがある)が、\csnameをこの仕様に改変するのは流石に非互換性が大きすぎるだろう。

それよりは元の\csnameに近い仕様として

  • 未定義の場合は展開結果のトークンにisrelaxマークをつける

が考えられる。この仕様であれば、\csname自体の展開結果の意味は\relaxのままであり、\csnameの副作用を利用するパターンの

\def\ifundefined#1{\expandafter\ifx\csname#1\endcsname\relax ...} % 引数を制御綴化したものが未定義または\relaxか調べる

というコードの挙動を変えないし、別のパターンとして

\def\begin#1{\begingroup\csname #1\endcsname} % コマンド名 <与えられた環境名> の値が未定義でもエラーにしない
\def\end#1{\csname end#1\endcsname\endgroup} % コマンド名 end<与えられた環境名> の値が未定義でもエラーにしない

のコードの挙動も変えない。なおかつ、「未定義の制御綴に\relaxを代入する」という副作用は起こらない。

\csnameの仕様をこれに変更しても既存のTeXコードはほぼそのまま動いちゃうのでは…?という気がするが、実際に試したわけではないのでわからない。


TeX言語のトークンと値 その2:\noexpandと、挿入された\relax」への1件のフィードバック

  1. ピンバック: TeX言語のトークンと値 その2.5:noexpandとexpandafterの話、それとexpandedの近況 | 雑記帳

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です