e-(u)pTeX をいじってみた話

> これは「[TeX ＆ LaTeX Advent Calendar 2021](https://adventar.org/calendars/6724)」の 13 日目の記事です．12 日目は [yukishita さん](https://qiita.com/Mopepe51/items/d7185771a966ee3f8598)，14 日目は [Daiji さん](https://daiji256.github.io/posts/tex-latex/auto-maze/) です．

今年の重点テーマが「TeX言語(とか)しましょう！」なので，ここでは「WEB 言語で e-(u)pTeX をいじってみた」話をします．

:::note alert
TeX Live 2022 の pTeX 系列 (p4.0.0) には\ptexlineendmode という名前の内部整数が追加されますが，本ページ「ネタ2」で説明している \epTeXlineendmode とは全く異なるものです．混同しないでください．
:::

# WEB 言語とは
この記事で扱っている WEB とは，World Wide Web ([Wikipedia](https://ja.wikipedia.org/wiki/World_Wide_Web)) のことでは**ありません**．ここでの WEB は Knuth 教授が提唱している「文芸的プログラミング」の実装であり，

 * Pascal で書かれたプログラムとそれの TeX ソースによる「解説文書」を 1 つのファイル（たとえば `sample.web`）に記述する
 * `sample.web` を `tangle` というプログラムで処理することで Pascal ソース `sample.p` を得る
 * `sample.web` を `weave` というプログラムで処理することで「解説文書」の TeX ソース `sample.tex` を得る

という流れになっています．TeX 自身がこの WEB 言語で記述されています (`tex.web`)．

`tex.web` に対する変更は Knuth 教授しか行えず，機能拡張や実装依存の部分は WEB 言語の change file という形式でパッチされることになっています．たとえば，TeX Live 2021 における e-pTeX バイナリは，`tex.web` と 25 個程度の change file（そして C 言語による各種ライブラリ）から作られています．

:::note
e-TeX, (e-)(u)pTeX, Omega, Aleph は 以上で述べたように tex.web に大量の change file を積み重ねる方式をとっていますが，pdfTeX, XeTeX はそれぞれ pdftex.web, xetex.web からスタートしています．また，LuaTeX は WEB ではなく，C 言語で記述されています．
:::

# ネタ 1：LuaTeX の `\suppresslongerror` プリミティブ等を実装してみた
これは，LuaTeX に実装されている，各種エラー抑止のためのプリミティブ（いずれも内部整数）

> `\suppresslongerror`, `\suppressoutererror`, `\suppressmathparerror`, `\suppressfontnotfounderror`, `\suppressifcsnameerror`, `\suppressprimitiveerror`

のうち，前者 3 つのプリミティブを実装した，というものです．

 * `\suppresslongerror`：0 でない場合，`\long` なしマクロの引数に `\par` が含まれた場合のエラーを抑止する
 * `\suppressoutererror`：0 でない場合，`\outer` 付きマクロが禁止されている状況で `\outer` 付きマクロが来た場合のエラーを抑止する
 * `\suppressmathparerror`0 でない場合，数式モード中に `\par` が来た場合のエラーを抑止する

初期値はいずれも 0（つまり従来どおりの動作）です．

実装自体は LuaTeX ソースの該当部分の変更を e-(u)pTeX 用に書き直すだけなので単純です．tex-jp-build リポジトリの [個人フォークの suppress_errors ブランチ](https://github.com/h-kitagawa/tex-jp-build/tree/suppress_errors) におきました．

:::note warn
ここで追加した 3 つのプリミティブは「エラーが出なくなる」という意味では便利なものですが，「早期に書き忘れ・書き間違いを防止する」目的で決められた TeX の仕様を破るものです．安易に次の 3 行をプリアンブルに書くのはやめたほうが良いでしょう．
<code>
\suppresslongerror=1
\suppressoutererror=1
\suppressmathparerror=1
</code>
:::

:::note info
このネタは TeX Live 開発版に <a href="https://www.tug.org/svn/texlive?view=revision&revision=61239">r61239</a> (2021-12-07) として入りました．TeX Live 2022 の e-(u)pTeX バイナリでは利用できることになるでしょう．
:::

<details><summary>他の 3 つのプリミティブを見送った理由は……</summary><div>

 * `\suppressfontnotfounderror`：フォント (tfm) が見つからない場合，どっちみち外部プログラム `mktextfm`が走ることには変わりがないため，必要性が薄い
 * `\suppressifcsnameerror`：実装（とくにたくさんの change file 間の同期）が面倒そう
 * `\suppressprimitiveerror`：e-(u)pTeX, pdfTeX, XeTeX では必要性はない（`\(pdf)primitive` でエラーが出るのは LuaTeX だけ）

</div></details>

## `\suppresslongerror` について
このプリミティブは LuaTeX [beta-0.36.0](https://tug.org/pipermail/luatex/2009-March/000465.html) で導入されたものですが，`\long` についてここで説明しておきます．

### `\long` の意味
TeX において，標準ではマクロの引数内に `\par` や空行を含んでいてはならない，ということはよく知られています．例を用いて説明します．

``` TeX
% plain TeX
\def\hoge#1{#1}
\hoge{a\par b}
\bye
```
2 行目で定義された`\hoge` は引数を 1 つとるマクロですが，**`\long`なしで定義されているため** 引数に `\par` が含まれることは許容されていません．しかし 3 行目の `\hoge` の引数には `a\par b` と明示的な  `\par` が含まれているので，

```
! Paragraph ended before \hoge was complete.
<to be read again>
                   \par
l.3 \hoge{a\par
                b}
?
```

とエラーが発生します．

また，TeX ソース中の空行は改段落を引き起こしますが，これは「空行があると，`\par` が自動的に挿入される」という TeX の規則によるものです．したがって，

```
\hoge{a

b}
```

も同様のエラーを引き起こします．

:::note info
このように「標準ではマクロの引数内に \par や空行を含んでいてはならない」という仕様は，引数を閉じる } の書き忘れを早期に検出するためのものと言えます．
:::

マクロ定義の際に「このマクロでは引数に `\par` が含まれても良い」ことを明示するフラグが `\long` です．たとえば

```TeX
\long\def\hoge#1{#1}
```

と `\long` 付きでマクロを定義すると，

```
\hoge{a\par b}
\hoge{a

b}
```

のどちらでもエラーは起こりません．

<details><summary>……とここまで書いてきましたが，[r60054](https://www.tug.org/svn/texlive?view=revision&revision=60054) (2021-07-25) で状況が変わりました．</summary><div>
[r60054](https://www.tug.org/svn/texlive?view=revision&revision=60054) では `\partokenname`, `\partokencontext` という 2 つのプリミティブが pdfTeX, XeTeX, (e-)(u)pTeX に追加されました．
`\partokenname` は上の説明で述べた `\par` の「空行（等）で自動挿入される」「`\long` なしマクロでの引数内で禁止される」という特殊な役割を別の名称の命令へと置き換えるものです．以下に例を示します．

```TeX
\partokenname\MYPAR
\def\MYPAR{!!\par}
\def\hoge#1{(#1)}
a

b % 空行によって \par ではなく \MYPAR が挿入される
\hoge{x\par y} % ==> no error
\hoge{x\MYPAR y} % ==> error "! Paragraph ended before"
```

`\partokencontext` は，`\par`（あるいは`\partokenname` で別のものに置き換わっているかも）が自動挿入される場所を制御します．
</div>
</details>

### LaTeX での現象
LaTeX の世界に話を移します．たとえば次の LaTeX ソースを考えます．

``` LaTeX:lipsum.tex
\documentclass{article}
\usepackage{lipsum} % \lipsum 命令のため
\begin{document}
ABC%
\smash{\rlap{\footnotesize\sffamily
  \begin{minipage}[t]{30em}
    \lipsum[1]

    \lipsum[2]
  \end{minipage}}}%
\lipsum[3]

\end{document}
```

「`\smash{\rlap{}}` の中に minipage 環境を入れる」という記述がどうなのかという点は無視して，まあ書きそうなソースです．minipage が専有するスペースを無視して本文が続く，以下のような出力が期待されます：
![lipsum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61603/94591a62-50f2-e1ac-8c73-20d627279692.png)

しかし，`lipsum.tex` をタイプセットすると，以下のエラーが出て止まってしまいます：

```
Runaway argument?
{\rlap {\footnotesize \sffamily \begin {minipage}[t]{30em} \lipsum [1\ETC.
! Paragraph ended before \makesm@sh was complete.
<to be read again>
\par
l.8

?
```

ひとことでいえば，これは `\smash` が `\long` 付きで定義されていないためです．
<details><summary>この文はごまかしがあります．正確に言うと……</summary><div>
`\smash` は実行箇所が数式モードか否かで処理を切り替えるだけで，その本体は `\makesm@sh` です．以下の latex.ltx の抜粋から，`\makesm@sh` が `\long` なしで定義されていることがわかります：

``` TeX:latex.ltx
\DeclareRobustCommand\smash{%
  \relax % \relax, in case this comes first in \halign
  \ifmmode
    \expandafter\mathpalette\expandafter\mathsm@sh
  \else
    \expandafter\makesm@sh
  \fi}
\def\makesm@sh#1{%
  \setbox\z@\hbox{\color@begingroup#1\color@endgroup}\finsm@sh}
\def\mathsm@sh#1#2{%
  \setbox\z@\hbox{$\m@th#1{#2}$}\finsm@sh}
\def\finsm@sh{\ht\z@\z@ \dp\z@\z@ \leavevmode@ifvmode\box\z@}
```
</div></details>

----
なお，zr_tex8r  さんの記事「[完全攻略！ LaTeX のマクロ定義](https://qiita.com/zr_tex8r/items/5067307890d36c0e4882)」で述べられているように，`\newcommand`（や `\renewcommand`）で定義されたマクロは，標準では引数に `\par` を許容（つまり `\long` 付きで定義）します．アスタリスク付きの`\newcommand*`（や `\renewcommand*`）で定義した場合は `\par` を許容しなくなります．

:::note warn
もともと \par は TeX のプリミティブです．しかし LaTeX2e 2021-06-01 以降では段落頭・終端でのフックを実現するため，\par, \endgraf はプリミティブではなくなりました．
::::

### 伝統的な回避法
`\long` なしのマクロが引数中に禁止しているのは **`\par` という名前**の制御綴です．したがって，`\par`の中身を別の制御綴に `\let` してあげればエラーは起きないことになります．plain TeX や LaTeX では `\par` の別名として既に `\endgraf` が定義されているので，これを使えば良いことになります．

``` LaTeX
\documentclass{article}
\usepackage{lipsum}
\begin{document}
ABC%
\smash{\rlap{\footnotesize\sffamily
  \begin{minipage}[t]{30em}
    \lipsum[1]
    \endgraf
    \lipsum[2]
  \end{minipage}}}%
\lipsum[3]

\end{document}
```

### `\suppresslongerror` を使った回避法
`\suppresslongerror` を使うと，次のように書くことが出来ます．

``` LaTeX:lipsum3.tex
\documentclass{article}
\usepackage{lipsum} % \lipsum 命令のため
\begin{document}
ABC%
{\suppresslongerror=1
\smash{\rlap{\footnotesize\sffamily
  \begin{minipage}[t]{30em}
    \lipsum[1]

    \lipsum[2]
  \end{minipage}}}}%
\lipsum[3]

\end{document}
```

原理的には `\suppresslongerror=1` をプリアンブルに書いてしまってもよいのですが，それだと「引数を閉じる `}` の書き忘れ」が起こった場合にファイル終端でようやくエラーが出されることになります．
グルーピングしておけば，影響を最小限にとどめることができます．

:::note warn
 ! Paragraph ended before ... のエラーは \smash の引数を取得する部分で発生しているので，\smash{\suppresslongerror=1 \rlap{... と記述するのは効果がありません．
:::

## `\suppressoutererror` について
「`\long` なし」のようにマクロに関する制限として，もう一つあるのが `\outer` です．
`\outer` 付きで定義されたマクロは，他のマクロの内部や条件文 `\if... \else...\fi` などで使えないという制限があります．例を以下に示します．

```TeX
% plain TeX
\outer\def\F{hoge}
\def\G{piyo\F piyo}       % ==> error
\immediate\write-1{\F}    % ==> error
\message{\F}              % ==> error
\iftrue a\F \else b\F \fi % ==> error
\halign{#\F&#\cr a&d\cr}  % ==> error
```

2 行目で `\outer` 付きでマクロ `\F` が定義されていますが，3 行目以降の各行で `! Forbidden control sequence found while scanning ...` エラーが発生します．

`\suppressoutererror` が 0 でない場合は，この `! Forbidden control sequence found while scanning ...`エラーがどれも出ないようになります．

:::note
LaTeX 本体や LaTeX 用の各種パッケージでは，\outer 付きマクロはほとんど使われません．ただ，まったくないわけではありません（bm パッケージや fancyvrb パッケージなど）．
:::

## `\suppressmathparerror` について
数式モード中に `\par`（もちろん，`\par` を生み出す「空行」も）を使うことは出来ません．

```
$ x+1 =

ab$
```

という入力からは，空行由来の `\par` から `! Missing $ inserted.` というエラーが発生します．

この仕様も「`$` の書き忘れを早期に検出する」という意味はありますが，次のように「何を書こうか迷ってとりあえず空行のまま放置した」場合にエラーが発生して嫌な気分になったことはあるかもしれません．

```LaTeX
\documentclass{article}
\begin{document}
\[ 
 \ frac 1n \sum_{k=0}^{n-1} \left(\frac{k}{n}\right)^2=\int_0^1 x^2\,dx

\]% 何を書こうか迷っている
\end{document}
```

このような場合，`\suppressmathparerror` を 0 以外の値にすると，`! Missing $ inserted.` エラーが発生せずにタイプセットすることができます．

:::note warn
amsmath パッケージの提供する各種環境（align 環境など）では，\suppressmathparerror, \suppresslongerror の両方に非ゼロの値を設定しないとエラーは消えません．
:::

# ネタ 2：「次の行が和文文字から始まる」場合に行末由来のスペースを挿入しない
こちらが本来のネタ（TeX Live に取り込んでもらう気はない，という意味で）です．

:::note alert
冒頭でも注意しましたが，ここで述べている話題は TeX Live 2022 の pTeX 系列 (p4.0.0) に追加される内部整数 \ptexlineendmode とは全く異なるものです．混同しないでください．
:::


きちんと追ったわけではありませんが，[CSS における議論](https://github.com/w3c/csswg-drafts/issues/5086) も関連しそうです．

## 背景
(p)TeX における行末（改行文字）の扱いでは，次の点が有名です：

 * 欧文文字直後の改行では空白文字を発生させる
 * 和文文字直後の改行は何も発生させない

<details><summary>詳細</summary><div>
TeX の入力プロセッサは，「どのカテゴリーコードの文字が来たか」で状態が遷移する有限オートマトンとしてとらえることが出来ます．詳細はここでは説明しません（[ptex-manual](https://github.com/texjporg/ptex-manual) などを参照してください）が，上の 2 点に補足することとして，

* コントロールワードや「`\ `」の直後の改行は何も発生しない
* 「`\ `」以外のコントロールシンボルの直後の改行は空白文字を発生させる
* 和文文字→グループ境界 (`{`. `}`) が有限個という状態で行が終わった場合は，何も発生しない

があります．個人的には最後の点はやや不思議ですが，
</div></summary>

----
このルールに従うと，例えば

```LaTeX
この
alphabet
は
```

と入力した場合，

 * 1 行目は和文文字「の」で終わっているので，空白は発生しない
 * 2 行目は欧文文字「t」で終わっているので，空白が発生する

ということになり，結局次の入力と同じことになります：

```LaTeX
このalphabet は
```

さて，「の」「a」の間には標準の和欧文間空白 (`\xkanjiskip`) が，「t」「は」の間には欧文空白が挿入されることになります．pLaTeX の標準では前者は `2.40553pt plus 1.0pt minus 1.0pt`，後者は `3.33333pt plus 1.66666pt minus 1.11111pt` (cmr10) ですから，結果的に**「alphabet」の前後で入る空白量が異なる**ということになります．

前後の不統一が発生するのは，上で説明したように行末の扱いがそれより前にのみ依存するからです．
では，「先読みを行って，行末の前後の文字からその行末の扱いを決定できないか」というのが今回のネタになります．

## 作ってみた
例によって tex-jp-build リポジトリの [個人フォークの kitagawa_lineend_1 ブランチ](https://github.com/h-kitagawa/tex-jp-build/tree/kitagawa_lineend_1) におきました．

行末由来の空白をどういう場合に挿入するか制御する `\epTeXlineendmode` という内部整数を追加しています．この値によって

| 値 | 欧文→欧文 | 欧文→和文 | 和文→欧文 | 和文→和文 | 先読み | 備考 |
|:----|:----|:----|:----|:----|:----|:---|
| 0 | 空白 | 空白 | なし | なし | なし | pTeX 標準 |
| 1 | 空白 | なし | なし | なし | あり| |
| 2 | 空白 | 空白 | 空白 | 空白 | なし | |
| 3 | 空白 | 空白 | 空白 | なし | あり | |

と変わります．

 * ファイル終端では，「欧文文字が続く」ものとして扱います．
 * 端末からの入力の場合，「次の行の入力」があるかどうかはわからないので，先読み処理をせずに「次の行は欧文文字から始まる」と想定します．
 * 先読み処理は単純に「空白以外で先頭にくる文字が和文文字か否か」のみで行っています．行の先頭が catcode 9 (ignore) とか ^^ 記法だった場合の考慮はしていません．

### 実装方針
TeX では，「次のトークンを取得する」処理は `get_next` プロシージャにまとまって記述されています．その中の「外部ファイルから読む」部分の処理は，大雑把に言って

```Pascal
switch: if （現在の行が終了していない） then
  begin （現在位置の文字を取得，cur_cmd をその文字の catcode にセット）
  （cur_cmd の値と入力プロセッサの内部状態に合わせて分岐）
  end 
else begin
  （次の行に移動する準備）
  goto switch;
end;
```

のようになっています．後半の else 節では次の行のトークンを取得する準備だけをしている段階であることに注目すると，「先読みをして行末由来の空白を挿入するか調べる」ことを示す変数 `pending_space` を準備して

```Pascal
switch: if （現在の行が終了していない） then
  begin （現在位置の文字を取得，cur_cmd をその文字の catcode にセット）
  （cur_cmd の値と入力プロセッサの内部状態に合わせて分岐．
    \epTeXlineendmode の値などに応じて pending_space を設定）
  end 
else begin
  （次の行に移動する準備）
  if pending_space then begin
    pending_space:=false;
    （catcode が 10 でないような最初の文字が欧文文字なら，空白文字を戻り値にする．
    和文文字なら，goto switch）
  end;
  goto switch;
end;
```

のようにすれば一応の形になるというわけです．

:::note
行頭にある catcode 10 の文字（空白の役割）たちは無視されるので，この先読み部分でも無視しています．
:::


# おわりに
WEB 言語しましょう！