TeXでもSATySFiでもコンパイル可能なファイルを作りたい話

:::note info
これは「<a href="https://adventar.org/calendars/6724">TeX &amp; LaTeX Advent Caleandar 2021</a>」の7日目の記事です。
（6日目は <a href="https://doratex.hatenablog.jp/entry/20211206/1638749451">doraTeX さん</a>、8日目は <a href="https://qiita.com/zr_tex8r/items/c72f8afa644d567cf8cd">zr_tex8r さん</a>です。）
:::

:::note info
これは「<a href="https://adventar.org/calendars/6336">SATySFi Advent Caleandar 2021</a>」の7日目の記事でもあります。
（8日目は <a href="https://qiita.com/zr_tex8r/items/c72f8afa644d567cf8cd">zr_tex8r さん</a>です。）
:::

プログラミング言語に関するニッチな話題の1つに「**Polyglot**」というものがあります。これは「**単一のソースコードで異なる複数の言語で実行可能なもの**」を指しています[^polyglot-alt]。

[^polyglot-alt]: “Polyglot programming”という語は「単一のソフトウェアを複数の言語を用いて作製すること」を指すこともあります。

* [Polyglot (computing)][wp-polyglot]（Wikipedia）

例えば、以下に挙げるのはPerlとRubyのpolyglotです。

```ruby
print(0?"Ruby\n":"Perl\n")
```

本記事では**TeXとSATySFiのpolyglot**に挑戦してみます。

## LaTeXとSATySFiでpolyglotできない件

ご存じの通り、TeXのフォーマットで圧倒的に有名なのはLaTeXです。そこでまずは、LaTeXフォーマット[^latex-format]の使用を前提にしましょう。

[^latex-format]: さすがに「LaTeX言語の仕様」の範囲内でpolyglotを作るのは明らかに困難なので、ここでは「TeX言語の使用を許可したLaTeXフォーマットのTeX」（いわゆる**“TeX on LaTeX”**）を対象にします。

ここでの最大の問題は「**TeXとSATySFiで行コメント開始文字がともに“%”である**」ということです。polyglotのコードを書く際の常套的に使われるのが「片方の言語のコメントを開始して、もう一方の言語でのみ実行される文脈を用意する」という手段ですが、TeXとSATySFiの組み合わせではこれが通用しないわけです。もちろん、TeXでは「カテゴリコードを変える」などの“裏技”が使えるのですが、それでもまず初めには「通常のカテゴリコードに従ったコード」を書く必要があります。

特にLaTeXフォーマットで厄介なのは、本体開始（`\begin{document}`より前）で“普通の文字”（LaTeXの非特殊文字）を書く（正確には“実行する”）と**エラーになってしまうこと**です。“裏技”に持ち込むためには**事前に**`\catcode`等の制御綴を実行できないといけません。

対して、SATySFiのソースコードはまずプログラムモードで開始され、そこでは“普通の文字”を書く必要があります。TeXの（`\catcode`等の）制御綴と同じ形の文字列になるのはSATySFiではインライン命令ですが、仮にインラインテキストを導入して`\catcode`を書ける状態にどうにか持ち込めたとしても、SATySFiで`\catcode`が**事前に**（プログラムモードで）定義されてなければならず、この要件を満たせないので行き詰ってしまいます。

私が考える限りでは「“LaTeXフォーマットのTeX”とSATySFiのpolyglot」を実現するのは残念ながら無理そうです。

## TeXとSATySFiでpolyglotしてみた件

というわけで、LaTeXフォーマットは諦めて、**plain TeXを使う**ように方針転換しました。幸いなことに、こちらは**“強い裏技”**を使うことで実現できました。以下のコードが「plain TeXとSATySFiのpolyglot」となります。

```text:whatsthis
@require: pervasives
let s =``STARTTEX \newdimen\pwd \newdimen\pht \newdimen\fs
 \pwd=160bp \pht=90bp \fs=36bp
 \ifnum\ifx\pdfoutput\unDef0\else\pdfoutput\fi>0
  \pdfpagewidth=\pwd \pdfpageheight=\pht \let\os\relax
 \else \def\os{\special{papersize=\the\pwd,\the\pht}}\fi
 \shipout\vbox to\pht{\os \font\ff=cmr10 at\fs \vskip-1inplus1fil
  \moveleft1in\hbox to\pwd{\hfil\ff \TeX!!\hfil}\vskip1inplus1fil}
 \output{\setbox0\box255\deadcycles=0}\bye ENDTEX``
let-inline ctx \dm m = inline-nil
let (pwd, pht, fs) = (160pt, 90pt, 36pt)
let ctx = set-font-size fs (get-initial-context pwd (command \dm))
let ib = read-inline ctx {\SATySFi;} in
page-break (UserDefinedPaper (pwd, pht))
(fun _ -> (| text-origin = (0pt, 28pt); text-height = pht |))
(fun _ -> (| header-origin  = (0pt, 0pt); header-content = block-nil;
 footer-origin  = (0pt, 0pt); footer-content = block-nil |))
(line-break true true ctx (inline-fil ++ ib ++ inline-fil))
```

このwhatsthisファイルをplain pdfTeXでコンパイルするには以下のコマンドを実行します。

```sh
# plainなのでコマンド名は'pdflatex'ではなく'pdftex'
pdftex whatsthis
```

出力結果は以下の通りで、「TeX!!」と書かれたPDFが得られます。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/2996612b-1e61-176c-22b9-581fdf19c9ab.png)


※TeXのワークフローとしては、pdftexの他に「tex＋dvips」「tex＋dvipdfmx」にも対応しています。texの代わりにetex、(e)ptex、(e)uptexも使用可能です。

一方、whatsthisファイルをSATySFiでコンパイルするには以下を実行します。

```sh
satysfi whatsthis
```

この場合は、「SATySFi」と書かれたPDFが得られます。
![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/616892f9-bff0-9053-77d0-41dfdf13b10d.png)

どうやら所望のpolyglotが実現できたようです:smiley: 

## たねあかし

### TeXコードとしてのwhatsthis

plain TeXには「プレアンブル」「本文」という概念がないため、いきなり「出力する文字」を書き始めることができます。例えば以下のソースは（完全な）plain TeXの文書ファイルになっています。

```tex:example-1.tex
% いきなり文字を書き始められる
Hello, plain {\TeX} world!
\bye %←終了する命令
```

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/fdb8eff1-8e20-7c62-b728-6e947e49e78c.png)

従って、LaTeXとは異なり、`let s = …`等のSATySFiのコードを（少なくともTeXの特殊文字を含まない限りは）自由に書けます。もちろん、このままではこの文字列が“出力されてしまう”ことになり都合が悪いのですが、実はTeXでは**「出力ルーチンを無効化する」**ことにより、一旦出力したテキストを**“なかったこと”**にできます。

```tex:example-2.tex
@require: pervasives
%↑TeXでは単なるテキスト.
%本来"出力"されるはずだが, オマジナイを実行しているため出力されない.
%↓出力ルーチンを無効化するオマジナイ
\output{\setbox0\box255\deadcycles=0}
\bye
```

上記のexample-2.texをpdftex（やtex）でコンパイルしても、PDF（やDVI）ファイルは出力されません[^no-output]。出力ルーチンが無効化されているため、出力すべきページが存在しないと判断されるからです。

[^no-output]: ログ出力をみると`No pages of output.`と書かれています。

出力ルーチンを無効化した場合はソース上に普通に書いたテキストは出力されないのですが、`\shipout`という命令[^shipout-arg]によってボックスの内容を強制的にページに出力させることができます。

[^shipout-arg]: TeXの`\shipout`プリミティブは「ボックスの値」を引数に取ります。example-3.texでは`\hbox{\TeX!!}`という“直接記述された水平ボックス”が引数になっています。

```tex:example-3.tex
@require: pervasives %←出力されない
\output{\setbox0\box255\deadcycles=0}%←オマジナイ
%↓ボックスを強制的に出力する
\shipout\hbox{\TeX!!}
\bye
```

※この例ではレイアウトの調整をしていないので、既定の出力用紙サイズ[^default-paper]が適用され、さらに“既定の1インチ”のマージンを伴った位置に文字が出力されています。

[^default-paper]: 出力用紙サイズの既定値はTeXシステムの設定に依存しますが、大抵はA4判かレターサイズの何れかです。

この**“強い裏技”**を利用することでpolyglotのコードを構成できます。

  * まず「`@require`等のSATySFiソースで先頭に置くべきコード」を書く。
  * 次に```let s=``…``​```としてSATySFiの文字列リテラルを導入する。
      * 文字列リテラルの中では**任意の文字が書ける**（そして結局文字列にすぎないのでSATySFiの実行に影響しない）ので、この中で“TeXで必要な処理”、つまり「準備的処理」「オマジナイ」「`\shipout`命令の実行」を記述して最後に`\bye`を書く。
  * TeXは`\bye`が実行されたらそれ以降のソースは読まないため、**これ以降はSATySFiのソースを自由に記述できる。**

```text
@require: pervasives
let s = ``…………
……【TeXの準備的処理】……
\output{\setbox0\box255\deadcycles=0}%←オマジナイ
\shipout\hbox{……【TeXの出力内容】……}
\bye``
……【SATySFiコード】……
```

今回はTeXでは「160pt×90ptの用紙の中央に“TeX!!”の文字を配置したもの」を出力したいので、以下のコードを実行しています。

  * 出力用紙サイズを160pt×90ptに設定する。
      * この処理はTeXエンジンの出力モードがPDF出力かDVI出力かによって異なる[^papersize]ので事前に判別を行っている。
      * なお、個別処理を加えることで、XeTeXやLuaTeXへの対応も可能。
  * 160pt×90ptのボックスを作り、その中央に“TeX!!”の文字を配置する。
      * 中央揃えはTeXのグルーを用いて行う。
      * ただしその際に“既定の1インチ”を相殺するための変移を加えている。

[^papersize]: PDF出力の場合は`\pdfpagewidth`/`height`パラメタの値を設定します。DVI出力の場合はpapersize spacial命令を発行します。

### SATySFiコードとしてのwhatsthis

全体の構成を改めてSATySFiの立場でみてみましょう。

```text
@require: pervasives
let s = ``【謎の文字列】``
……【SATySFiコード】……
```

結局（SATySFiにとっては有用でない）“謎の文字列”のlet文があるだけで普通のSATySFiのコードになっていることがわかります。

※`@require`の追加も可能なので、普通にstdjaクラスの文書も作れます。

```text:stdjaクラスの文書の例
@require: stdja
@require: pervasives
let s = ``【謎の文字列】``
in
document (|
  title = {\SATySFi;}; author = {某氏};
  show-title = true; show-toc = false;
|) '<
  +p {私と一緒にサティスファイ！}
>
```

今回はSATySFiでは「160pt×90ptの用紙の中央にSATySFiのロゴを配置したもの」を出力したいのですが、このような出力に対応できる文書クラスは標準ライブラリの中にはないので、page-breakプリミティヴを直接利用してdocument型の値を構築する必要があります。whatsthisの後半部分はそのdocument型の値を表すコードです。

※page-breakプリミティヴについては以前に書いた記事[「SATySFiの『最短コード』」][shortest]で少し触れられているので、参考にしてください。

```text
let-inline ctx \dm m = inline-nil
let (pwd, pht, fs) = (160pt, 90pt, 36pt)
let ctx = set-font-size fs (get-initial-context pwd (command \dm))
let ib = read-inline ctx {\SATySFi;} in
page-break (UserDefinedPaper (pwd, pht))
(fun _ -> (| text-origin = (0pt, 28pt); text-height = pht |))
(fun _ -> (| header-origin  = (0pt, 0pt); header-content = block-nil;
 footer-origin  = (0pt, 0pt); footer-content = block-nil |))
(line-break true true ctx (inline-fil ++ ib ++ inline-fil))
```

概略は以下の通りです。

  * 幅を160ptとしたcontextを作る。
      * 数式は使わないので、数式のハンドラとなるインライン命令はダミー（`\dm`）にしている。
  * SATySFiのロゴ[^satysfi-logo]の両端にinline-filを付けた（1行分しかない）inline-boxesを作って、それをline-breakで“行分割”（実際には分割されない）してblock-boxesに変換し、それをpage-breakで“ページ分割”（分割されない）して1ページ分の本文の版面を得る。
      * SATySFiのロゴは160ptの幅の中で中央揃えになる。
  * 結果の本文領域を160pt×90ptの用紙に配置する。
      * 縦方向の中央揃えは（手抜きして:upside_down:）変移値`28pt`を直接指定している。

[^satysfi-logo]: pervasivesパッケージを読み込んでいるため、普通に`\SATySFi;`命令を使ってSATySFiのロゴが出力できます。

## まとめ

「[TeX & LaTeX Advent Calendar 2021][texadvent]」および「[SATySFi Advent Calendar 2021][satysfiadvent]」にはまだまだ空き枠が残っていてアレなので、**皆さん、どんどん登録しましょう！**:information_desk_person:（まとめろ）

[wp-polyglot]: https://en.wikipedia.org/wiki/Polyglot_%28computing%29
[shortest]: https://qiita.com/zr_tex8r/items/53d05e1766478c4dd8d2
[texadvent]: https://adventar.org/calendars/6724
[satysfiadvent]: https://adventar.org/calendars/6336