先日、TeX言語GW特別キャンペーン🍀なので、例によってクイズを出してみた。
【LuaTeXクイズ🌝🤮】
— 某ZR(ざんねん🙃) (@zr_tex8r) 2026年5月5日
以下の仕様を満たす命令 \myMakeCS をLuaTeXで実装せよ。
「\myMakeCS{‹文字トークン列›} をn回展開すると引数の文字列を名前とする制御綴のトークンになるが、\csname~\endcsname と異なり、当該の制御綴が元々未定義の場合に“relax化”が起こらない。」#TeX #TeX言語🍀
その結果、例によって誰も解かなかったので(ざんねん🙃)本記事では正解例を発表することにする。
必要な前提知識
- フツーのTeX言語🤮の知識。
- フツーにLuaTeXでLuaするための知識。
本記事で掲載するプログラムコードはplainまたはLaTeXの\makeatletterの状態を前提とする。
その問題
以下の仕様を満たす命令
\myMakeCSをLuaTeXで実装せよ。
「\myMakeCS{‹文字トークン列›}をn回展開すると引数の文字列を名前とする制御綴のトークンになるが、\csname~\endcsnameと異なり、当該の制御綴が元々未定義の場合に“relax化”が起こらない。」※nは引数非依存の定数で好きに決めてよい。
※もちろんLuaコードを使ってよい。
※ただしLuaコードを使わない回答ができればもっとよい🙃
この問題で難しい点は「“relax化”が起こらない」という条件である。文字トークン列から制御綴を生成するには普通は\csname~\endcsnameを使うが、これは“relax化”(当該の制御綴の意味が未定義だった場合に意味が\relaxプリミティブに変更される)が起こってしまい、もちろんLuaTeXでもその仕様は変わらない。
一方で「当該の制御綴が未定義であるか」の判定は\ifcsnameを使って(“relax化”を起こさずに)行える。定義済なら普通に\csnameが使えるので、以下では未定義である場合の対処を考えてみる。
以前の記事で紹介した通り、LuaTeXには\begincsnameという\csnameの変種があり、これは「“relax化”を起こさない」ので一見使えそうであるが、未定義時に制御綴を作らずに空に展開されてしまうので、この問題では実は役に立たない。TeX言語で「制御綴の生成」をしたいなら結局\csnameを使うしかないのである。
少し視点を変えてみる。\csnameによる“relax化”が避けられないなら、その後でもう一度その制御綴に未定義を代入1すればよい。しかし今回の問題では完全展開可能で実装する必要があるため、代入操作は使えない。“relax化”は局所的定義であることを利用して「\csnameの展開をグループ内で行う」という手段も考えられる2が、「グループに出入りする」のは実行操作であるため、完全展開可能の実装ではこの手段も使えない。
その正解例(Luaするやつ)
TeX言語だけで考えると行き詰ってしまうが、しかしこの問題はLuaTeXが前提なので、Luaコードが使える。
Luaコードではtex.sprint()関数でTeXコードの文字列3をプログラム的に組み立てることができる。この性質を利用すれば\csnameを使わずに容易に制御綴を「生成」できてしまう。\myMakeCSの引数(#1)の文字列をLuaで受け取って、「その前に\を付けた文字列をtex.sprint()する」だけでよい。このアイデアを素直に実装すると以下のようになる。
% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を % 名前とする制御綴になる. \def\myMakeCS#1{% \directlua{ tex.sprint("\string\\#1") }% }
※1回展開で\directlua{…}になり、さらに展開するとLuaの実行結果になる。常に2回展開で所望の制御綴が得られるので、問題の要件を「n=2」で満たしたことになる。
※\string\\を完全展開した結果はthe-文字列の\\なので、Lua文字列リテラルの中で\の文字を表す。
この素朴な実装は引数が「カテゴリコード11の文字」のみからなる場合には実際に問題の要件を満たす。
% \expandafterの3重連で"\mymakeCS"を2回展開する. % ("\show\duckduck"になるはず.) \expandafter\expandafter\expandafter\show\myMakeCS{duckduck} %==>"\duckduck=undefined."と表示(期待通り)
そうなると残っている課題は「引数がカテゴリコード11以外の文字を含む」場合である。この場合は「引数の文字列の前に\を付けたもの」が制御綴として字句解釈されないので、このままでは動作しない。しかしtex.sprint()は「カテゴリコードを予め指定した状態でTeXコード文字列を出力する」ことができる。だから、「全ての文字のカテゴリコードが11である状態」を用意すればこの問題は解決する。(ただし先頭に付ける\はエスケープ文字として働く必要があるので、取りあえず\のカテゴリコードだけは0にする。)
「カテゴリコードの状態」は実際にはLuaTeXの「カテゴリコードテーブル」の機能を用いて管理する。本記事ではカテゴリコードテーブルについての解説は割愛するので、プログラムコード内のコメントを見て概略を把握してほしい(ざんねん🙃)
%% 事前準備 % ※カテゴリコードテーブルの番号は(一旦)固定値100にする % カテゴリコードテーブルを生成(初期化)する \initcatcodetable100 % カテゴリコードテーブルの設定はTeXでもできるが面倒なので % Luaコードで実行する. \directlua{ % カテゴリコードを全て11に変更する for c = 0, 0x10FFFF do % カテゴリコードテーブル100の文字コードcのコードを11に設定 tex.setcatcode('global', 100, c, 11) end % ただし'\'(U+005C)は0にする tex.setcatcode('global', 100, 0x5C, 0) } %% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を % 名前とする制御綴になる. \def\myMakeCS#1{% \directlua{ % カテゴリコードテーブル100の状態でTeXコードを出力する tex.sprint(100, "\string\\#1") }% }
これで「英字以外を含む名前」でも要件通りに動作するようになる。
\expandafter\expandafter\expandafter\show\myMakeCS{duck?duck!} %==>"\duck?duck!=undefined."と表示(期待通り)
あとは、実用のコードで使用しても問題ないように調整を施す。
- カテゴリコードテーブルの番号を固定値(100)にしていたのを
\newcatcodetableによる動的確保に変える。
※\newcatcodetableで得られた番号をLuaコード中で参照するにはluatexbase.registernumber()関数を利用する。
※plainで\newcatcodetableやluatexbase.registernumber()を使うにはltluatex.texというファイルを読み込む必要がある。 - 引数文字列に
\を含められるようにするため、エスケープ文字を\からU+FDD1(Not-a-Charの符号位置の1つ4)に変更する。 - TeXコードの引数をLuaに渡すときに
\luaescapestringを使う。
最終的なプログラムは以下のようになった。
% plainではltluatex.texを読み込む \unless\ifdefined\newcatcodetable \input{ltluatex} \fi %% 事前準備 % \my@cc@table: 利用するカテゴリコードテーブルの番号 \newcatcodetable\my@cc@table \initcatcodetable\my@cc@table \directlua{ local escape_code = 0xFDD1 % エスケープ用の文字 local escape = utf8.char(escape_code) %% 所望のカテゴリコードテーブルを用意する local cctable = luatexbase.registernumber("my@cc@table") % カテゴリコードを全て11に変更する for c = 0, 0x10FFFF do tex.setcatcode('global', cctable, c, 11) end % ただしエスケープ用の文字は0にする tex.setcatcode('global', cctable, escape_code, 0) %% '\myMakeCS'のLua部分の実装 function my_make_cs(name) tex.sprint(cctable, escape..name) end } %% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を % 名前とする制御綴になる. \def\myMakeCS#1{% \directlua{my_make_cs("\luaescapestring{#1}")}% }
どうやら期待通りに動作しているようだ😍
\let\XA\expandafter \XA\XA\XA\show\myMakeCS{noexpand} % 定義済の制御綴 %==>「\noexpand=\noexpand.」と表示 \XA\XA\XA\show\myMakeCS{duck?duck!} % 未定義の制御綴 %==>「\duck?duck!=undefined.」と表示 \XA\XA\XA\show\myMakeCS{"アレ\string\?} %引数は「"アレ\?」 %==>「\"アレ\?=undefined.」と表示
まとめ
というわけで、TeX言語GW特別キャンペーン☘️も今日でおしまいです。皆さんのTeX言語活動の進捗が満足のいくものであったことを願います😃