TeX tuneup 2021: 7年ぶりの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の以下の記事で解説されています。
- Donald E. Knuth, “The TeX tuneup of 2021,” TUGboat 42:1 (2021), pp. 901–904,https://tug.org/TUGboat/tb42-1/tb130knuth-tuneup21.pdf.
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^^?X1Qvこれをinvalid0.inというファイルに保存して、次のように(Tuneup以前の)iniTeXで実行するとsegmentaiton faultが発生していました。
$ tex -ini < invalid0.inThis 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 \batchmodesegmentation 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を用意します。そして、以下の内容を入力します。
hI\&vE具体的な再現手順としては、再び上記をinvalid1.inとでも名前を付けて保存して、texコマンドに標準入力から流します。
$ tex -ini < invalid1.inThis is TeX, Version 3.14159265 (TeX Live 2020) (INITEX)**(./h.tex! Undefined control sequence.l.1 \? ! Undefined control sequence.<insert> \& vl.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.texThis 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}? hI'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というプログラムの開発の様子の分析とともにその読み方が解説されています。
- Donald E. Knuth, “The errors of TeX,” Software: Practice and Experience, Volume 19, Issue 7 (1989), pp. 607–685,https://doi.org/10.1002/spe.4380190702.
この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年末までのバグ報告を目指して頑張りましょう?!
その他にerrorlogに載らなかった軽微な修正が2つあるそうです。 ↩︎
本稿の末尾で解説しますが、errorlogのカテゴリのうちA, B, D, F, L, M, R, S, Tに属するものは「バグ」である一方で、残りのC, E, G, I, P, Qのものは「拡張」に相当するそうなので、厳密には4つのバグと1つの拡張と言うべきかもしれません。 ↩︎
ソースの
errorlog.texはTeX Live 2021から含まれるようになったようです。こちらは例えばkpsewhich --format=doc errorlog.texなどとすると簡単に見つけられます。 ↩︎Knuth曰く「大局に思いを馳せるあまり、細かいことを考える脳のリソースが残っていなかった」ケースに該当するそうです。 ↩︎
ちなみに今回はわかりやすくsegmentation faultを起こすバグたちでしたね。 ↩︎