TeX tuneup 2021: 7年ぶりの TeX アップデート

2021-04-03  #TeX 

昨日 TeX Live 2021 がリリースされました.このリリースには例年通りさまざまな TeX 関連プロダクトの新しいバージョンが含まれていますが,今年は実に7年ぶりに Knuth によるオリジナルの TeX 処理系もバージョンアップしました.もちろん TeX は既に開発終了が宣言されており,今回も大きな変更が入ることはありませんでしたが,いくつかのマイナーなバグ修正が行われました.実用的な TeX ユーザにはほとんど影響のないところではありますが,TeX 言語者としてはその更新内容はとても興味深いものであるので,本稿ではその修正内容について語ってみようと思います.

背景

Knuth によって開発されたオリジナルの TeX 処理系 (Knuthian TeX) は,バージョン3になった時点で既にその開発が概ね終了したと宣言されており,以降は原則としてバグ修正以外は行われないことになっています.そして,Knuth の遺言により,彼の死とともに TeX はバージョン π となり,永久にアップデートが行われないことと定められています.

さて,そんな TeX 処理系ですが,近年もマイナーな更新(主にバグ修正)は断続的に行われており,最近の更新年を確認すると次のようになっています:

  • 1993, 1994, 1996, 1999, 2003, 2008, 2014

これは見ての通り,階差数列が等差数列 $a_n = n$ となる数列です.Knuth は今後もこの規則に基づいて更新を行うことを自らのウェブサイト上で宣言しており,前回2014年のアップデートから7年となる今年,本当に2021年のアップデート TeX tuneup 2021 が実施されました.

その更新内容は TUGboat 42:1 の以下の記事で解説されています.

TUGboat の通常記事は次号の出版までは TUG 会員限定アクセスなのですが,この Knuth の記事は早くも一般からアクセスできるようになっています.また,Knuth 本人による記事とは別に StackExchange の “What’s new in TeX, version 3.141592653?” という質問に対する回答(LaTeX チームの Phelype Oleinik 氏によるもの)でも詳しく解説されています.

本稿も,主として上記2つの情報源を参考に執筆しました.

Tuneup の概要

前回2014年の Tuneup で TeX 本体に施された修正がわずか1つ(それもとても軽微なもの)であったのに対し,今回はより多くの修正が行われました.とはいえ,Knuth の強い信念により TeX にはこれ以上「非互換な」変更は入らないことになっていて,もちろん今回もその方針は揺らいでいません.したがって,今回の修正の影響が出るのはあったとしても極めて稀なケースであり,ほとんどの文書の処理・組版結果には一切影響がないようなものとなっています.

TeX のバグ報告は Knuth に直接届く前に Karl Berry 氏をはじめとするエキスパート・チームによって厳しくフィルタリングが行われ,確実に Knuth が見るに値するものだけが彼の手許に届くような体制になっています.そのフィルタを介した上で,前回 Tuneup の際に Knuth が受け取った “不具合の可能性があるものリスト” はおよそ2ダースと少しだったようなのですが,今回はリスト長が250を超えるほどであったと言います.Knuth に渡されるリスト項目の中でも実際の修正につながったものはわずかでしたが,それでも今回は相対的に多くの修正がありました.

具体的に,今回 TeX の “errorlog”(後述)に掲載された修正は合わせて10個でした1.このうち Knuth が先述の TUGboat 記事で触れた5つのものは特に重大なもので,Knuth によって Bank of San Serriffe の 0x\$80.00 (\$327.68) 小切手に値すると評価されたようです.以下では,この5つの修正内容について述べていきたいと思います.

修正された主な TeX のバグ

対話モードの不審な挙動

小切手が与えられた5つの主たるバグ2のうち過半数にあたる3つは TeX の対話モードに関するものでした.今どきは TeX を対話的に使用している人は少数派かもしれませんが,それはともかく \batchmode 以外ではエラーが起きた際には次のヘルプメッセージにあるように,TeX に対して対応をユーザが指示することが可能です.

Type <return> to proceed, S to scroll future error messages,
R to run without stopping, Q to run quietly,
I to insert something, E to edit your file,
1 or ... or 9 to ignore the next 1 to 9 tokens of input,
H for help, X to quit.

ここで言う対話モードに関わるバグというのは,こうしたユーザからの「アルファベット1文字の指示」と深く関わりのあるものです.

S949. 対話最中の不正な \batchmode 化(報告者:潇洒张)

最初に紹介する2つはある1人のユーザから StackExchange 上で報告されたものです (q551313, q552113).どうもこの方は,最初から TeX のバグを見つける目的で「デバッグ」をしていたようです.めちゃめちゃ優秀なデバッガですね.

では,いよいよ1つ目のバグを引き起こす不正な入力を見てみましょう.

\catcode`\^=7 \catcode`\^^?=15 \s^^?X
1
Q
v

これを invalid0.in というファイルに保存して,次のように(Tuneup 以前の)iniTeX で実行すると segmentaiton fault が発生していました.

$ tex -ini < invalid0.in
This is TeX, Version 3.14159265 (TeX Live 2020) (INITEX)
**! Undefined control sequence.
<*> \catcode`\^=7 \catcode`\^^?=15 \s
                                     ^^?X
? ! Text line contains an invalid character.
<*> \catcode`\^=7 \catcode`\^^?=15 \s^^?
                                        X
? OK, entering \batchmode
segmentation fault

この不正な入力が「やろうとしていること」を簡単に説明します.まず,invalid.in のうち冒頭の1行は普通に TeX の入力ストリームです.重要なのは末尾の \s^^?X の部分で,各トークンの役割を噛み砕くと次のようになります.

  • \s は未定義のコントロール・シークエンス
  • ^^? は無効文字(カテゴリーコード15の文字)
  • X は普通の文字(ここは X でなくとも任意の入力でよい)

この入力を TeX 処理系に食わせると,もちろんまず \s のところで “! Undefined control sequence.” のエラーが発生します.そこで(対話モードで)1 を入力すると1トークン(ここでは \s)が読み飛ばされて次に無効文字 ^^? が読まれて,再び今度は “! Text line contains an invalid character.” のエラーが発生します.

ここで対話モードから再び Q と畳み掛けます.これは TeX に対する “run quietly” という指示で,要するに以降はエラーで止まることも端末に何かメッセージを表示することもせずに可能な限り処理を続行するようにという要請です.

この一連の特殊な対話入力が TeX の内部状態を予期せぬ形に持っていく(具体的には interaction = error_stop_mode のときにしか実行されるべきでない分岐に interaction = batch_mode の状態で入ってしまう)ようで,特に Web2C 実装の場合は,無効文字 ^^? の後ろに何らかの文字があると開いてもいない \write ストリームに対してその文字の書き込みを試みることになり,システムがクラッシュするというのが顛末のようです.

S950. “E” オプションの不正な受付(報告者:潇洒张)

さて2つ目のバグ挙動も,かなりトリッキーなインタラクションによって引き起こされます.まず \ の1文字だけを含む TeX のソースファイル h.tex を用意します.そして,以下の内容を入力します.

h
I\&v
E

具体的な再現手順としては,再び上記を invalid1.in とでも名前を付けて保存して,tex コマンドに標準入力から流します.

$ tex -ini < invalid1.in
This is TeX, Version 3.14159265 (TeX Live 2020) (INITEX)
**(./h.tex
! Undefined control sequence.
l.1 \

? ! Undefined control sequence.
<insert>   \&
             v
l.1 \

? No pages of output.
Transcript written on h.log.
segmentation fault

では何が起こっているか追ってみます.

まず最初の入力 h によって,ファイル h.tex が読まれます.h.tex には \ 1文字が記入されているわけですが,制御綴 \^^M は(iniTeX では)未定義なので例によって “! Undefined control sequence.” エラーが発生し,ユーザからの指示待ち状態になります.

この状況で I とタイプすることで何か TeX へのトークン列を与えることになりますが,そこで再び \& という未定義の制御綴を入力して,2度目の “! Undefined control sequence.” エラーを発生させ,またユーザからの指示待ち状態にします.この入力待ちに対して,今度は「TeX の処理を終了し,エディタでソースを編集する」という選択肢である E オプションを使用しようとすると,TeX が妙なファイルをエディタで開こうとして segmentation fault が発生するということのようです.

上記は \& の後ろに何らかの文字(この例では v)がないと再現しなかったので,もう少し複雑な条件が必要そうですが,基本的な状況としては I オプションによって「ターミナルからの対話的な入力」を処理している最中なのに,E オプションによって「ファイル編集」を試みることによって,この問題が引き起こされていたというわけです.

I948. \tracingparagraphs の最中にフリーズしてしまう(報告者:Udo Wermuth)

対話モードに関わるものの3つ目は,上の2つとは少し毛色の異なるものです.再現するのはとても簡単で,次のような plain TeX コード(適当に test.tex とでも名前を付けます)を用意します.

\tracingparagraphs=1 A\hss B\end

そして,これを普通に tex コマンドによって処理すると,何のエラーメッセージもなしに TeX がフリーズします.

$ tex test.tex
This is TeX, Version 3.14159265 (TeX Live 2020) (preloaded format=tex)
(./test.tex

なぜこのようなことが起こっていたのかというと,本来であれば上記コードを処理した際に表示されるべき “! Infinite glue shrinkage …” エラーのメッセージが表示されることなく TeX がユーザからの入力受付状態に入ってしまうことが原因です.

基本的に \tracingparagraphs がオン (>0) のときには TeX は(\tracingonline=1 でない限り) log_only な状態になります.エラーが起こった際には,もちろんそのエラーメッセージはログファイルだけでなく,ターミナルにも出力されないと困るわけなのですが, “! Infinite glue shrinkage …” のエラーに関してその出力先の切り替え処理が抜け落ちていたというのが問題だったようです.その結果,TeX は黙ったままユーザの入力受付状態に入ってしまい(見かけ上)フリーズしたように見えていたというわけです.

TeXbook の記述と食い違う TeX 言語の境界ケース

続く2つのバグはインタラクションとは無関係な,純粋に TeX 言語そのものの仕様に関わるものです.したがって上の3つと比べれば,これまで35年間このバグの影響を受けるようなコードが「実際に実行されていた」可能性がありますが,それでもかなり特殊な境界ケースであることには変わりがなく,Knuth は「その可能性はあまりない」と考えているようです.

B952. # 直後の暗黙ブレース(報告者:Udo Wermuth)

\def 類の仕様の中でもマイナーなものだと思いますが,パラメタテキストの末尾がパラメタトークン # である場合,{ がパラメタテキストと置き換えテキストの両方の末尾に挿入されたものとして扱うというルールがあります(参考).この機能は要するに,最後の引数の終端を表すデリミタに,直後のグループ開始文字 { を利用したいというような場合に用いられます.

さて,\def 系の命令の書式は,TeXbook の286ページで次のように定義されています.

  • <definition><def><control sequence><definition text>
  • <def>\def | \gdef | \edef | \xdef
  • <definition text><parameter text><left brace><balanced text><right brace>

ここで最終項目の <left brace> が問題です.TeXbook の記述でトリッキーな部分の1つなのですが,TeX 言語を定義する BNF 記法において { と出てきたときには暗黙的・明示的いずれの「グループ開始文字」でも構わないのですが,<left brace> という表記の場合は必ず明示的である必要があります.

したがって,いわゆる置換テキスト <left brace><balanced text><right brace> の両端は必ず明示的な {} である必要があって,暗黙的なもの,例えば \bgroup\egroup であることは上記の記述から明確に禁止されています.

ところが,どうやらいままでは先述したようにパラメタテキストの末尾が # である場合に限って,仕様上 <left brace> であるはずのところが,暗黙的な文字でも認められてしまっていたようです.すなわちこれまでの TeX では以下のコードがエラーなく通っていました.

\def\cs#1#\bgroup hi#1}

ちなみにこれによって定義された \foo というのは \show によって定義内容を確認してみると次のように表示されていました.

> \cs=macro:
#1\bgroup ->hi#1\bgroup .

これはつまり \bgroup{ であるときとまったく同等に振る舞っていたということです.極めて特殊なケースではあるものの,これは確かに TeXbook の記述と明確に異なる挙動であるとして,上記のようなコードは禁止されました.新しいバージョンの TeX で上記のコードを読ませると “! Parameters must be numbered consecutively.” のエラーが出るようになったようです.

R953. 9つ目の引数の後の不正なトークン(報告者:Bruno Le Floch)

そして5つ目は「TeX の最後のバグ」となる可能性のあるものです(と Knuth が言っています).ご存知の通り,TeX のマクロは #1 から #9 まで最大9つの引数を取ることができます.パラメタテキストで許容される最後のパラメタトークンである #9 を記述した後には,もちろんそれ以上 # を記述することはできないので,もしそのような # を書くと “! You already have nine parameters.” というエラーが出ます.ここで仮に H オプションをタイプしてヘルプメッセージを出すと次のように言われます.

I’m going to ignore the # sign you just used.

このメッセージの内容自体は正しい,つまり必要以上に「正確に」実際の TeX 処理系の挙動を表しているのですが,結果的に本来ではあり得るはずのない「新しい」真実を作り出してしまいます.というのも,このエラーを出した後の TeX 処理系は # のみならず,その後続の文字も,それが仮に不正なものであっても,無視してしまうという挙動を示していました.

例えば,次のように9個のパラメタを指定したあとに10個目のパラメタ #0 を置こうとしてみます.

\def\bar#1#2#3#4#5#6#7#8#9#0{}

すると,TeX 処理系は既に書いたように “! You already have nine parameters.” エラーを発出します.そして,言葉通り #9 直後の # を無視するのですが,その後の 0 を残したままにしてしまいます.その結果,ここで定義した \bar の定義を \show で確認すると,次のような表示が返ってきます.

> \bar=macro:
#1#2#3#4#5#6#7#8#90->.

このぐらいであればまだいいのですが,# の直後が本当にどんな文字でも入力として受け付けられてしまうのがこのバグのすごいところで,この挙動を利用するとさらにとんでもないこともできてしまいます.

例えば #0 の代わりに ## として次のような定義を行っても TeX に受け付けられていました.

\def\baz#1#2#3#4#5#6#7#8#9##{}

その上で \baz の定義を \show すると……

> \baz=macro:
#1#2#3#4#5#6#7#8#9##->.

なんと # が最後の引数のデリミタになってしまっています.そのため

\baz12345678hello#

のように \baz を使用すると,あろうことか hello の部分が #9 になってしまいます.

さらには,グループ終了の } をエラーなしに TeX に対して「マクロの引数として」認識させるという曲芸まで可能だったようです.以下は,大元のバグ報告にあったコード例らしいのですが……

\def\foo#1#2#3#4#5#6#7#8#9#}##{\show#9}
\show\foo
\foo12345678} }#
\end %        ^^ delimiter

これを実行すると,もちろん “! You already have nine parameters.” のエラーは起きますが,それを無視すると2つの \show の結果が見られます.まず \foo の定義を確認すると

> \foo=macro:
#1#2#3#4#5#6#7#8#9}##->\show #9.

というようになっており,}# が最後の引数のデリミタになっています.ここでポイントとなるのは } がデリミタの1文字目になっており,そのため TeX が9つ目の引数を読み取っている際は,この「デリミタ開始」の } を探している状態になります.その結果 \foo12345678} }# の1つ目の } を読んだ際にも “! Argument of \foo has an extra }” のエラーを出すことなく,そのまま「引数として」受け付けてしまいます.

最終的に \show#9 の部分の結果として次が表示されることになります.

> end-group character }.
<argument> }

はい,かなりとんでもないですね.

TeX Tuneup 2021 による修正により “! You already have nine parameters.” エラーに続くヘルプメッセージが少し長くなりました.

! You already have nine parameters.
l.1 \def\foo#1#2#3#4#5#6#7#8#9#}
                                ##{\show#9}
? h
I'm going to ignore the # sign you just used,
as well as the token that followed it.

追加されたのはもちろん “as well as the token that followed it” の部分で,実際の挙動もその通り # のみならずその直後の不可解なトークンも一緒に無視されるようになりました.

その他の細かな修正

本稿では小切手発行の対象となった主なバグ5つの解説を行いましたが,今回の Tuneup では他にもいくつかの細かなバグ修正が入っています.そうした残りの修正内容はすべて TeX の “errorlog” で確認することができます.これはもちろん TeX Live に含まれていて,例えば次のようにすると閲覧することができます3

$ texdoc errorlog

これは今回の修正内容のみならず,1978年以来の TeX の変更履歴の詳細を Knuth 自身が記録したものです.1989年に発表された以下の論文において,TeX というプログラムの開発の様子の分析とともにその読み方が解説されています.

この errorlog にある各修正内容は,アルファベット1文字で識別される15のカテゴリに分類されているのですが,そのカテゴリについても上記の論文で説明されています.本稿の各バグ解説の見出しに付していた “S949” のような文字列は,実は errorlog におけるナンバリング(カテゴリ+通し番号)です.本稿に登場したカテゴリについてだけ,簡単にまとめておくと次のようになります.

  • B (blunder or botch): うっかりミスの類4
  • I (interactive improvement): 対話モードの改善に関わるもの
  • R (reinforcement of robustness): システムの堅牢性に関わるもの
  • S (surprising scenario): Knuth がオリジナルの考えを改めねばならぬほど性質の悪いバグ5

次回 Tuneup は2029年

これまでと同様の規則にしたがって,次回 Tuneup は2029年に行われると予告されています.Tuneup の実施自体は2029年ですが,今年の流れを見る限り,エキスパート・チームにより厳選されたバグレポートが Knuth の手許に送られるのは前年の年末ごろになると思われるので,次の Tuneup に間に合わせるには遅くとも2028年の秋頃までにはレポートを送っておく必要があります.そして,具体的な TeX のバグ報告の手順は TUG ウェブサイトの下記ページに詳しく書いてあります:

というわけで,Knuth 小切手を狙う方は2028年末までのバグ報告を目指して頑張りましょう?!


  1. その他に errorlog に載らなかった軽微な修正が2つあるそうです. ↩︎

  2. 本稿の末尾で解説しますが,errorlog のカテゴリのうち A, B, D, F, L, M, R, S, T に属するものは「バグ」である一方で,残りの C, E, G, I, P, Q のものは「拡張」に相当するそうなので,厳密には4つのバグと1つの拡張と言うべきかもしれません. ↩︎

  3. ソースの errorlog.tex は TeX Live 2021 から含まれるようになったようです.こちらは例えば kpsewhich --format=doc errorlog.tex などとすると簡単に見つけられます. ↩︎

  4. Knuth 曰く「大局に思いを馳せるあまり,細かいことを考える脳のリソースが残っていなかった」ケースに該当するそうです. ↩︎

  5. ちなみに今回はわかりやすく segmentation fault を起こすバグたちでしたね. ↩︎