LaTeXはExcel方眼紙の夢を見るか

はじめに

近年,Excel方眼紙が注目されている(要出典).Excel方眼紙とは,自由に罫線を引くためにあらかじめセルを正方形状に設定しておいたExcelファイル,およびそれによって作成された帳票を指す.そのサンプルや問題点については,参考文献を読むとよい.

Excel方眼紙は,書類としてのデザインに重きを置かれて作成されているため,データ入力や抽出・再利用に余計な労力が必要になる.しかし,これを捨てるのは容易ではない.参考文献の言葉を借りれば『「紙」文化圏の大人にとって,「データ」文化への切り替えは容易ではない』*1

この状況を改善するには,「データ」文化側が,

  1. Excel方眼紙の存在を許容し,データ入力や抽出・再利用の方法を工夫する
  2. 「紙」文化側でもそれなりに扱えるような軽量かつグラフィカルにできる文書作成方法を提供する

といった対策が考えられる.

1については,OOXMLを直に操作したりPowerShellからExcelのCOMを叩く*2といった方法が考えられる.しかし,手動でExcel方眼紙を扱う際に発生する余計な労力と,これらの学習コストが釣り合うかどうかという問題がある.

2について考える.現状でも,LaTeXなどは表形式に対するマークアップ記法を用意している.しかし,これらの形式は単純な表に対して最適化されており,Excel方眼紙のターゲットとなる自由に罫線を引いた文書には向いていない*3.しかし,マークアップ記法さえ確立してしまえば問題は解決できる.

本記事では,2の手法について自由に罫線を引いた文書に対するマークアップ記法を提案し,そのLaTeXでの実装を示す.

マークアップ記法

今回作成した試作品は,Githubで公開している.

サンプルは,Excel方眼紙公開討論会を参考にして作成した*4

作成したスタイルファイル,hogan.styでは,次の形式で自由に罫線を引いた文書に対するマークアップ記法を提供する.

% <<x>>は横方向のセル数,<<y>>は縦方向のセル数
% <<w>>は1セルの幅,<<h>>は1セルの高さ
\begin{hogan}[cells=<<x>>*<<y>>, size=<<w>>*<<h>>]
    \begin{lines}
        <<罫線部分へのマークアップ>>
    \end{lines}
    <<各セルへのテキストの配置>>
\end{hogan}

いくつか例を示そう.まずは,3行3列の各セルに実線の罫線を引き,それぞれにテキストを入れる場合,次のようにする.

\begin{hogan}[cells=3*3, size=2.5cm*1cm]
    \begin{lines}
        +-----+-----+-----+
        |     |     |     |
        +-----+-----+-----+
        |     |     |     |
        +-----+-----+-----+
        |     |     |     |
        +-----+-----+-----+
    \end{lines}
        \ctextbox[at={(0,0)}]{\textbf{左上}}
        \ctextbox[at={(1,0)}]{}
        \ctextbox[at={(2,0)}]{\hbox{\tate 右上}}
        \ctextbox[at={(0,1)}, align=left]{左側\\左寄せ}
        \ctextbox[at={(1,1)}]{}
        \ctextbox[at={(2,1)}, align=right]{右側\\右寄せ}
        \ltextbox[at={(0,2)}, align=left]{左側\\セルも左寄せ}
        \ctextbox[at={(1,2)}, align=center]{中央\\中央寄せ}
        \rtextbox[at={(2,2)}, align=right]{右側\\セルも右寄せ}
\end{hogan}

この記述により,次の出力を得る.

f:id:hak7a3:20170819233334p:plain

上記のように,hogan.styでは罫線をlines環境配下にアスキーアートで与える.各行の高さは1行であることを求めるが,各列の幅は何文字あってもよい.実線の罫線を引く場合,横方向なら-,縦方向なら|を用いる.

描画時の幅および高さはhogan環境のオプションのみで決定される.Excelと違い,ある行の高さもしくは列の幅のみを操作はできない*5

セルへ文字列を印字するには,\ltextbox\ctextbox,もしくは\rtextboxのいずれかの命令を用いる.atオプションでセルの座標を与える.命令はそれぞれ,セル内の配置が左寄せ,中央寄せ,右寄せのテキストを生成する.これらの命令にはalignオプションを与えることができる.このオプションは引数のテキストが複数行のときに,そのテキストの整列法を指定する.なお,現状の実装ではセル内の折り返しを実装していないので,改行は手動で与える必要がある*6

hogan.styでは,実線以外にもいくつかの罫線を提供している.横・縦方向ともに*で太線が引かれる.また,横方向で",縦方向で:を用いると点線が引かれる.サンプルを示しておこう.

\begin{hogan}[cells=3*2, size=2.5cm*1cm]
    \begin{lines}
        +-----+-----+-----+
        |     :     *     |
        +"""""+-----+*****+
        |     :     *     |
        +-----+-----+-----+
    \end{lines}
        \ctextbox[at={(0,0)}]{↓→点線}
        \ctextbox[at={(2,1)}]{↑←太線}
\end{hogan}

f:id:hak7a3:20170819233345p:plain

セル結合は,セル間の罫線を引かない機能と,テキストを複数セル間にまたがって配置する機能を組み合わせて実現する.サンプルを以って示しておこう.

\begin{hogan}[cells=3*2, size=2.5cm*1cm]
    \begin{lines}
        +-----+-----+-----+
        |     .     |     |
        +     +     +-----+
        |     .     |     |
        +-----+-----+-----+
    \end{lines}
        \ctextbox[at={(0,0)}, box=2*2]{セル結合}
\end{hogan}

f:id:hak7a3:20170819233359p:plain

実装

hogan.styの描画は,単純にTikZを利用して実装している.工夫したことといえば座標の計算のみである.hogan環境内部はtikzpicture環境となっているので,TikZの力を借りて吹き出しを入れるといったことも可能である*7

lines環境のパースも特筆すべきことはない*8.現状はカテゴリーコードの変更をかけていないが,変更すれば縦方向の罫線なしの記法をにすることも可能な見込みなので,この辺りは再考の余地がある.

考察

hogan.styによって,アスキーアートを利用したある程度グラフィカルな文書作成環境を提供できる.しかし,現状のhogan.styだけでは大規模な文書の作成が難しい.Githubに載せているサンプルを作成してわかったのだが,セル数が増えれば増えるほど,lines環境に与えるアスキーアートを描くコストが増大する.その点を考えると,Excelは罫線を引くUIとして使いやすい*9.データだけではなく,罫線も入力しやすくする方法を模索していきたい.

まとめ

Excel方眼紙は悪い文明だが,駆逐するのは難しい.

*1:さらに,筆者の経験談でいえば,多少のプログラミング経験も改善には役に立たない.集計機能が付くことはあるが,データ入力のしやすさが改善されるような改良は観測したことがない.

*2:実際,お仕事では後者を使っている.処理が非常に遅いのでお勧めはしない.

*3:そもそも,Excel方眼紙のターゲットを「表」とみるのが誤りか

*4:なお,筆者は参加申し込みを忘れていた模様

*5:実装上の都合が大きな要因.私はそのようなExcel方眼紙が特に嫌いなので,実装する予定もない.

*6:こちらについては,時間があれば実装する見込み.

*7:ただし,現状は座標系の違いにより配置が面倒

*8:LISP on TeXより数段楽

*9:もっとも,印刷まで考慮するとExcelWYSIWYGとは言い難いが……

もうひとつのTeXがチューリング完全であることの証明

この記事はTeX & LaTeX Advent Calendar 2016の17日目の記事です. 2日目はYasuhide Minodaさんでした.18日目はVoDさんです.

はじめに

これまで,TeXチューリング完全であるかを証明する様々な試みがあった.ここにいくつか挙げておこう.

私の知る限り,そのすべてがTeXの胃,すなわちプリミティブの実行までを使用している.しかし,TeXには胃の前に口,すなわちマクロの展開がある.ここで,気になるのは次の問題である.

TeXはマクロ展開のみでチューリング完全であるのか?

この問いに答えるため,簡単なBrainf*ck処理系を作成してみた.それが,次である.

実用するためのプログラムでないため,使い方の説明は省略するが,sample.tex\edefの中にBrainf*ckコードがあり,それが\showによって正しく処理されていることがわかる.\edefの中ではTeXの胃が動かないことがわかっているので,TeXはマクロ展開のみでチューリング完全であることが示された.

この処理系実装には,いくつかの面白い点がある.今回の記事では,そのTeX言語テクニックを紹介していこう.

マクロ展開のみにすることによるメリット・デメリット

テクニックそのものの前に,TeX言語をマクロ展開のみに制限して使用することのメリットとデメリットをまとめておこう.

マクロ展開のみで望んだ結果が得られるマクロのことを,界隈では「完全展開可能な」マクロと呼ぶ.TeX内において,完全展開可能なマクロは制限なく使用することができる.というのも,TeXは文脈によって,完全展開可能であることを要求されるのだ.先のsample.texにおける\edefの中などがそれである.そのような状況でも使用可能なマクロは,他のマクロと組み合わせて使用することが容易である.

デメリットは,完全展開可能であるために,ほとんどの副作用が禁じられる*1点だ.これにより,次のことができなくなる.

  • マクロ展開中のマクロ定義
    • ラムダ計算のように,その場でマクロを定義できない
    • 値の書き換えといった動作も禁止される
  • 算術演算
    • レジスタへの書き込みが禁止されるため,四則演算が封じられる
    • e-TeX拡張を使えば可能なのだが,面白くないので縛っておく

これらの制限により,展開可能なマクロを実装することは困難である.

完全展開可能Brainf*ck処理系実装のためのテクニック

では,完全展開可能Brainf*ck処理系を実装するためのテクニックを見てみよう.

入出力・環境

まずはBrainf*ck処理系を実行していった時のテープなどの状態,すなわち環境をどう表現するかを見ていこう.まず,完全展開可能にする時点で,副作用が一切使えない.よって,直接テープを書き換えるといった実装はできない.ここでは,Brainf*ck処理系を環境を受け取り,環境を返すマクロとして作成することとした.もう少し詳しく説明すると,次のようになる.

Brainf*ck処理系における 環境 とは,(テープ, 入力, 出力, コード) である.テープとは,次節で示す数値を{}で囲んだものの列である.テープには,読み取り位置という,特別な要素がひとつ存在する.入力と出力はともに文字列である.コードとは<>+-.,[]から構成される列*2である.

Brainf*ck処理系における評価(→)とは,環境から環境への関数である.評価を複数回適用することを→*で表現する.

初期環境Iを(初期テープ, s, ε, c)とする.ここで初期テープとは,列のすべての要素が0の読み取り位置が左端のテープで,sは任意の文字列,cは任意のコードである.Eをコードが空列の環境とし,I→*Eであるとき,Eの出力を実行結果と呼ぶ.

ここで,本来は→の中身,すなわち評価規則を定義するべきだが,長くなるので省略する*3.評価規則は普通のBrainf*ckなので,定義自体は簡単なはずだ.

今回作成したBrainf*ck処理系とは,この関数→を完全展開可能なマクロとして実装したものにあたる.

数値の表現

ここで,前節で述べなかった数値の定義を示しておこう.当然,TeXの数値は完全展開可能な状況では使いづらいので,使いやすいように定義している.

  • 空列εは数値である.
  • 任意の数値nに対し,onは数値である.

よくある自然数の定義法である.ここでは,succを「oを前置する」関数としている*4

テープの表現

さて,ここで具体的にテープをどう実装したかを述べよう.テープに必要な要件は次である.

  • 副作用なしで,読み取り位置を前後させた状態を構築できる
  • 副作用なしで,読み取り位置の値を書き換えた状態を構築できる

あ,これ研究室ゼミでみたことある*5.Zipperだ!

Zipperとは,副作用なしで要素をたどり,書き換えるなどの操作ができることができる木やリストなどを実装するためのプログラミング技法である.先の要件を満たすリストはその典型である.例を挙げておこう.

リスト[1,2,3,4]に対し,読み取り位置が要素3の位置にあるとする場合,これを([2,1],[3,4])の組で表現する.読み取り位置を右にずらすときは,右側のリストの先頭を左側のリストの先頭に送る.すなわち([3,2,1],[4])のようになる.現在の読み取り位置の値を書き換えるには,右側のリストの先頭を置換えればよい.([3,2,1],[4])の先頭を42に書き換えると([3,2,1],[42])になる.

さて,これをTeXでどう実装するかである.左側のリストをl1,右側のリストをl2とする.これらは,単純に数値を{}で囲んだものの列である.これに対し,リストZipperは\@febf@mem@startl1{}{}\curl2で表現される.リストZipperへの操作は,マクロとパターンマッチで実装される.なお,l1の後に不要な{}{}があるように思えるが,これには意味がある.パターンマッチの都合上,左側に最低でも2要素必要だったのだ.

入出力

ここまでで,テープに数値がどのように書き込まれるのかを確認した.ここで問題になるのが入出力である.入出力は文字列なので,文字から自然数への相互変換が必要になってくる.これは,事前に相互変換を行うためのテーブルのようなマクロを用意することとした.ソースコードも分離してあり,in.texが文字から自然数への,out.tex自然数から文字への変換を行うマクロになっている.それぞれの中身を一部見てみよう.

% in.texの一部
\expandafter\def\csname @febf@in@'\endcsname{ooooooooooooooooooooooooooooooooooooooo}
\expandafter\def\csname @febf@in@(\endcsname{oooooooooooooooooooooooooooooooooooooooo}
\expandafter\def\csname @febf@in@)\endcsname{ooooooooooooooooooooooooooooooooooooooooo}
\expandafter\def\csname @febf@in@*\endcsname{oooooooooooooooooooooooooooooooooooooooooo}
\expandafter\def\csname @febf@in@+\endcsname{ooooooooooooooooooooooooooooooooooooooooooo}
% out.texの一部
\expandafter\def\csname @febf@out@ooooooooooooooooooooooooooooooooooooooo\endcsname{'}
\expandafter\def\csname @febf@out@oooooooooooooooooooooooooooooooooooooooo\endcsname{(}
\expandafter\def\csname @febf@out@ooooooooooooooooooooooooooooooooooooooooo\endcsname{)}
\expandafter\def\csname @febf@out@oooooooooooooooooooooooooooooooooooooooooo\endcsname{*}
\expandafter\def\csname @febf@out@ooooooooooooooooooooooooooooooooooooooooooo\endcsname{+}

文字aから対応する自然数への変換は,\@febf@in@aというマクロで,自然数nから対応する文字への変換は\@febf@out@nというマクロで表現される.これらの変換は\csnameを用いて動的にマクロを呼び出すことによって解決される.

当然,%など,TeXの中で特別な役割を持つ文字を,この方法で変換することは難しい.今回のBrainf*ck処理系の目的は,あくまでチューリング完全性の確認であるから,使用できる文字の集合が小さくなることは許容できるとしている.許容できないという読者は,ぜひ改善してみてほしい.

評価

準備が終わり,あとはコードを評価する部分だけである.この部分は先にコードを示しておこう.

\def\@febf\@febf@mem@start#1\cur#2\inp#3\out#4\code#5{%
  \expandafter\ifx\csname @febf@#5\endcsname\relax
    \expandafter\@febf@mem
  \else
    \expandafter\@febf@normal
  \fi{#1}{#2}{#3}{#4}{#5}%
}

評価→を表すマクロが,\@febfである.このマクロは引数として\@febf@mem@start#1\cur#2で示されるテープ,#3で入力,#4で出力,そして#5でコードの先頭1文字を取る.このマクロは,基本的に#5がどの文字かによって,どの評価規則を適用するかを決定しているだけである.

ただし,ここで[]の対応を取る処理をさぼるため,TeXのカテゴリーコードを使った技法を使っている.実は今回の実装では,[{]}を同等にしている.これは,[]のカテゴリーコードという値を{}と同一の値にすることで実現している.TeXにおいて,{}は自動で対応が取られるのはよく知られているであろう.これが[]にも適用されるのだ.

しかし,この方法を使う場合,TeXのパターンマッチ上,+[+]など,「1文字」と「[+1文字+]」が区別できないという問題が生じる.幸い,Brainf*ckでは<>が意味のない命令列として使用できるので,この問題はチューリング完全性を確認する目的に対しては障害とならない.

実行結果の取得

評価→が得られたら,最後はそれを可能な限り初期環境に対して適用し,実行結果を得るだけである.しかし,ここでも工夫が必要になる.残念なことに,TeXのマクロ展開はスモールステップなので,可能な限りマクロを展開し続けるという行為もテクニックが必要になるのだ.今回の実装では,\romannumeralトリックと呼ばれる技法を使った.詳しい説明は,すでに公開された記事があるので,そちらを参照してほしい.

まとめ

TeX関数型言語(諸説あります).

*1:実は,未定義のマクロを\csnameで呼んだ場合という例外がある.

*2:面倒なので他の文字はコードで与えられないとしている.

*3:ここまでで十分長いというのは認める

*4:なお,ソースコード上ではだんご数と呼んでいる.出元はご存知魔法言語 リリカル☆Lispである.

*5:実際にあった.なお,二分木を見たことは覚えているが,リストを見たかどうかは覚えていない

ELVMにTeXバックエンドを足した話

この記事はTeX & LaTeX Advent Calendar 2016の3日目の記事です. 2日目はdoraTeXさんでした.4日目はtex-ut-texさんです.

はじめに

2017年に学ぶべき言語12位はLaTeXである.すなわち,TeX言語をホンキで語ることが求められている(Advent Calendarテーマ回収).

前回の記事では,ELVMバックエンド作成法を紹介した.本記事では,それをどうTeXに適用したか,すなわちELVMのTeXバックエンドで用いているTeXプログラミング技法を紹介する.これが理解できると,TeXで言語処理系を作るのがあまり困難ではないということがわかるはずだ.

TeXバックエンドで用いたプログラミング技法

メモリおよびレジスタの表現

メモリは\@mem@42など,\@mem@ + 番地で表現される*1.初期値に0以外の値が与えられるか,EIR実行中に書き込みが行われた場合に定義される.事前に全番地分定義しておけばよいと思うかもしれないが,24ビット空間すべてを定義してしまうとTeXで定義できるマクロの最大数に到達してしまい,TeX処理系が停止してしまう*2.この事象の回避のため,メモリを表すマクロの定義タイミングを必要最低限にしている*3.これをどうにかするのが今後の課題だろう*4

レジスタ\@reg@pcなど,\@reg + レジスタ名で表現される.

命令列の表現および実行部

前回の記事でも述べたように,EIRの命令列のうち,同じpcの値を持つものは,同じグループに属するので処理をまとめる必要がある.TeXでは,これを\@inst@23のように\@inst@ +pcの値というマクロを生成することとした.このマクロは引数がなく,展開・実行するとグループ内の命令が実行されるようになっている.

EIRの実行を司る部分は次のようになっている.

\def\@loop@main{%
\let\@@next\@loop@main
\csname @inst@\@reg@pc\endcsname
\count0=\@reg@pc\relax
\advance\count0by1\relax
\edef\@reg@pc{\the\count0}%
\@@next}\@loop@main

\@loop@mainがループの本体で,\@@nextに自身を\letし,それを展開することによってループを末尾再帰で実現している(なぜ回りくどい方法を取ったかは後述).ループのメインは3行目の\csname @inst@\@reg@pc\endcsnameで,この部分で\@inst@ +pcの値を作り出し,それを展開・実行している.その展開・実行の後,pcの値をインクリメントしている*5

各命令の表現法

ここまでで,大まかなEIRからTeXへの変換法がわかった.次に,EIRの各命令がTeXにどのように翻訳されるか,その詳細を見ていこう.

入出力(PUTCGETC

TeXは他のプログラミング言語と異なり,標準入出力を扱うのに苦労するという話はよく知られているであろう.入力に関してはTeXの目*6を気にしないとならない.バイナリ列として入力を扱いたくてもそう簡単にはいかない.出力に関しても同様で,ASCIIコードの印字可能でない範囲を出力することが原理的に困難である.さらに,バージョン情報などの自動出力も抑制できない.

TeXバックエンドを作るにあたり,入出力に関してはTeXの外の世界に頼るしかないと割り切った.そこで,ここでは次のような変換を行うことにした.

  1. EIRに与えられる入力を外部の力を借りて,そのASCIIコード値の改行区切り列に変換する
  2. 1で得られた結果を,TeXの標準入力に与える
  3. TeXは,別ファイルにEIRの実行結果としての標準出力をASCIIコード値の改行区切り列で出力する
  4. 3で得られたファイルを外部の力を借りて通常の文字列に戻す

図示すると,次のような形になる.赤色はASCIIコード値の改行区切り列,グレーは捨てるファイルや出力を指す.また,foo.texTeXバックエンドより生成されたEIRのコンパイル結果である.

f:id:hak7a3:20161202195906p:plain

ここで,「入力をASCIIコード値の改行区切り列に変える」とは,

hoge
fuga

という入力を

104
111
103
101
10
102
117
103
97

に変換することを指す.

この変換を挟む関係上,TeXバックエンドの出力結果は入出力をインタラクティブに扱えないという制限を持つ.これを改善するためには,TeXで標準入出力を普通のプログラミング言語のように取り扱えるようにする技法が求められる*7

算術演算(ADDSUB

前回の記事で言及した通り,ELVMでは整数が24ビットである.そのため,多くの言語ではマスクしてやる必要がある.しかし,TeXはビット演算をプリミティブに持っていない.今回の加算の実装では,nビット整数とnビット整数の和はたかだかn+1ビット整数であるという性質を用いて,和が24ビットの範囲を超えているようなら,そこから224を引くという実装にしている.減算についても同様である.

EXIT

プログラムを終了するとき,メインループ\@loop@mainの末尾再帰を停止してやる必要がある.そこで,今回の実装では,EXIT\@@next\relax\letするように翻訳した.これにより,\@loop@mainの末尾が\relaxと等価になり,末尾再帰が終了する.

その他

特筆すべきものはないだろう.

まとめ

やはり,TeX言語はそんなに難しくない.

参考

ELVMには現在,興味深い言語のバックエンドがどんどん追加され,その実装解説が出ている.ここにリストで挙げておこう.

*1:当然,\csnameで生成するのでひとつのコントロールシーケンスである

*2:正確には,限界はもっと早い.ELVMのバックエンドのテストにelcのコンパイルがあるのだが,そのテストが通らない.

*3:なお,マクロを未定義の状態には戻せないので,メモリを大量に使用するプログラムを実行しようとすると死ぬ.

*4:むしろ,このままでは申し訳ないので,近いうちにやります.

*5:実は,この部分もやや冗長になっている.レジスタを単純にマクロに割り当てたことが主な原因で,レジスタTeXのカウントレジスタを割り当てるといいのかもしれない.

*6:入力列をトークンに変換する部分

*7:教えてください

ELVM Compiler Infrastructureバックエンド作成のすゝめ

初編

「天はELVM Compiler Infrastructureの上にELVM Compiler Infrastructureを造れり」と言えり。されば天よりCコンパイラを生ずるには、……

というわけで,この記事では,(どこかに作者の資料があるかもしれないが)ELVM Compiler Infrastructureバックエンド作成法を解説する.

ELVM Compiler Infrastructureとは

ELVM Compiler Infrastructure(以下,本記事ではELVMと省略)とは

から構成されるCコンパイラ*1である.作者は@shinh氏.ELVMの詳細はここあたりを参照するとよいだろう.要は,Cプログラムを様々な他言語に変換できる.

何がうれしいのか

Turing完全性が云々……という話を置いておけば,純粋に変換でしかないので,普通のプログラミング言語ならあまりメリットはないだろう.しかし,これが難解プログラミング言語になると話が変わってくる.作者がLispインタプリタをPietで動かしたように,記述が困難な言語で大規模なプログラムを生成しやすくなるのだ.

さらに,ELVMの大きな特徴として,ELVMそのものがELVMでコンパイルできることが挙げられる.つまり,ELVMのバックエンドに追加されている言語でCコンパイラをお手軽に作ることができるようになるのだ.

話は変わり,近年,私のTLにおいて次の事象が観測された.

こんな話もあった.

時代がTeXでCコンパイラを求めていることは自明であった.しかし,ELVMバックエンドにはTeXがない.というわけで,ELVMのTeXバックエンドを作成し,それを用いてCコンパイラ(8cc.tex)を作成した.

8cc.texの話は別の機会に行うものとして,さっそくELVMバックエンドの作成方法を記していこう.

ELVMバックエンド作成

準備

まずは,バックエンドに追加する言語を決めて本家をfork.

バックエンドの作成

実際,バックエンドを作成する際に作成するファイルは少ない.foo言語のバックエンドを作るなら,targetディレクトリにfoo.cを作成するだけである.正直,既存の多言語での実装を読めばどのように実装すべきかはすぐにわかるのだが,次の点に注意して読むとよりわかりやすくなるだろう.

  • レジスタは全部で7つ.特に気にするべきなのはpcである.名前の通りプログラムカウンタで,実行のループのたびにインクリメントする必要がある
  • EIRは24bitマシンであることを想定している.普通の言語なら適宜マスクをかける必要があるだろう
    • で,私はこの部分でUINT_MAX + 1を使用するという初心者レベルの失態を犯していた.後世のために,このミスを告白しておく
  • メモリの初期値はModule構造体のdataに0番地から順に格納されている.ただし,dataに入っていない部分についても読み込んだ時に0が返ってくるようにしておく必要がある
  • 命令はdataとは別にModule構造体のtextで与えられる*2.これをバックエンドに追加する言語用に変換していく.
    • textの型はInstで,単方向リストになっている.メンバpcが同一のInst同士は,ひとつのグループとして扱う必要がある*3.これをバックエンドに追加する言語用に変換していく.ジャンプもこのグループ単位で行われる点に注意

さらに,EIRは命令セットが小さい.大まかな特徴をあげておこう.

  • 算術演算は加算(ADD)と減算(SUB)しかない.なお,乗算,除算,およびビット演算はビルトイン関数で実装済みである
  • ファイルIOはない.libcの実装を見ればわかるが,fopenしてもstdinが返ってくる
  • 標準入出力の命令は1文字読み込み(GETC)と1文字書き込み(PUTC)しかない

ビルドおよびテストの準備

ここまでで,foo.cの作成が終了したとする.あとは,これを本体に組み込んでテストを実行するだけである.

まずは,バックエンドを使えるようにするため,target/elc.cfoo.cのメインの関数のプロトタイプ宣言を記述しよう.次のようになるはずである.

/* ...(中略)... */
void target_bef(Module* module);
void target_bf(Module* module);
void target_unl(Module* module);
void target_foo(Module* module); /* <- ここが追加 */
typedef void (*target_func_t)(Module*);

次に,target_fooが呼べるように条件分岐を追加する.次のようになるはずだ.

static target_func_t get_target_func(const char* ext) {
  if (!strcmp(ext, "rb")) {
    return target_rb;
  } else if (!strcmp(ext, "py")) {
    return target_py;
  } else if (!strcmp(ext, "js")) {
    /* ...(中略)... */
  } else if (!strcmp(ext, "unl")) {
    return target_unl;
  } else if(!strcmp(ext, "foo")) { /* <-ここが追加 */
      return target_foo;           /* <-ここが追加 */
  } else {
    error("unknown flag: %s", ext);
  }
}

なお.関数の追加位置については,(作者に本来は確認すべきだが)末尾のほうが良いだろう.最初に追加されたUnlambdaは末尾に,次に追加されたVim scriptはEmacs Lispとの対比の目的でその脇に配置されたが,私がTeXを追加するときに,誤ってVim scriptの脇に配置してしまった*4*5

そして,追加したファイルをmakeできるようにする.Makefileに次のように追加する.

# ...(中略)...
# 末尾にfoo.cを追加
ELC_SRCS := elc.c util.c rb.c py.c ... unl.c foo.c
# ...(さらに中略)...
# 同じような記述が並んでいるところ(Targets)に次を追加
TARGET := foo
RUNNER := <<foo-runner>>
include target.mk

<<foo-runner>>には,コンパイル結果を動かすためのコマンドを記載しておく.1コマンドで動かない場合は,tools/foo.shを追加し,1コマンドで動くようにしておく.TeXではこれを用いている.

これで,makeが行えるようになった.追加した言語のテストはmake foomake elc-fooで行う.これがすべて通過すれば追加した部分は問題ないだろう.その後,make testすることによって,全言語のテストを行うことができる.make testは私の環境*6で時間がかかったので,お茶でも飲んで行く末を見守ると良いだろう.

テストまで通ったら,あとはREADME.mdのバックエンド一覧にfoo言語を追加し,言語数をインクリメントする.

終わりに

世界にELVMバックエンドが増えるといいな.

*1:現状は.フロントエンドが追加されればあるいは

*2:ハーバードアーキテクチャ

*3:Javaバックエンドでは同一のメソッドになるようになっている.

*4:さらに,README.mdとmakeなどの順序が異なってしまっている

*5:実は,その後に追加されたCommon LispTeXの脇に配置されてしまった.本当に申し訳ない

*6:Surface Pro 3上のHyper-Vで動くArch Linux(512MB memory)

TeX芸人の知らない#の挙動

8/28 \edefにおいて,マクロ展開とパラメータトークン生成のどちらが先に走るのかを追加

はじめに

このツイートから始まった一連の話.

この事象を解析していくと,TeXがマクロ中の#をどのように扱っているかの一端を知ることができる. この記事では,私が得たTeXにおける#の扱いを紹介する.

基本

TeXのデフォルトでは,文字#に「パラメータ文字」という役割を与えている. この文字はマクロ定義おける引数を表すために用いられる. 次の例を見てみよう*1

\def\somecs#1{#1 is crazy}  % マクロ定義
\toks0\expandafter{\somecs{\TeX}}
\showthe\toks0              % -> \TeX  is crazy.

よく知られているように,\somecsが次のようなマクロとして定義されている.

  • 1引数をとる
  • 引数を<<第1引数>>としたとき,<<第1引数>> is crazy.というトークン列を出力する

このとき,引数であることを示すための文字の役割がパラメータ文字である. パラメータ文字はカテゴリーコードが6の文字である.

パラメータ文字#はマクロ定義内で次の役割を持つ*2

  1. #1から#9を第1引数から第9引数として取り扱う
  2. 置換えテキストで##を単一の#に置換える
  3. パラメータテキストの末尾が#である場合,{がパラメータテキストと置換えテキストの両方の末尾に挿入されたものとして扱う(TeXブック p.276)

ここで,置換えテキストとは{}で囲われた部分で, パラメータテキストとはコントロールシーケンスと{の間のことである.

1についてはよく知られているだろう.

2は#をエスケープするための規則である.主にマクロを定義するマクロで使用される. なお,パラメータテキスト内では2の規則がないため, #にパターンマッチすることはできない*3

3はLaTeXを使っている場面においては触れる機会が少ないルールであろう.次の例を見てみよう.

\def\testA#1#2{!#2!#1!}
\def\testB#1#{\testA{<#1>}}
\edef\result{\testB foo{bar}}
\show\result % !bar!<foo>!

この例の\testBが3のルールを使用したマクロである.\testBの定義を見ると,

> \testB=macro:
#1{->\testA {<#1>}{.
<*> \show\testB

となる.->の前にあるパラメータテキストだけでなく, 置換えテキストの末尾にあることがわかるだろう.よって,\testB foo{bar}は 次のように展開される.

  1. \testB foo{bar}
  2. \testA {<foo>}{bar}\testB foo{\testA {<foo>}{に置き換えられた)
  3. !bar!<foo>!\testAの展開)

ツイートへの回答

最初に挙げたツイートに回答するには,これに合わせて次のルールを知る必要がある.

  1. 基本で挙げたすべてのルールはマクロ定義時に解決される
  2. カテゴリーコードが6の文字が出力されるとき,その文字は2回出力される
  3. マクロ定義外では,このルールは適用されない

3は簡単だとしても,1と2については例が必要であろう.次のコードを考える.

\def\testA#1{#1##}
\show\testA

これを実行すると次を得る.

> \testA=macro:
#1->#1##.

一連の流れを考える.まず,\testAが定義される.このとき,#の マクロ定義中のルールが適用され,#1が第1引数を表すようになる.このとき, 特別なトークン(パラメータトークン)が生成される.さらに###に 置換される.よって,\testAのパラメータテキストは「パラメータトークン#1」, 置換えテキストは「パラメータトークン#1#」になる.これが先のルール1である.

そして,このマクロを\showする.このとき,パラメータトークンが#1のような 形式に,#が先のルール3により2回出力される.よって,#1->#1##が出力される. このようなマクロ定義の逆の処理が行われることにより,\showしたときに直感的に わかりやすい表示が得られる.

さて,ツイートの回答に移ろう.最後に知るべきことは\unexpandedの中はマクロ定義中 でないということである.よって,\testの置換えテキストは#1から構成されることになる. これが出力されるので,先のルール2が適用され,#1->##1という出力を得る.

応用

さて,感のいい読者諸兄は,次のような疑問を持ったであろう.

  • カテゴリーコード6の文字が複数あったらどうなるのか
  • パラメータトークンをつくるときの1から9の文字になにか条件はないのか
  • \edefにおいて,マクロ展開とパラメータトークン生成のどちらが先に走るのか

これらの疑問を解決していこう.

カテゴリーコード6の文字が複数あったらどうなるのか

この疑問については,TeXブックのEXERCISE 20.7でほぼ解決される.幸いなことに,回答が ついている問題である.基本的には次のルールを覚えておけばいいだろう.

  • \letしたトークンも有効
  • ##から#への置き換え時に残るのは2つめのトークン
  • \letしたトークンはコンソール出力時の2回出力の対象外
  • 置換えテキスト内のパラメータトークンを出力する際のパラメータ文字は,パラメータテキストの最後のパラメータで使用された文字

次の例を見てみよう.

\let\S=#       % \Sは#と同等
\catcode`\+=6  % +もパラメータ文字に
\def\foo#1\S2+3#{#++##\S\S2}
\show\foo

次の出力が得られる.

> \foo=macro:
#1#2+3{->++##\S +2{.

パラメータテキストでは,\S#と同じ扱いになっていることがわかるだろう. 置換えテキストでは#++に,+##に,#\S\Sに置き換えられる. そして,+++として,###として出力される.さらに,置換えテキストの パラメータトークン#2+2となって出力される.よって,先の出力が得られるのである.

パラメータトークンをつくるときの1から9の文字になにか条件はないのか

これはTeXブックに記載が見当たらなかったので,調べた限りの結果を示しておこう.

  • \letしたトークンは無効
  • カテゴリーコードが12でないときも無効

この例でわかるだろう.

\let\One=1
\catcode`\2=11
\def\foo#\One{#\One}
\def\bar#1#2{#2}

出力は次の通り.

! Parameters must be numbered consecutively.
<to be read again>
                   \One
l.3 \def\foo#\One
                 {#\One}
?
! Illegal parameter number in definition of \foo.
<to be read again>
                   \One
l.3 \def\foo#\One{#\One
                       }
?
! Parameters must be numbered consecutively.
<to be read again>
                   2
l.4 \def\bar#1#2
                {#2}
?
! Illegal parameter number in definition of \bar.
<to be read again>
                   2
l.4 \def\bar#1#2{#2
                   }
?

なぜこのような動きをするのかについては未調査である.

\edefにおいて,マクロ展開とパラメータトークン生成のどちらが先に走るのか

これは私もかなり混乱した*4.結論としては, マクロ展開が先に走る.次の例を見てみよう.

\def\firstoftwo#1#2{#1}
\edef\testA#1{\firstoftwo#91}
\show\testA

出力は次の通り.

> \testA=macro:
#1->#1.

この件に関しては,Twitterでかなりのご迷惑をおかけしました.お詫び申し上げます.

まとめ

TeXは難しい.

*1:出力に不要な空白が入るように見えるのはコントロールシーケンスの出力規則のせい

*2:実はアラインメントでも#を使うのだが,本記事の対象外である

*3:##を定義に含めようとするとエラーになる.私は回避策を知らない

*4:hak7a3のTwitterを参照するとよくわかる

ユーザ定義マクロのみを展開する手法に関する考察(1)

はじめに

こんな話が出ていた.

面白そうなので,実際に作成してみることにした.

中間報告

で,作ってみたのがこれ.

github.com

実行してみると,ユーザ定義マクロが展開され,マクロの定義が消失することがわかる.アイデア自体は簡単で,次を実行するプログラムを作成しただけである.

  1. \@argdef を書き換えることにより,ユーザ定義マクロの定義を取得(header.texの内容)
    • 要は\newcommandの内側をフック
    • 結果はgathergetuser.texに書きこまれる
  2. 元文書をの各行について,次を処理($\mbox{expander.texの内容} + \alpha $)
    1. \documentclassなど,LaTeXが定義しているマクロに\relaxを代入
      • 例外的に\newcommandはコメント行を出力するようなマクロに再定義
    2. 1で取得したユーザ定義マクロを再定義
    3. 対象の行を\edefにより展開
      • このタイミングでユーザ定義マクロのみが展開される
    4. 得られた結果をファイルに出力

各行ごとに処理しているのは,改行を保持するためである.

考察

一見するとうまくいっているように感じるこの手法だが,実はうまくいかないケースが多い.かなり数が多いのだが,いくつか例を挙げておこう.

展開しないコントロールシーケンスの列挙が面倒

展開したくない制御綴りはexpander.texで列挙している.今回の試作ではサンプルを通すための必要最低限しか定義していないので,このあたりのメンテナンスがつらい.また,各種(自作以外の)パッケージ由来のマクロも展開してはいけないはずなので,このあたりも考慮して展開してはいけないマクロ一覧を列挙する必要がある.

実は,「展開したくないもの」よりも「展開したいもの」を列挙する方が圧倒的に楽なので,文書中の全トークンをなめて,展開したいものならば展開し,そうでないならそのままというような処理の方が適切であるように思える*1

完全展開してはいけないコンテキスト

verbatim環境の中など,そもそもユーザ定義マクロであろうとも展開してはいけないケースがあるが,現在の手法ではそれに対応していない.そのようなコンテキストはそんなにないはずなので,その部分のみ特殊な展開処理をしてやる必要がある.

ユーザ定義マクロを完全展開することが不適切な場合

ユーザ定義マクロが副作用を含む場合,単純な完全展開では求める結果が得られない.ユーザ定義マクロを安全に展開できるのは1段階のみである.しかし,ユーザ定義マクロが別のユーザ定義マクロを使用している場合,1回の展開ではそれを除去しきれない.

このあたりは,「ユーザ定義マクロは完全展開しても安全なものに限定する」,「ユーザ定義マクロがある程度残ることを無視して1回の展開に限定する」などの対策しか思い浮かんでいない.

展開が複数行にまたがる場合

今回は改行を保存するために1行ごとに処理しているが,そもそも展開に複数行使う場合がある.

TeXのエラーをハンドリングして,エラーが出なくなったらその出力を採用する」という方法や,「そもそも文書全体を処理した後,改行を何らかの方法で復元する」,「改行をあきらめる」などが考えられる.

まとめ

まだまだである.なので,別アプローチで試作している.

*1:これを実現するためにはTeX芸人検定2段が必要になる

\csnameの挙動

この記事は TeX & LaTeX Advent Calendar 2015 の20日目の記事です。
昨日はumireonさんでした.明日はtermoshttさんです.

はじめに

今年もAdvent Calendarの季節がやってきた.今年の重点テーマは

「今さら人に聞けない、TeXのキホン」

だそうだ.そこで,今回はTeXの基本的なプリミティブである\csnameについて,その挙動を紹介しよう*1と思う.

基本的なこと

\csnameは,端的に言うと文字列から制御綴りを生み出すためのプリミティブであり,\csnameと\endcsnameの間にある文字トークン列を名前とする制御綴りに展開される.例えば,

\csname Large\endcsname

は\Largeと書いたものと同様に扱われる.これだけでは利便性がないが,中の文字列を動的に変更できるようにすると,使用する制御綴りを動的に選択できるようになる.次のようなマクロを考えるとわかりやすいだろう.

\newcommand{\hoge}[1]{\expandafter\show\csname#1\endcsname}

この\hogeというマクロは,マクロもしくはプリミティブの名前について,その意味をコンソールに出力する.\hoge{Large}とすると,(クラスファイルにもよるが)次のような出力が得られるはずである.

> \Large=\long macro:                
->\@setfontsize \Large \@xivpt {18}. 
<recently read> \Large               
                                     
l.4 \hoge{Large}                     

\csnameは便利なプリミティブであり,LaTeXでは\beginおよび\endの実装に使われていることで有名[要出典]である.簡単にいうと,\begin{hoge}は\hogeに,\end{hoge}は\endhogeに展開される.この実装に\csnameが使われている.

与えることが可能なトークン

The TeXbookでは,\csnameを次のように説明している.

When TeX expands \csname it reads to the matching \endcsname, expanding tokens as it goes; only character tokens should remain after this expansion has taken place.

すなわち,文字トークン以外が与えられるとエラーになる.同書では,\csname\TeX\endcsnameがエラーになる例として紹介されている*2

未定義な名称を使った時の挙動

\csnameで与えた名称が事前に定義されていない場合はどうなるのだろうか? The TeXbookの\csnameに関する記述には続きがある.

Then the "expansion" of the entire \csname...\endcsname text will be a single control sequence token, defined to be like \relax if its meaning is currently undefined.

そう,定義されていない場合は\relaxのように定義されるのである.もし与える名前が間違っていても無意味なものとして取り扱ってくれる.

しかし,この性質はTeXにおいて特異なものでもある.TeXは,マクロの世界とプリミティブの世界を厳格に区別していて,マクロ展開時には定義や代入を含むすべての副作用を発生させることができない,純粋な言語だ*3.にもかかわらず,\csnameだけはマクロ展開時に定義を行う.実は,この定義の動作自体も,他の定義と違っていたりする部分がある.ここからは,\csnameがマクロを\relaxに定義する際の挙動を見ていこう.

\relax自身が書き換えられている場合

TeXは色々なものをユーザが書き換え可能である.プリミティブもその対象で,\relaxを別のマクロに置き換えることができる.では,\relaxが書き変わっている状況で\csnameで未定義の制御綴りを呼び出したらどうなるか? 答えはオリジナルの\relaxが使用される.*4この動作は次のコードで試すことができる.

\show\relax
\def\relax{yukidaruma}
\let\hoge\relax\show\hoge
\expandafter\show\csname fuga\endcsname
\bye

これの出力は次のようになる.

> \relax=\relax.                                
l.1 \show\relax                                 
                                                
?                                               
> \hoge=macro:                                  
->yukidaruma.                                   
l.3 \let\hoge\relax\show\hoge                   
                                                
?                                               
> \fuga=\relax.                                 
<recently read> \fuga                           
                                                
l.4 \expandafter\show\csname fuga\endcsname     
                                                
?                                               

\relaxを代入した\hogeが書き変わった後の定義になっているのに対し,\fugaはオリジナルの\relaxになっている.つまり,あるコードで\relaxを書き換えられてしまっても,それを復元する手段が常にあるということである*5

\globaldefsとの組み合わせ

TeXには\globaldefsというパラメータがあり,このパラメータが正の値の場合,定義や代入は常に\globalが付与されたものとして扱われる.*6すなわち,グルーピングの枠を超えて,マクロが大域的に定義されるようになる.実は,\csnameによる代入はこの\globaldefsに関係なく常にローカルに行われるようである.次のコードを見てみよう.

\globaldefs=42
\show\hoge \show\fuga
\begingroup
\let\hoge\relax
\csname fuga\endcsname
\show\hoge \show\fuga
\endgroup
\show\hoge \show\fuga
\bye

結果は次のようになる.

> \hoge=undefined.              
l.2 \show\hoge                  
               \show\fuga       
?                               
> \fuga=undefined.              
l.2 \show\hoge \show\fuga       
                                
?                               
> \hoge=\relax.                 
l.6 \show\hoge                  
               \show\fuga       
?                               
> \fuga=\relax.                 
l.6 \show\hoge \show\fuga       
                                
?                               
> \hoge=\relax.                 
l.8 \show\hoge                  
               \show\fuga       
?                               
> \fuga=undefined.              
l.8 \show\hoge \show\fuga       
                                
?                               

\hogeは大域的に定義されているのがわかるが,\fugaはそうなっていない.

\afterassignmentとの関係

TeXは様々な場所にフックを仕掛けておくことができる.\afterassignmentは定義や代入の直後に対するフックである.しかし,\csname由来の代入では\afterassignmentは動かない.次のコードを見てみよう.

\def\hook{\show\expandafter}
\afterassignment\hook \let\hoge\relax
\afterassignment\hook \csname fuga\endcsname
\show\hoge
\show\fuga
\bye

実行すると,次の結果が得られる.

> \expandafter=\expandafter.                 
\hook ->\show \expandafter                   
                                             
l.2 \afterassignment\hook \let\hoge\relax    
                                             
?                                            
> \hoge=\relax.                              
l.4 \show\hoge                               
                                             
?                                            
> \fuga=\relax.                              
l.5 \show\fuga                               
                                             
?                                            

\fugaに\relaxが代入されているにも関わらず,\afterassignmentが起動しない.

おわりに

いささか,TeXの闇を見せたような気がするが,\csnameは便利なプリミティブである.ぜひ活用していってほしい.

*1:毎年のように重点テーマと関係ない記事書いていたことに目をつぶりつつ

*2:定義の中に\kernが入っている

*3:なのに,二つの世界の文法がほぼ同一.

*4:某言語のmethod_missing()のようなものには使えない.

*5:どこかの言語でundefinedを入手する手段に似ている

*6:逆に,負なら\globalが無視される.通常は0で,この時は\globalが付与されているかを考慮する