マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

LuaTeXで“relax化しないcsname”をつくる件

先日、TeX言語GW特別キャンペーン🍀なので、例によってクイズを出してみた。

その結果、例によって誰も解かなかったので(ざんねん🙃)本記事では正解例を発表することにする。

必要な前提知識

  • フツーの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で\newcatcodetableluatexbase.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言語活動の進捗が満足のいくものであったことを願います😃


  1. つまり、制御綴が\fooなら\let\foo\@undefinedとすればよい。
  2. この技法は実用のプログラムでも時々使われている。
  3. TeX言語ではトークン列を組み替える処理しかできないので、例えばfooという文字トークン列を組み替えて\fooという制御綴を作り出すことはできない。それをするためには\csnameという「機能」が必要なのである。
  4. つまり「まさかNot-a-Charの文字は使わないよね😊」と仮定している。