これは「TeX & LaTeX Advent Caleandar 2021」の7日目の記事です。
(6日目は doraTeX さん、8日目は zr_tex8r さんです。)
これは「SATySFi Advent Caleandar 2021」の7日目の記事でもあります。
(8日目は zr_tex8r さんです。)
プログラミング言語に関するニッチな話題の1つに「Polyglot」というものがあります。これは「単一のソースコードで異なる複数の言語で実行可能なもの」を指しています1。
- Polyglot (computing)(Wikipedia)
例えば、以下に挙げるのはPerlとRubyのpolyglotです。
print(0?"Ruby\n":"Perl\n")
本記事ではTeXとSATySFiのpolyglotに挑戦してみます。
LaTeXとSATySFiでpolyglotできない件
ご存じの通り、TeXのフォーマットで圧倒的に有名なのはLaTeXです。そこでまずは、LaTeXフォーマット2の使用を前提にしましょう。
ここでの最大の問題は「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」となります。
@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でコンパイルするには以下のコマンドを実行します。
# plainなのでコマンド名は'pdflatex'ではなく'pdftex'
pdftex whatsthis
出力結果は以下の通りで、「TeX!!」と書かれたPDFが得られます。
※TeXのワークフローとしては、pdftexの他に「tex+dvips」「tex+dvipdfmx」にも対応しています。texの代わりにetex、(e)ptex、(e)uptexも使用可能です。
一方、whatsthisファイルをSATySFiでコンパイルするには以下を実行します。
satysfi whatsthis
この場合は、「SATySFi」と書かれたPDFが得られます。
どうやら所望のpolyglotが実現できたようです
たねあかし
TeXコードとしてのwhatsthis
plain TeXには「プレアンブル」「本文」という概念がないため、いきなり「出力する文字」を書き始めることができます。例えば以下のソースは(完全な)plain TeXの文書ファイルになっています。
% いきなり文字を書き始められる
Hello, plain {\TeX} world!
\bye %←終了する命令
従って、LaTeXとは異なり、let s = …
等のSATySFiのコードを(少なくともTeXの特殊文字を含まない限りは)自由に書けます。もちろん、このままではこの文字列が“出力されてしまう”ことになり都合が悪いのですが、実はTeXでは**「出力ルーチンを無効化する」ことにより、一旦出力したテキストを“なかったこと”**にできます。
@require: pervasives
%↑TeXでは単なるテキスト.
%本来"出力"されるはずだが, オマジナイを実行しているため出力されない.
%↓出力ルーチンを無効化するオマジナイ
\output{\setbox0\box255\deadcycles=0}
\bye
上記のexample-2.texをpdftex(やtex)でコンパイルしても、PDF(やDVI)ファイルは出力されません3。出力ルーチンが無効化されているため、出力すべきページが存在しないと判断されるからです。
出力ルーチンを無効化した場合はソース上に普通に書いたテキストは出力されないのですが、\shipout
という命令4によってボックスの内容を強制的にページに出力させることができます。
@require: pervasives %←出力されない
\output{\setbox0\box255\deadcycles=0}%←オマジナイ
%↓ボックスを強制的に出力する
\shipout\hbox{\TeX!!}
\bye
※この例ではレイアウトの調整をしていないので、既定の出力用紙サイズ5が適用され、さらに“既定の1インチ”のマージンを伴った位置に文字が出力されています。
この**“強い裏技”**を利用することでpolyglotのコードを構成できます。
- まず「
@require
等のSATySFiソースで先頭に置くべきコード」を書く。 - 次に
let s=``…``
としてSATySFiの文字列リテラルを導入する。- 文字列リテラルの中では任意の文字が書ける(そして結局文字列にすぎないのでSATySFiの実行に影響しない)ので、この中で“TeXで必要な処理”、つまり「準備的処理」「オマジナイ」「
\shipout
命令の実行」を記述して最後に\bye
を書く。
- 文字列リテラルの中では任意の文字が書ける(そして結局文字列にすぎないのでSATySFiの実行に影響しない)ので、この中で“TeXで必要な処理”、つまり「準備的処理」「オマジナイ」「
- TeXは
\bye
が実行されたらそれ以降のソースは読まないため、これ以降はSATySFiのソースを自由に記述できる。
@require: pervasives
let s = ``…………
……【TeXの準備的処理】……
\output{\setbox0\box255\deadcycles=0}%←オマジナイ
\shipout\hbox{……【TeXの出力内容】……}
\bye``
……【SATySFiコード】……
今回はTeXでは「160pt×90ptの用紙の中央に“TeX!!”の文字を配置したもの」を出力したいので、以下のコードを実行しています。
- 出力用紙サイズを160pt×90ptに設定する。
- この処理はTeXエンジンの出力モードがPDF出力かDVI出力かによって異なる6ので事前に判別を行っている。
- なお、個別処理を加えることで、XeTeXやLuaTeXへの対応も可能。
- 160pt×90ptのボックスを作り、その中央に“TeX!!”の文字を配置する。
- 中央揃えはTeXのグルーを用いて行う。
- ただしその際に“既定の1インチ”を相殺するための変移を加えている。
SATySFiコードとしてのwhatsthis
全体の構成を改めてSATySFiの立場でみてみましょう。
@require: pervasives
let s = ``【謎の文字列】``
……【SATySFiコード】……
結局(SATySFiにとっては有用でない)“謎の文字列”のlet文があるだけで普通のSATySFiのコードになっていることがわかります。
※@require
の追加も可能なので、普通に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の『最短コード』」で少し触れられているので、参考にしてください。
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のロゴ7の両端にinline-filを付けた(1行分しかない)inline-boxesを作って、それをline-breakで“行分割”(実際には分割されない)してblock-boxesに変換し、それをpage-breakで“ページ分割”(分割されない)して1ページ分の本文の版面を得る。
- SATySFiのロゴは160ptの幅の中で中央揃えになる。
- 結果の本文領域を160pt×90ptの用紙に配置する。
- 縦方向の中央揃えは(手抜きして)変移値
28pt
を直接指定している。
- 縦方向の中央揃えは(手抜きして)変移値
まとめ
「TeX & LaTeX Advent Calendar 2021」および「SATySFi Advent Calendar 2021」にはまだまだ空き枠が残っていてアレなので、皆さん、どんどん登録しましょう!(まとめろ)
-
“Polyglot programming”という語は「単一のソフトウェアを複数の言語を用いて作製すること」を指すこともあります。 ↩
-
さすがに「LaTeX言語の仕様」の範囲内でpolyglotを作るのは明らかに困難なので、ここでは「TeX言語の使用を許可したLaTeXフォーマットのTeX」(いわゆる**“TeX on LaTeX”**)を対象にします。 ↩
-
ログ出力をみると
No pages of output.
と書かれています。 ↩ -
TeXの
\shipout
プリミティブは「ボックスの値」を引数に取ります。example-3.texでは\hbox{\TeX!!}
という“直接記述された水平ボックス”が引数になっています。 ↩ -
出力用紙サイズの既定値はTeXシステムの設定に依存しますが、大抵はA4判かレターサイズの何れかです。 ↩
-
PDF出力の場合は
\pdfpagewidth
/height
パラメタの値を設定します。DVI出力の場合はpapersize spacial命令を発行します。 ↩ -
pervasivesパッケージを読み込んでいるため、普通に
\SATySFi;
命令を使ってSATySFiのロゴが出力できます。 ↩