これは「TeX & LaTeX Advent Caleandar 2022」の1日目の記事です。
(2日目は h20y6m さんです。)
例によって、今年も「TeX & LaTeX Advent Calendar」の季節が到来しました。素敵な記事が毎日(日付通りに
)届けられるといいですね!
さて、今年の重点テーマは(昨年の続きということで)「やっぱりTeX言語(とか)しましょう」です。というわけで、初日となる今日は、「自作のTeX言語プログラムの動作を解説する」というテーマで話をします。
対象となるTeX言語プログラムは……これです。
はい……「TeX & LaTeX Advent Calendar」の大ファンの方なら、こういうチョット読みにくいプログラムはお馴染みでしょう(クリスマス🎄の日によく見かけますね
)。読みにくいプログラムを頑張って “解読” してみましょう![]()
前提知識
-
フツーのTeX言語の知識。
この記事ではplain TeXを扱いますが、plain特有の知識は前提としないので「LaTeX上のTeX言語の知識」でかまいません。
\expandafterが絡んだトークン列の展開が解っていれば十分です。(フツーですね

)
\expandafterに自信がないという人は、例の「\expandafter のキホン」の記事を読んで復習しておきましょう。
「ドドスコ」(ラブ注入) を説明してみる話
「ドドスコ」というのは、今年(2022年)の夏に発生した「プログラミング大喜利」のお題の一つで、以下の仕様を満たすプログラムをつくることを求めています。
【問題】配列{"ドド","スコ"}からランダムに要素を標準出力し続け、『その並びが「ドドスコスコスコ」を3回繰り返したもの』に一致したときに「ラブ注入♡」と標準出力して終了するプログラムを作成せよ
この “お題” に寄せられた回答を集めた「Awesome-DDSK」というGitHubレポジトリも作られています。
本記事ではこの問題のことを「ドドスコ問題」と呼ぶことにします。
とにかく「ドドスコ」を実行してみる話
先ほどのTeX言語のソースファイルは以下のGistで公開しています。
- TeX言語でラブ注入♡(ドドスコプログラム)(Gist)
プログラムの実行方法は00README.mdファイルの説明の通りですが、改めて説明します。
- (チョット読みにくい)TeX言語ソースファイル
ddsk.tex - 謎のファイル
ddsk.tcx
この2つのファイルを同じディレクトリに置いた上で、そのディレクトリで次のようにpdftex(plain pdfTeXの起動コマンド)を実行すると、「ドドスコ問題」の出力が書かれたPDFファイルddsk.pdfが出力されます。
pdftex -translate-file=./ddsk ddsk.tex
「ドドスコ問題」の出力は乱数の出方によって毎回異なりますが、例えば以下のような出力が得られます。
何度か実行してみると、確かに「ドドスコ問題」の条件を満たす出力が得られていることがわかります。
まずはファイルを正しく“読んで”みる話
プログラムの動作は解ったので、改めて(チョット読みにくい)ソースを見てみましょう。
ド♡!ㇻゴタド入♡!ド!ド入♡!ド♡!ㇻゴタド!ド♡ド♡!ド♡ㇻゴタッド♡
ゴㇲプズブダズポタゴルド!ド!ド♡ッ♡♡ド♡ゴㇲプズブダズポタゴルド!ド!
ド♡ゴㇲプズブダズポタゴルド!ド♡ㇻゴタッド♡♡ド!ド♡ッ♡!!ド♡♡ド!
ド♡ㇻゴタド!ド♡♡ド♡!!ド♡♡ド♡♡!ド♡ズダボズブットゴッド!ド♡♡
ド♡!!ド♡♡ド♡♡♡ド♡ㇻトトラダゴド!ド♡♡ド♡!!ド♡♡ド♡!♡ド♡
トズタトラダゴド!ド♡ブゴゾトラグブタッド!ド!!ド!!スダド♡♡!ド!!
コストド♡トガズルダゴポッド!ド!♡ド!!ド♡!♡スボド!♡ド♡♡!ド!!
ド!!ド♡!♡スゾド!!ド♡ダゴポド!ド!♡♡ボド!!ド♡♡!ド!!ド!♡
ド♡♡♡ゾド!!スㇲド♡♡!ド!!コスズド♡♡!ド!!ド!♡ド♡♡!ド!!
ド!!ド♡♡♡スガド!♡♡スクド!♡♡スㇻド!♡♡スブド!♡♡スラド!♡♡
スコッド!♡♡スタッド!♡♡ススド!♡♡スプッド!♡♡スルド!!ド!!スズ
ド♡♡!ド!!ド!♡ド♡♡♡スソド!!ボッド!!スズッド♡♡!ド!!スズッ
ド♡♡!ド!!コスポド♡!♡スグド♡!♡ド!!ド♡!♡ド!!ド♡!♡ススッ
グ♡♡♡スドグ♡♡♡スタグ♡ㇻラゾッゴルトズソッゴッグ!ボグ♡ㇲダゴポグ!
グ!♡♡ボッコトドブドクスラドブドポドラドコドガコトスドブクブクコトドブド
クスラドブドポドラドコドガコトススブクブクㇻガコトドゴドポスガドクドスドコ
ドゴスコドダドㇻドコスㇻスブコトドドドポドゴスラコトドドラクブダドルスガス
プドダㇻダスㇻㇻダスコブㇻブドコトドドドポドゴスラコトドドラㇻブダドルスガ
スプドダㇻダスㇻㇻダスコブコブクコトドドドポドゴスラコトドドラブブダドルス
ガスプドダㇻダスㇻㇻダスコブドドブコトドドドポドゴスラコトドドラソブダドル
スガスプドダㇻダスㇻㇻダスコブブブガコトドドラソコトドㇻドクスブドコドトド
ルドゴドコスブドソドルスガブダブクブㇻスガスラスガドトスコスブブㇻスガスラ
コトドプスブドルスズドコブダブラブガドコドダコトドゴドコススドルドドコトド
ルドドラブコトドゴドコススドブドポスコドゴスラコトドルコブコトドゴドコスス
ドブドポスコドゴスラコトドルコㇻコトドゴドポドルドゴドラドコドゴスラコトラ
ブスラスㇻスコドココトドトドポドポスガコトドルドドラブコトドルコㇻブダコト
スガドラドドスコドゴドルドドドポスㇻドダドラドコスドドルドクスラドコブㇻコ
トスㇻドコドトドクスプコトドルドドドポドラドラコトドルコㇻコトドブドプドク
スㇻブㇻブガブクコトドブドプドクスㇻブㇻブガブクㇻガコトドコドトスブドココ
トドブドプドクスㇻブクブプブココトドブドプドクスㇻブクブスブルㇻガコトドド
ドルコトドプスブドソドルスガブガスガスラㇻガコトドダスコドトスラドルスガド
トスルコトドルコブブㇻコトドクドラスドドクドゴドブドココトドルコブコトドル
コㇻコトドルドドドゴスコドダコトドルコブブトㇻㇻブクブガブガブガㇻガコトド
コドトスブドココトドクドラスドドクドゴドブドココトドルコブㇻダㇻㇻブクブガ
ブガブガㇻガコトドドドルコトドルドドドゴスコドダコトドルコブブダㇻㇻブプブ
プブプㇻガコトラブドドドクドトスブドココトドドドルコトスㇻドコスガドコドク
スラコトドブドプドクスㇻブㇻブブブブコトドブドプドクスㇻブㇻブクブラコトド
プスブドソドルスガブガスガスラㇻガコトドドラブコトドブドプドクスㇻブㇻブブ
ブㇻコトドドラㇻコトドブドプドクスㇻブクブガブクコトドプスブドソドルスガブ
ガスガスラㇻガコトドドラクコトドブドプドクスㇻブルブスㇻガコトドㇻスルドコ
ゾゾゾド♡♡ド♡!!ド♡♡ド♡♡♡ド♡プダポグブゴソトズプゴガゴㇲド!ド♡
ソトズブタラクゴブソド!ド♡♡ボド♡♡♡ド♡♡ボド!♡♡ゾゾド!ラブ注入♡
……なんというか「チョット読みにくい」どころの話ではなく全くわけがわからないですね
\defとか\expandafterとかのTeX言語のお馴染みのモノは全く見当たらず、代わりに日本語文字だけが延々と並んでいます。これは本当にTeX言語のプログラムなのでしょうか![]()
そういえば、エンジンとしてpdfTeXを使っていました。pdfTeXは、LuaTeXのような「Unicodeを扱うTeX」とも(u)pTeXのような「日本語文字を扱うTeX」とも違い、ソースコードを常に「バイトの列」として取り扱います。ということは、今見ている「奇妙な日本語文字の羅列」のソースコードは、pdfTeXには次のようなバイト列として見えているはずです。
E3 83 89 E2 99 A1 EF BC 81 E3 87 BB E3 82 B4 E3
82 BF E3 83 89 E5 85 A5 E2 99 A1 EF BC 81 E3 83
89 EF BC 81 E3 83 89 E5 85 A5 E2 99 A1 EF BC 81
E3 83 89 E2 99 A1 EF BC 81 E3 87 BB E3 82 B4 E3
82 BF E3 83 89 EF BC 81 E3 83 89 E2 99 A1 E3 83
89 E2 99 A1 EF BC 81 E3 83 89 E2 99 A1 E3 87 BB
E3 82 B4 E3 82 BF E3 83 83 E3 83 89 E2 99 A1 0A
E3 82 B4 E3 87 B2 E3 83 97 E3 82 BA E3 83 96 E3
83 80 E3 82 BA E3 83 9D ......(以下省略)
ではこれでTeX言語のプログラムとして解釈できるようになったかというと、結局「80h以上のバイトが延々と並んでいる」状態になっただけです。別に「plainフォーマットでは80h以上のバイトに特別な意味が与えられている」わけではないので全く解釈できそうにありません。実際、この文字列(バイト列)自体はそのままでは有効なTeX言語のプログラムにはなっていません。このファイルをコンパイルした際に “謎のオプション” を付けていたことを思い出しましょう。
pdftex -translate-file=./ddsk ddsk.tex
ここでは-translate-file=./ddskというオプションを付けています。オプション名に “file” とあるので、恐らく何かのファイルを指定しているのでしょう。そういえば、ddsk.texの他にもう1つ、ddsk.tcxという “謎のファイル” がありました。
0x80 0x64
0x81 0x65
0x82 0x00
0x83 0x00
0x85 0x6e
0x87 0x00
0x88 0x63
0x89 0x5c
「80h以上の値と80h未満の値のペア」が書かれた行が並んでいます。どうやらこれが “暗号解読表” になっていそうです
「左列のバイト値を対応する右列のバイト列に置き換える」という変換をddsk.texに適用してみましょう。
00 00 5C 63 73 6E 61 6D 65 00 00 6C 00 00 65 00
00 74 00 00 5C 65 6E 64 63 73 6E 61 6D 65 00 00
5C 61 6D 65 00 00 5C 65 6E 64 63 73 6E 61 6D 65
00 00 5C 63 73 6E 61 6D 65 00 00 6C 00 00 65 00
00 74 00 00 5C 61 6D 65 00 00 5C 63 73 6E 00 00
5C 63 73 6E 61 6D 65 00 00 5C 63 73 6E 00 00 6C
00 00 65 00 00 74 00 00 00 00 00 5C 63 73 6E 0A
00 00 65 00 00 78 00 00 70 00 00 61 00 00 6E 00
00 64 00 00 61 00 00 66 ...(以下省略)
所々にNULバイト(値が00hのバイト)が含まれていますが、TeXにとってはNULバイトも有効な入力です。とりあえずNULバイトを·で表して文字に起こすと以下のようになります。
··\csname··l··e··t··\endcsname··\ame··\endcsname··\csname··l··e··t··\ame··\csn··\csname··\csn··l··e··t·····\csn
··e··x··p··a··n··d··a··f··t··e··r··\ame··\ame··\csn···csncsn··\csn··e··x··p··a··n··d··a··f··t··e··r··\ame··\ame
··\csn··e··x··p··a··n··d··a··f··t··e··r··\ame··\csn··l··e··t·····\csncsn··\ame··\csn···csnameame··\csncsn··\ame
··\csn··l··e··t··\ame··\csncsn··\csnameame··\csncsn··\csncsname··\csn··a··d·····a··n·····c··e·····\ame··\csncsn
··\csnameame··\csncsn··\csncsncsn··\csn··l··c··c··o··d··e··\ame··\csncsn··\csnameame··\csncsn··\csnamecsn··\csn
··c··a··t··c··o··d··e··\ame··\csn··n··e·····c··o··u··n··t·····\ame··\ameame··\ameame··`··d··\csncsname··\ameame
·····`··c··\csn··c··h··a··r··d··e··f·····\ame··\amecsn··\ameame··\csnamecsn··`·····\amecsn··\csncsname··\ameame
··\ameame··\csnamecsn··`·····\ameame··\csn··d··e··f··\ame··\amecsncsn·····\ameame··\csncsname··\ameame··\amecsn
...(以下省略)
お馴染みの\csnameや\endcsnameが出てきました
さらによく見るとNUL文字に紛れて “expandafter” という文字列も確認できます
どうやらddsk.tcxが解読表になっているという解釈で当たっているようです。
ここで使われているのは「TeX Character Translation (TCX)」と呼ばれている機能で、本来は「EBCDIC等のASCII非互換の文字コードが使われているシステム」の上でのTeXの運用を容易にすることを目的としています1。システムの文字コードからASCIIへの対応をTCXファイル(拡張子.tcx)に書いておいて、そのファイルを--translate-fileオプションで指定することで、システムのASCII非互換な文字コードのファイルが(何も改変していない)TeXエンジンでそのまま扱えるようになります。今ではEBCDIC系のシステムはほぼ絶滅してしまっているので、TCXの機能が本来の目的で使われることはもはやなさそうです。
結局、この “解読後” のソースコードをplain TeXで実行することを考えればいいわけですが、いきなり先頭にNULバイトがあるので、plainでのNULバイトの扱いを調べてみます。plain.texには以下のような記述(コメント)があります。
% When INITEX (the TeX initializer) starts up,
% it has defined the following \catcode values:
% \catcode`\^^@=9 % ascii null is ignored
つまり、NULバイト(TeXエスケープ表記で^^@)のカテゴリコードは「TeXの初期値で9であり、plainではそれを変えていない」ということです。TeXの字句解析においてカテゴリコード9(無視)は以下のような性質をもちます。
- その文字自体は無視される(トークンにならない)。例えば、
foo^^@barはfoobarと書いたのと等価になる。 - ただし制御語2の名前のスキャン中にその文字が現れた場合はスキャンはそこで終わる。つまり、
\foo^^@Xは「制御綴\fooX」ではなく「制御綴\fooと文字トークンXの列」と解釈される。これは\foo Xと空白文字を置いたのと等価になる。
要するに、「NULバイトは制御語の直後では空白文字と同じで、それ以外の箇所では何もないのと同じ」ということになります。NULバイトは邪魔なので、この性質を利用してNULバイトを除去することにします。結果のソースコードは以下の通りです。
\csname let\endcsname\ame\endcsname\csname let\ame\csn\csname\csn let\csn
expandafter\ame\ame\csn csncsn\csn expandafter\ame\ame
\csn expandafter\ame\csn let\csncsn\ame\csn csnameame\csncsn\ame
\csn let\ame\csncsn\csnameame\csncsn\csncsname\csn advance\ame\csncsn
\csnameame\csncsn\csncsncsn\csn lccode\ame\csncsn\csnameame\csncsn\csnamecsn\csn
catcode\ame\csn newcount\ame\ameame\ameame `d\csncsname\ameame
-`c\csn chardef\ame\amecsn\ameame\csnamecsn `v\amecsn\csncsname\ameame
\ameame\csnamecsn `w\ameame\csn def\ame\amecsncsn v\ameame\csncsname\ameame\amecsn
\csncsncsn w\ameame `x\csncsname\ameame -`a\csncsname\ameame\amecsn\csncsname\ameame
\ameame\csncsncsn `h\amecsncsn `k\amecsncsn `l\amecsncsn `n\amecsncsn `o\amecsncsn
`-\amecsncsn `t\amecsncsn ``\amecsncsn `p\amecsncsn `r\ameame\ameame `a
\csncsname\ameame\amecsn\csncsncsn `s\ameame v\ameame `a\csncsname\ameame `a
\csncsname\ameame -`f\csnamecsn `u\csnamecsn\ameame\csnamecsn\ameame\csnamecsn ``
ucsncsncsn `\ucsncsncsn `tucsn lowercaseuame vucsn xdefuame
uamecsncsn v-c\n\k`o\n\f\o\-\h-c`\nknk-c\n\
k`o\n\f\o\-\h-c``nknklh-c\e\f`h\k\`\-
\e`-\d\l\-`l`n-c\\\f\e`o-c\\oknd\r`h`
p\dld`lld`-nln\-c\\\f\e`o-c\\olnd\r`h
`p\dld`lld`-n-nk-c\\\f\e`o-c\\onnd\r`
h`p\dld`lld`-n\\n-c\\\f\e`o-c\\osnd\r
`h`p\dld`lld`-nnnh-c\\os-c\l\k`n\-\c\
r\e\-`n\s\r`hndnknl`h`o`h\c`-`nnl`h`o
-c\p`n\r`a\-ndnonh\-\d-c\e\-``\r\\-c\
r\\on-c\e\-``\n\f`-\e`o-c\r-n-c\e\-``
\n\f`-\e`o-c\r-l-c\e\f\r\e\o\-\e`o-co
n`o`l`-\--c\c\f\f`h-c\r\\on-c\r-lnd-c
`h\o\\`-\e\r\\\f`l\d\o\-`\\r\k`o\-nl-
c`l\-\c\k`p-c\r\\\f\o\o-c\r-l-c\n\p\k
`lnlnhnk-c\n\p\k`lnlnhnklh-c\-\c`n\--
c\n\p\k`lnknpn--c\n\p\k`lnkn`nrlh-c\\
\r-c\p`n\s\r`hnh`h`olh-c\d`-\c`o\r`h\
c`r-c\r-nnl-c\k\o`\\k\e\n\--c\r-n-c\r
-l-c\r\\\e`-\d-c\r-nncllnknhnhnhlh-c\
-\c`n\--c\k\o`\\k\e\n\--c\r-nldllnknh
nhnhlh-c\\\r-c\r\\\e`-\d-c\r-nndllnpn
pnplh-con\\\k\c`n\--c\\\r-c`l\-`h\-\k
`o-c\n\p\k`lnlnnnn-c\n\p\k`lnlnkno-c\
p`n\s\r`hnh`h`olh-c\\on-c\n\p\k`lnlnn
nl-c\\ol-c\n\p\k`lnknhnk-c\p`n\s\r`hn
h`h`olh-c\\ok-c\n\p\k`lnrn`lh-c\l`r\-
www\csncsn\csnameame\csncsn\csncsncsn\csn pdfunescapehex\ame\csn
scantokens\ame\csncsn v\csncsncsn\csncsn v\amecsncsn ww\ame on-cendcsn
以下の解説ではこの “整形後” のコードを読んでいくことにします。
TeX言語では文字のカテゴリコードを変更できるため、実際にはコードの実行を追ってみないと「コードのどの部分が制御語なのか」は正確にはわかりません。(実際、このコードでは途中で\のカテゴリコードが変更されています。)しかしここでは説明の便宜のため、NULバイトを “正しく除去” したコードを前もって提示することにしました。実際に “解読作業” を行う際には、影響のあるカテゴリコード変更が判明した時点でそれ以降のNULバイト除去をやり直す必要があります。
頑張ってソースコードを解読してみる話
1~4行目
\csname let\endcsname\ame\endcsname\csname let\ame\csn\csname\csn let\csn
expandafter\ame\ame\csn csncsn\csn expandafter\ame\ame
\csn expandafter\ame\csn let\csncsn\ame\csn csnameame\csncsn\ame
\csn let\ame\csncsn\csnameame\csncsn\csncsname\csn advance\ame\csncsn
先頭にあるのは\csname let\endcsnameでこれを展開すると\letとなるので、結局このコードの “最初の文” はこれになります。
\csname let\endcsname\ame\endcsname
↓(展開)
\let\ame\endcsname
つまり\ameが\endcsnameと等価になりました。
その次には\csname let\ameとあり、\ame=\endcsnameなのでこうなります3。
\csname let\ame\csn\csname
↓(展開)
\let\csn\csname
\csnが\csnameと等価になりました。ちょうど “csname” の前半 “csn” と後半 “ame” になっていて意外とわかりやすいですね(えっ
)。この後にも “csn” と “ame” を並べた名前の制御綴がやたらと登場しますが、これらは(\csname以外は)全てTeXにとっては意味のない(未定義の)制御綴です4。
さて、次の “文” は少し長くて3行目の最初の\ameの位置まで続きます5。
\csn let\csn expandafter\ame\ame\csn csncsn\csn expandafter\ame\ame\csn expandafter\ame
\csname(=\csn)と\endcsname(=\ame)の組がネストしていて、しかもこれを展開すると\expandafterが多数出現してかなり複雑なことになりそうです。説明のため、\csnameと\endcsnameを\<と\>と略記して、かつ各々の出現に番号を付けてみます。
\<₁let\<₂expandafter\>₃\>₄\<₅csncsn\<₆expandafter\>₇\>₈\<₉expandafter\>₁₀
ここで「①を(先頭で)1回展開した結果のトークン列は何か」を考えてみましょう。\csnameの展開結果は「『対応する\endcsnameとの間にあるトークン列の完全展開』を名前とする制御綴」であることに注意すると以下のようになります。
- 先頭の
\<₁を展開しようとする。 -
\<₁に対応する\endcsnameは\>₄なので、制御綴名を得るためにlet\<₂expandafter\>₃を完全展開する。-
letは展開不能な文字トークンだからそのまま残る。 -
\<₂expandafter\>₃を展開して\expandafterを得る。\expandafterプリミティブは展開可能なので、さらに展開しようとする。 -
\expandafterに続く2つのトークンは\>₄\<₅であり、\expandafter\>₄\<₅の展開結果は(例の記事に従って考えると)「\>₄の後に『\<₅を1回展開したトークン列』を続けたもの」になる。- (ここに
\<₅を1回展開する処理が入る。)
- (ここに
-
\>₄に達したので終了。制御綴名は “let” なので展開結果は\letである。
-
「\<₅を1回展開する」の部分の考慮を一旦措いてまとめると、①の1回展開は以下のようになります。
\let【\<₅以降のトークン列の1回展開】
次にその「\<₅の1回展開」を考えますが、ここで「\<₁から\>₄までのトークン列」と「\<₅から\>₈までのトークン列」が全く同じ構造を持っていることに注意してください。つまり「\<₅の1回展開」の際には先ほどの「\<₁の1回展開」での過程がもう一度繰り返されるのです。
- 先頭の
\<₅を展開しようとする。 -
\<₅に対応する\endcsnameは\>₈なので、let\<₆expandafter\>₇を完全展開する。-
csncsnは展開不能だからそのまま残る。 -
\<₆expandafter\>₇を展開して\expandafterを得る。さらに展開しようとする。 -
\expandafter\>₈\<₉の展開結果は「\>₈の後に\<₉を1回展開したトークン列」になる。- (ここに
\<₉を1回展開する処理が入る。)
- (ここに
-
\>₈に達したので終了。制御綴名は “csncsn” なので展開結果は\csncsnである。
-
従って結果は以下のようになります。なお、\<₅の1回展開は\<₁の1回展開に付随して行われるため、以下に示す結果は「①の2回展開」ではなく「①の1回展開」であることにも注意しましょう。
\let\csncsn【\<₉以降のトークン列の1回展開】
「\<₉以降のトークン列」というのは\<₉expandafter\>₁₀であり、その1回展開は\expandafterです6。ここでようやく「①の1回展開」の正しい姿が判明しました。
\let\csncsn\expandafter
あの長い文の目的は「制御綴\csncsnを\expandafterと等価にする」ことでした。
次の文(4行目の最初の\ameの位置まで)を見ましょう。
\csn let\csncsn\ame\csn csnameame\csncsn\ame\csn let\ame
‖(等価)
\<let\expandafter\>\<csnameame\expandafter\>\<let\>
先ほどの①の場合と同様に展開を追うと(ほとんど同じなので詳細は省略します)、②の1回展開は以下のようになることがわかります。
\let\csnameame\let
ここまでの動作をまとめると以下のようになります。\expandafterと\letが“単一の制御綴として使える”ようになりました。
-
\csn=\csname -
\ame=\endcsname -
\csncsn=\expandafter -
\csnameame=\let
4~6行目
TeXのプリミティブと等価である独自の制御綴は元のプリミティブに置き換えて示します。ただし\csnameと\endcsnameは見やすさのため\<と\>で示します。また、前節までで既に実行が終わっている部分は省略して……で示します。
……\expandafter\let\expandafter\csncsname\<advance\>\expandafter
\let\expandafter\csncsncsn\<lccode\>\expandafter\let\expandafter\csnamecsn\<
catcode\>\< newcount\>\ameame\ameame `d\csncsname\ameame
前節ではいきなり超絶複雑な展開過程が登場しましたが、そこを乗り越えてしまえば、後に出てくる展開過程はずっと簡単です。
\expandafter\let\expandafter\csncsname\<₁advance\>₂
これは典型的な「\expandafter の鎖」(詳細は例の記事を参照)のパターンになっていて、先頭の\expandafterを展開すると\<₁が1回展開されます。つまりこうなります。
\let\csncsname\advance
次の2つの文も全く同じ構造をもっています。
\expandafter\let\expandafter\csncsncsn\<lccode\>
↓(展開)
\let\csncsncsn\lccode
\expandafter\let\expandafter\csnamecsn\<catcode\>
↓(展開)
\let\csnamecsn\catcode
本節の動作のまとめです。“制御綴として使える” プリミティブがさらに増えました。
-
\csncsname=\advance -
\csncsncsn=\lccode -
\csnamecsn=\catcode
6~8行目
……\<newcount\>\ameame\ameame`d\advance\ameame
-`c\<chardef\>\amecsn\ameame\catcode`v\amecsn\advance\ameame
\ameame\catcode`w\ameame\<def\>\amecsncsn v\ameame\advance\ameame\amecsn
かなりフツーのコードになってきましたね
ここからは複数の文をまとめて見ていくことにします。
\<newcount\>\ameame
\ameame`d
\advance\ameame-`c
‖(上の3文は下と等価)
\newcount\ameame
\ameame=100
\advance \ameame by -99 % (値は1)
整数レジスタトークン\ameameを定義して、それに100を代入してすぐに99を引いています。\ameameの現在の値は1です。
\<chardef\>\amecsn\ameame
‖(等価)
\chardef\amecsn=\ameame % (値は1)
値が1であるchardefトークン\amecsnを定義しています。TeX言語プログラミングでよく使われる手法ですが、このchardefは「整数定数として使う」ことを意図されています。つまり\amecsnは「定数の1」だというわけです。
\catcode`v\amecsn
\advance\ameame\ameame
\catcode`w\ameame
‖(等価)
\catcode`v=1
\advance \ameame by \ameame % (値は2)
\catcode`w=\ameame % (値は2)
文字vのカテゴリコードを1({相当)、wのカテゴリコードを2(}相当)に変更しました。
本節の動作のまとめです。
- 整数レジスタ
\ameameを定義。(現在の値は2) - 定数:
amecsn= 1 - カテゴリコード変更:
v→1;w→2
8~12行目
……\< def\>\amecsncsn v\ameame\advance\ameame\amecsn
\lccode w\ameame`x\advance\ameame-`a\advance\ameame\amecsn\advance\ameame
\ameame\lccode`h\amecsncsn`k\amecsncsn`l\amecsncsn`n\amecsncsn`o\amecsncsn
`-\amecsncsn`t\amecsncsn``\amecsncsn`p\amecsncsn`r\ameame\ameame`a
\advance\ameame\amecsn\lccode`s\ameame v\ameame`a\advance\ameame`a
どんどん読み進めていきましょう。
\< def\>\amecsncsn v\ameame\advance\ameame\amecsn\lccode w
‖(等価)
\def\amecsncsn{\ameame\advance\ameame\amecsn\lccode}
vとwが{と}に相当するので、要するに単純なマクロ定義です。この中のコードはそのままでは意味を成していないように見えますが、それで問題がないことが後でわかります。
\ameame`x
\advance\ameame-`a
\advance\ameame\amecsn
\advance\ameame\ameame
‖(等価)
\ameame=120
\advance \ameame by -97 % (値は23)
\advance \ameame by 1 % (値は24)
\advance \ameame by \ameame % (値は48)
要するに\ameameに48を代入しています。次に続くコードは少し変な形をしています。
\lccode`h\amecsncsn
\lccode`hで文を開始しているためこれは「パラメタ\lccode`h7への代入文」であり、その途中8にマクロである\amecsncsnが現れているため、この\amecsncsnは完全展開されます。つまりこうなるわけです。
\lccode`h\ameame\advance\ameame\amecsn\lccode
すなわち「\lccode`h\ameame」「\advance\ameame\amecsn」の2つの文が実行されて、そして\lccodeでまた次の文が開始されています。その後に続いているのは……。
(\lccode は前の \amecsncsn の展開の残り)
\lccode`k\amecsncsn
また同じパターンになっています。つまり、10行目の\lccodeから次の行の\ameameは同じパターンの繰り返しになっていて、\amecsncsnを(結局展開されるわけなので)その内容に置き換えると“文単位”に分けて理解できます。
\lccode`h\amecsncsn
`k\amecsncsn
`l\amecsncsn
`n\amecsncsn
`o\amecsncsn
`-\amecsncsn
`t\amecsncsn
``\amecsncsn
`p\amecsncsn
`r\ameame
‖(等価)
\lccode`h=\ameame % (値は48=`0)
\advance\ameame by 1 \lccode`k=\ameame % (値は49=`1)
\advance\ameame by 1 \lccode`l=\ameame % (値は50=`2)
\advance\ameame by 1 \lccode`n=\ameame % (値は51=`3)
\advance\ameame by 1 \lccode`o=\ameame % (値は52=`4)
\advance\ameame by 1 \lccode`-=\ameame % (値は53=`5)
\advance\ameame by 1 \lccode`t=\ameame % (値は54=`6)
\advance\ameame by 1 \lccode``=\ameame % (値は55=`7)
\advance\ameame by 1 \lccode`p=\ameame % (値は56=`8)
\advance\ameame by 1 \lccode`r=\ameame % (値は57=`9)
結局、いくつかの文字(h,k,l,n,o,-,t,`,p,r)のlccodeを数字(0~9)に設定していることがわかりました。例えばこの直後で\lowercase{hklno-t`pr}を実行したとすると「0123456789」と出力されるはずです。
\ameame`a
\advance\ameame\amecsn
\lccode`s\ameame
‖(等価)
\ameame=97
\advance \ameame by 1
\lccode`s=\ameame % (値は98=`b)
今度はsのlccodeをbに設定しました。
本節の動作のまとめです。どうやら\lowercaseで文字列変換をしたいようですね。
-
h,k,l,n,o,-,t,`,p,r,sのlccodeをそれぞれ
0,1,2,3,4,5,6,7,8,9,bに設定。 -
\ameameの現在の値は98
※\amecsncsnの定義はこの先使わないので忘れてもよいでしょう。
12~15行目
……v\ameame`a\advance\ameame`a
\advance\ameame-`f\catcode`u\catcode\ameame\catcode\ameame\catcode``
ulccode`\ulccode`tucsn lowercaseuame vucsn xdefuame
uamecsncsn v-c\n\k`o\n\f\o\-\h-c`\nknk-c\n\
15行目の途中から40行目まで、TeXとして明らかに意味をなさない変な文字列が続いています。その直前のvまでを読みます。
v
文頭がいきなりv(つまり{)なので、これは局所化グループの開始です。
\ameame`a
\advance\ameame`a
\advance\ameame-`f
\catcode`u\catcode\ameame
\catcode\ameame\catcode``
‖(等価)
\ameame=97
\advance \ameame by 97 % (値は194)
\advance \ameame by -102 % (値は92=`\)
\catcode`u=\catcode\ameame % (値は0)
\catcode\ameame\catcode`` % (値は12)
ここで\のカテゴリコードを0から12(通常文字)に変えて、代わりにuのカテゴリコードを\相当の0に変えています。これ以降は\でなくてuで制御綴を始めることになります。
ulccode`\ulccode`t
‖(等価)
\lccode`\\=\lccode`t % (値は54=`6)
文字\9のlccodeをtのlccodeと同じ、つまり数字の6に設定しました。
ucsn lowercaseuame v
‖(等価)
\<lowercase\>{
↓(展開)
\lowercase{
\lowercaseの引数の中に入りました。末尾のv(={)に対応するw(=})が現れるまで、文字トークンには小文字変換が適用されることになります。
ucsn xdefuame uamecsncsn v
‖(等価)
\<xdef\>\amecsncsn {
(小文字変換しても変化なし)
↓(展開)
\xdef\amecsncsn{
小文字変換は文字トークンにのみ適用されるので、uame(=\ame)等の制御綴は変化しません。xdefは文字トークン列なので適用されますが、これらの文字は「先にlccodeを設定した文字」には含まれておらず、またplainのデフォルトでも(元々小文字であるため)lccodeが設定されていません。なので結局xdefのまま変化しません10。結局\xdefが実行されることになります。
本節の動作のまとめです。
-
\lowercase{\xdef\amecsncsn{の中に入った。
15~40行目
……-c\n\k`o\n\f\o\-\h-c`\nknk-c\n\
k`o\n\f\o\-\h-c``nknklh-c\e\f`h\k\`\-
……(中略)……
nl-c\\ol-c\n\p\k`lnknhnk-c\p`n\s\r`hn
h`h`olh-c\\ok-c\n\p\k`lnrn`lh-c\l`r\-
ここは以下の文字群のみからなる(TeXとして意味を成さない)文字列となっています。
h k l n o - \ ` p r a s c d e f
前節で\lowercaseの引数の中に入っているため小文字変換が適用され、lccodeの設定によりそれぞれ以下の文字に変換されます(a,c,d,e,fは変化なし)。
0 1 2 3 4 5 6 7 8 9 a b c d e f
つまり、この部分の文字列は「16進数字の列」に変換されることになります。具体的には次の文字列です。なお、改行には何も細工をしてないので、通常のTeXの字句解析規則に従って、改行は空白文字11として読み込まれます。
5c636174636f6465605c7631315c636
174636f6465605c773131205c6e6f70616765
6e756d626572735c666f6e745c66413d69707
86d2d722d7532365c666f6e745c66423d6970
786d2d722d7535315c666f6e745c66433d697
0786d2d722d7536635c666f6e745c664b3d69
70786d2d722d7533305c664b5c626173656c6
96e65736b69703d31327074706c7573327074
5c6873697a653d3430656d5c6e657769665c6
966435c6e6577636f756e745c69535c6e6577
636f756e745c69525c6e6f696e64656e745c4
3747275655c6c6f6f705c6966435c69523d5c
706466756e69666f726d64657669617465325
c72656c61785c69666f64645c69525c636861
723230315c63686172323031205c656c73655
c636861723138355c63686172313739205c66
695c68736b6970307074205c6d756c7469706
c795c6953325c616476616e63655c69535c69
525c69666e756d5c69533c2231303030205c6
56c73655c616476616e63655c69532d223130
3030205c66695c69666e756d5c69533d22383
838205c4366616c73655c66695c7265706561
745c636861723233335c636861723231345c6
8736b6970307074205c66435c636861723233
325c66425c636861723130315c68736b69703
07074205c66415c636861723937205c627965
41~42行目
www\expandafter\let\expandafter\csncsncsn\<pdfunescapehex\>\<
scantokens\>\expandafter v\csncsncsn\expandafter v\amecsncsn ww\> on-cendcsn
いよいよ最後の2行になりました。ところでこれまでの動作を振り返ってみると、設定や定義はイロイロとやっているものの、まだ何も出力していません。そういえば、このプログラムはそもそも「ドドスコ問題」を解くためのものだったはずですが、当問題に関する定義っぽいものも何も出てきていません。一体どうやって問題を解いているのでしょう……![]()
ww
最初のw(=})で⑥に現れたv(={)を、次のwで⑤に現れたvを閉じています。改めて⑤から読み直すと、以下のようなコードを実行したことになります。
\lowercase{\xdef\amecsncsn{-c\n\k`o\n……lh-c\l`r\-␣}}
(ただし内側の{}の中の \ はカテゴリコード12)
\lowercaseの引数中で実行されたコードは以下の通りで、先ほど示した16進数字の文字列がマクロ\amecsncsnにグローバルに定義されました12。
\xdef\amecsncsn{5c63617463……205c627965␣}
その次にまたwがあります。
w
これは④のvを閉じています。これで\とuのカテゴリコード(および\のlccode)が元に戻ります。つまりこれ以降はフツーに\で制御綴が始まる世界に戻ります。
\expandafter\let\expandafter\csncsncsn\<pdfunescapehex\>
↓(展開)
\let\csncsncsn\pdfunescapehex
\csncsncsnを\pdfunescapehexというpdfTeX拡張プリミティブと等価にしました。(これまで\csncsncsnは\lccodeと等価でした。)
\<scantokens\>\expandafter v\csncsncsn\expandafter v\amecsncsn ww
‖(等価)
\<scantokens\>\expandafter{\pdfunescapehex\expandafter{\amecsncsn}}
↓(展開)
\scantokens\expandafter{\pdfunescapehex\expandafter{\amecsncsn}}
実質的にこれが「最後の文」となります。\scantokens(e-TeX拡張のプリミティブ)と\pdfunescapehexという見慣れないプリミティブが使われています。\scantokensの直後にある\expandafterはその2つ先にある\pdfunescapehexを展開するので、⑦は以下の形のコードに帰着されます13。
\scantokens{【“\pdfunescapehex\expandafter{\amecsncsn}”の1回展開】}
\pdfunescapehex\expandafter{\amecsncsn}を展開しようとすると、まずその中の\expandafterを展開する14ことになり、結局\expandafterの2つ先にある\amecsncsnが展開されます。\amecsncsnの内容は前掲の16進数字列でした。
【“\pdfunescapehex\expandafter{\amecsncsn}”の1回展開】
‖
【“\pdfunescapehex{【“\amecsncsn”の1回展開】}”の1回展開】
‖
【“\pdfunescapehex{5c63617463……205c627965␣}”の1回展開】
引数が確定したので、いよいよ問題のpdfteX拡張プリミティブの\pdfunescapehexが実際に展開されます。このプリミティブの仕様は以下の通りです。
-
\pdfunescapehex{<16進数字列>}: 引数の16進数字列を「バイト列の各バイトの値を16進2桁で表記したものを並べたもの」と解釈して、元のバイト列(の \the-文字列15)に変換する。16進数字(0~9、A~F、a~f)以外の文字は無視される。
つまり、引数の16進文字列16は次のような文字列に変換されます。なんと、別のTeX言語コードが出てきました![]()
\catcode`\v11\catcode`\w11 \nopagenumbers\font\fA=ipxm-r-u26\font\fB=ipxm-r-u51\font\fC=ipxm-r-u6c\font\fK=ipxm-r-u30\fK\baselineskip=12ptplus2pt\hsize=40em\newif\ifC\newcount\iS\newcount\iR\noindent\Ctrue\loop\ifC\iR=\pdfuniformdeviate2\relax\ifodd\iR\char201\char201 \else\char185\char179 \fi\hskip0pt \multiply\iS2\advance\iS\iR\ifnum\iS<"1000 \else\advance\iS-"1000 \fi\ifnum\iS="888 \Cfalse\fi\repeat\char233\char214\hskip0pt \fC\char232\fB\char101\hskip0pt \fA\char97 \bye
とりあえず先に進みましょう。\pdfunescapehexの展開結果が上記のTeX言語コードらしき文字列なので、以下のコードを実行することになります。
\scantokens{\catcode`\v11\catcode`\w11……\bye}
\scantokensはe-TeX拡張のプリミティブで、その仕様は以下の通りです。
-
\scantokens{<トークン列>}: 引数のトークン列を脱トークン化して、改めて字句解析対象となる文字列として読み込む。トークン列を一旦(仮想的な)ファイルに書き出してそれをすぐに読み込んだのと同じになる。
つまり、\scantokensは「ソースコードの字句解析をやり直す」という作用をもっているわけです。例えば、\scantokensの引数の文字列は\catcodeから始まっていますが、これは(\pdfunescapehexの仕様のため)カテゴリコード12の文字トークンの列であって制御綴の\catcodeではありません。ところがこれを\scantokensで処理すると「字句解析が(現在のカテゴリコード設定に従って)やり直される」ため、制御綴の\catcodeに変化します。結局、\scantokensの実行により、前掲の「TeX言語コードらしき文字列」が実際にTeX言語のコードとして実行されることになります。
さてそのコードですが、最後に\byeと書かれています。\byeはplain TeXにおいて文書の終了を表す命令(LaTeXの\end{document}に相当する)です。つまり前掲のコードを実行すると、その最後で\byeが実行されてpdfTeXの実行はそこで終了します。
というわけで、チョット読みにくいddsk.texの解析もこれでオシマイです。その内容は「中に埋め込まれた別のTeX言語のプログラムをデコードして実行する」というものでした。
実際には、最後の\scantokensの文の後にもまだコードが少し残っています。
\ame on-cendcsn
しかし\byeの実行によりpdfTeXの実行は既に終了しているため、この部分は実行されません。このダミーの部分は元のddsk.texの末尾の部分をラブ注入♡にするために敢えて追加されているものでした。
以上で一件落着。おつかれさまでした![]()
まだ一件落着していない気がする話
ここで記事を終えたいところですが、そうすると、
「アレレ、“中に埋め込まれた”コードの方の解説は?
」
と思った人もいるでしょう。
\catcode`\v11\catcode`\w11 \nopagenumbers\font\fA=ipxm-r-u26\font\fB=ipxm-r-u51\font\fC=ipxm-r-u6c\font\fK=ipxm-r-u30\fK\baselineskip=12ptplus2pt\hsize=40em\newif\ifC\newcount\iS\newcount\iR\noindent\Ctrue\loop\ifC\iR=\pdfuniformdeviate2\relax\ifodd\iR\char201\char201 \else\char185\char179 \fi\hskip0pt \multiply\iS2\advance\iS\iR\ifnum\iS<"1000 \else\advance\iS-"1000 \fi\ifnum\iS="888 \Cfalse\fi\repeat\char233\char214\hskip0pt \fC\char232\fB\char101\hskip0pt \fA\char97 \bye
でもこちらは別に読みにくいものではありません。少し読みやすく整形してコメントを付けたコードを載せておきます。
% "本体"部でのcatcode変更を取り消す
\catcode`\v=11 \catcode`\w=11
% レイアウト設定
\nopagenumbers % ノンブル無し
\font\fA=ipxm-r-u26 % 和文サブフォント宣言(U+26xx)
\font\fB=ipxm-r-u51 % 〃 (U+51xx)
\font\fC=ipxm-r-u6c % 〃 (U+6Cxx)
\font\fK=ipxm-r-u30 % 〃 (U+30xx; 仮名用)
\fK % 仮名用フォントを選択
\baselineskip=12pt plus 2pt % 行送り
\hsize=40em % 行長(40文字)
\newif\ifC % ループ用のスイッチ
\newcount\iS % 過去の状態
\newcount\iR
\noindent
\Ctrue \loop \ifC % 無限ループ
\iR=\pdfuniformdeviate2\relax % 0~1の乱数を取得
\ifodd\iR % 1なら
\char201\char201 % "ドド"出力
\else % 0なら
\char185\char179 % "スコ"出力
\fi
\hskip 0pt % 行分割可能にする
%↓ iS := (iS×2 + iR) mod 1000h
\multiply \iS by 2
\advance \iS by \iR
\ifnum\iS<"1000
\else
\advance \iS by -"1000
\fi
\ifnum\iS="888 % "ドドスコスコスコ"×3
\Cfalse % ループ継続停止
\fi
\repeat
\char233\char214 \hskip0pt % "ラブ"出力
\fC\char232 \fB\char101 \hskip0pt % "注入"出力
\fA\char97 % "♡"出力
% おしまい
\bye
この通り、ホントにホントにフツーのTeX言語コードです。ここまでddsk.texの読みにくいコードを解読してきた皆さんなら容易に理解できますね![]()
いくつかの点を補足しておきます。
- 本体で行った
vとwのカテゴリコードの変更はまだ有効であり、これによる問題を防ぐために冒頭でカテゴリコードを元に戻している。なお、lccodeの変更もまだ有効であるが、これによる影響は考えられないのでこちらは対処していない。 - pdfTeXで日本語文字を出力するために、CJKパッケージ等で使われる “Unicodeサブフォント” を利用している。具体的には、IPAフォントの字形を持つipaex-type1パッケージのフォント群を利用している。
- 例えば、“ド”(U+30C9)はフォント
ipxm-r-u30の文字コードC9h(=201)にあるため、\font\fK=ipxm-r-u30でフォント定義した後に\fK\char201を実行することで “ド” を出力できる。 - pdfTeXでは文字間が自動的に行分割可能になるわけではないので、行分割可能にしたい箇所で幅ゼロのグルーを入れている。
- 例えば、“ド”(U+30C9)はフォント
-
\pdfuniformdeviateは擬似乱数を発生するpdfTeX拡張17プリミティブ(展開可能)で、\pdfuniformdeviate<整数n>は0以上n未満の一様ランダムな整数値に展開される。 - 「ドドスコ問題」を解くロジックの実装は、古のズンドコキヨシの時代から知られた「整数値をビット配列と見なす方法」を使っている。
まとめ
「チョット読みにくいコード」は作るのも読むのもトッテモ楽しいものです
「せっかくTeX言語を学習したのだから何か楽しいことがしたい
」と考えている方は、ぜひとも、TeX言語で「チョット読みにくいコード」を作ってみましょう!![]()
蛇足してみる
2017年のアドベントカレンダーのフィナーレに出したチョット読みにくいコードは本記事で紹介した原理(TCX)が使用されています。ぜひ解読してみてください!![]()
-
つまり、TeXの内部で使われているコードは常にASCII互換(7ビット部分がASCII)であるわけです。例えば、
\ifnum`A=65というTeX言語の条件文は、たとえEBCDIC系のシステムであっても真と判定されます。なお、XeTeXやLuaTeXでは代替となる機能があるためTCXの機能は削除されています。 ↩ -
制御語(control word)とは「名前が英字からなるcontrol sequence」のことを指します。また、この記事では “control sequence” に対する訳語として「制御綴*」を用います。 ↩
-
後の
\csnameはlet文の右辺なので展開されません。うっかり「対応する\endcsnameの探索」をしないように注意しましょう。 ↩ -
「なぜそんな変な名前の制御綴を多用しているのか」については、賢明な読者に対する演習問題としておきます
↩ -
もちろん「文がどこまで続くか」も本来は解読して初めて判明することです。 ↩
-
\<₉expandafter\>₁₀の1回展開で生じた\expandafterはそのままlet文の右辺になるため展開されません。このことを見抜くためには複雑な展開過程を精密に追う力が必要です。 ↩ -
`hはhの文字コード(=104)を表すので、\lccode`hは「文字hのlccode」のパラメタを意味します。 ↩ -
厳密にいうと、
`hという“数字表記”の整数の直後に空白を入れずに\amecsncsnがあるため、「整数を終結させるために\amecsncsnが完全展開される」ことになります。「\romannumeral トリック」を知っている人なら聞いたことがある話でしょう。 ↩ -
\のカテゴリコードが12である(つまり\だけで文字トークンとなる)ため、`\\と書かなくても`\だけで整数の表記として通用します。 ↩ -
ちなみに、
vも文字トークンなので小文字変換が適用されます(結局変化しませんが)。ただし、もし仮にこれで文字が変わったとしても、カテゴリコードが変化していないので、結局{の役割を果たすことは変わりません。 ↩ -
正確に言うと「カテゴリコードが10で文字コードが32の文字トークン」です。 ↩
-
\gdefではなく\xdefなので引数は完全展開されますが、引数には展開不能な文字トークンしかないので実際には何も変化しません。 ↩ -
\scantokensは「{}で囲んだ引数をとる」という構文規則なのですが、こういう構文をもつプリミティブ(他には例えば\everyparが該当します)は「実行されても引数開始の{を見るまでは展開抑止にならない」という特徴をもちます。つまり、直後にある\expandafterは\scantokensトークンによる影響を受けずに展開することができます。 ↩ -
\pdfunescapehexも「{}で囲んだ引数をとる」という構文規則をもつプリミティブであるため\scantokensと同じ特徴をもちます。ただし\pdfunescapehexは展開可能プリミティブなので、「\pdfunescapehexを展開する際にはその直後にある\expandafterが展開される」という動作になります。 ↩ -
「\the-文字列」とは「(本来カテゴリーコードに意味がない)純粋な文字列を、空白(文字コード32)はカテゴリコード10、それ以外の文字はカテゴリコード12をもつ文字トークンの列として表したもの」です。TeXのプリミティブが“純粋な文字列を戻り値としたい”場合に実際には「その戻り値の \the-文字列に展開される展開可能プリミティブ」として実装されることがよくあります。その代表的な例が
\theであるためこの名で呼ばれています。 ↩ -
なお、引数の文字列には空白文字も含まれますが、空白文字は16進数字に該当しないので
\pdfunescapehexの仕様に従い無視されます。 ↩ -
ただし現在では主要エンジン全てがこのプリミティブをサポートしています。(XeTeX・LuaTeXでは
\uniformdeviateという名前で定義されています。) ↩
