TeXで色々な無限ループ

小ネタ.思うところがあったのでいくつか試してみた.

自身が再帰的に展開されていく例

TeXで無限ループやる場合は大抵これ[要出典].実装法はいくつかある.

単純なマクロ
\documentclass{article}
\begin{document}
\newcommand\hoge{\hoge}
\hoge
\end{document}

よくやる.

\aftergroup
\documentclass{article}
\begin{document}
\newcommand\hoge{{\aftergroup\hoge}}
\hoge
\end{document}

\aftergroupにより,\hogeが波閉じ括弧(グループの終了)の直後に挿入される.結構使う.

\afterassignment
\documentclass{article}
\begin{document}
\newcount\fuga
\newcommand\hoge{\afterassignment\hoge\fuga=0 }
\hoge
\end{document}

\aftergroupと同様に,代入の終了時に\hogeが挿入される.

垂直モードに移行しない\par

\documentclass{article}
\begin{document}
\def\par{}
hoge
\end{document}

どこで見たか思い出せないけど,割と面白い例.TeXは\parを挿入することで,垂直モードに移行しようとする.オリジナルの\parには垂直モードに移行する機能があるが,この例では,\defすることによってその機能を奪っている.結果,垂直モードに移行できない.で,水平モードのままなのでTeXが\parを挿入し,……を無限に繰り返す.なぜこんな仕様になってしまったんだ.

追記: あった
水平モードにおける垂直モード用グルーと\par

TeXだってテスト書きたい(続)

前回は,LaTeX単体テストを書くためのスタイルファイルであるqstest.styを紹介したが,今回はそれをJenkinsと連携させる方法を述べる.

Jenkinsからqstest.styの出力結果は直接読み取れない.テスト結果ならJUnitなどが吐き出すXML文書と同様の形式である必要がある.しかし,qstest.styを書き換えるのはもっと面倒である.なので,

  1. latexでqstest.styの形式で記述されたテストコードを処理し
  2. 結果をJUnitが吐き出すXML文書の形式に変換する

Pythonスクリプトを書いた.ここに公開している.変換自体は単純で,特筆することはないので省略.jenkins-qstest.pyがスクリプト本体で,次のように利用する.

python3 jenkins-qstest.py example.tex

実行するとexample.xmlができるので,これをテスト結果としてJenkinsに読ませればOK.標準でlatexコマンドを使用するようになっているが,その辺はオプションでコントロール可能にしてある.

次のテストファイ

\documentclass{article}
\usepackage{qstest}
\IncludeTests{*}
\LogTests{lgout}{*}{*}
\begin{document}
\begin{qstest}
\begin{qstest}{Example}{label}
 \Expect*{\the\numexpr1+2}{3}
\end{qstest}
\begin{qstest}{Example-fail}{label}
 \Expect*{\numexpr1+2}{3}
\end{qstest}
\end{qstest}
\end{document}

で実行した結果がこちら.



やったぜ.

ちなみに,これはLISP on TeXの今後の開発で利用するために作成したスクリプトで,実際に運用を開始している.

pdfTeXじゃないけどGhostscriptさえあれば画像は埋め込めるよねっ

2015/1/24 16:48 GhostscriptをGhostScriptと記述していました.お詫び申し上げます.

はじめに

こんなことTeXユーザの集いであった.そのなかで,TeXの限界というタイトルでいくつかのトピックが示された.私の知る限り,TeXチューリング完全なのだから,原理的に計算可能ならどうにかなるだろうと思った.実際,「え,list環境再実装すれb……」などの容易に実現できそうな項目もある.ただ,一つのトピックに目を奪われた.

画像のBase64埋め込みの非対応

確かに,TeXでバイナリを扱うのは面白そうな課題である.

やってみた.

といっても,Base64はまだ実現していないので,画像埋め込みをするお話*1

技術概要

某奥底にあるように,LuaTeXのような特殊な場合を除き,TeXエンジンでバイナリファイルを書くことは困難である.よって,TeXでバイナリで与えられる画像データを直接扱うのは不可能である.すなわち,テキストで書き出して,そのバイナリへの解釈をTeXの外部に任せることになる.さて,どうしようかと悩んだところで,ふと思いついた.EPSに出力して\includegraphicsで読めばいいじゃないか,と.EPSはテキスト形式なのでTeXから出力できる.また,ラスタ画像を埋め込むための命令もある.

作成した機能は,LaTeXのパッケージとして動作するようにした.そのパッケージ,binaryimage.styはここに置いておいた.このパッケージは,次のようにして利用する.

\documentclass{article}
\usepackage[dvipdfmx]{graphicx}
\usepackage{binaryimage}
\begin{document}
  ...
  \includebinary[size=wxh,option={width=6cm, clip}]<<バイナリの16進数表現>>\endbinary
  ...
\end{document}

画像を入れるために,includebinaryという命令を用意している.この命令は,オプションにsize(必須)とoptionをとる.sizeはwxhの形で幅と高さのピクセル数を与える.例えば,幅400px,高さ300pxの画像ならばsize=400x300とする.optionはこの命令によって自動的に挿入される\includegraphicsへ与えるオプションである*2.オプションの後には画像のバイナリを16進数表現したものを入れる.このバイナリは,JPEGなどの画像形式をそのまま入れるのではなく,「左上から右へ,右端まで行ったら次の行の左端へ」という順で対応するピクセルのRGBをそれぞれ1Byteで表現したものである.こんなPythonコードで変換できる*3

# -*- coding:utf-8-unix -*-
from PIL import Image
import sys


def main():
    img = Image.open(sys.argv[1])
    for j in range(img.size[1]):
        for i in range(img.size[0]):
            print("{:02X}{:02X}{:02X}".format(*img.getpixel((i,j))), end="")
        print()
    

if __name__ == "__main__":
    main()

実行すると,なぜか-binaryimage<0からの通し番号>.epsというファイルが生成され,\includegraphicsされる.epsファイルの中身は次のようになっている.

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 w h
/DeviceRGB setcolorspace
1 1 scale
<<
/ImageType 1
/Width w
/Height h
/BitsPerComponent 8
/Decode [0 1 0 1 0 1]
/ImageMatrix [w 0 0 h neg 0 h]
/DataSource currentfile /ASCIIHexDecode filter
>>
image

バイナリの16進数表現

ここで,w,h,および「バイナリの16進数表現」には\includebinaryで指定した値が入る.

これらの処理の結果,画像がtexファイルに埋め込まれたように見える.サンプルはここ(ソース結果).

実装小話(急カーブ注意×1)

ここで,binaryimage.styを実装するにあたり利用したテクニックを紹介する.

%の出力

TeXにおいて,%はコメントの開始文字なので,単純に出力できない.さらに,今回の場合は,\%も利用することはできない.\write(ファイル出力)のタイミングで\%が処理できないからである.そこで,binaryimage.styでは\lccodeを使ってコメント開始の役割を持たない%を作り出すことにした.

次のコードを見てみよう.

\lccode`+=`\%%
 \lowercase{\def~{+}}%

\lccodeはある文字に対して,その小文字の文字コードを指定するプリミティブである.上記のコードの1行目では,「+」の小文字に「%」を指定している.そして,\lowercaseを用いて「\def~{+}」の部分を小文字化する.これにより「\def~{%}」となるのだが,このときのカテゴリーコード(文字の役割)は変換前のものが使用されるため,「%」はカテゴリーコード12,すなわち一般の文字として扱われる.後は,%を出力したいところに~を書けば,~が展開されて%が出力される.

長いバイナリ入力への対応

\binaryimageでは,その用途の関係上,長大な入力を必要とする.しかし,TeXのマクロの引数として与えられる文字列はそんなに大きくない.そこで,今回は\edefを使って長大な入力を受け付けるようにした.

\binaryimageは,最初,オプション部分しか読まないようになっている.そして,\binaryimageを展開すると,末尾は次のようになる.

\edef\@bin@data{\ifnum`}=0\fi
       image^^J^^J

\@bin@dataの定義になっているように見えるが,閉じ波括弧が「\ifnum`}=0\fi」となっている.この部分が展開され,空になるので,上記のコードは

\edef\@bin@data{image^^J^^J

という閉じ波括弧がない未完成の\edefになる.いわゆる\ifnumトリックである.ここから,<<バイナリの16進数表現>>が読み込みおよび展開されていき,\endbinaryに達する.\endbinaryを展開すると,その先頭は次のようになっている.

\ifnum`{=0\fi}% end of \edef

これは,先の\ifnumトリックの対をなすコードで,閉じ波括弧単体となる.結果,\@bin@dataの定義が完成する.

この後,\endbinaryによってepsファイルの出力と\includegraphicsが行われ,画像が読み込まれる.

副次効果

binaryimage.styを通じて,Ghostscriptの闇などに触れてしまった.ここでは,その一部を紹介しよう.

Ghostscript9.10と9.15におけるヘッダ解釈の差異

現在,binaryimage.styが吐き出すepsファイルの1行目は「%!PS-Adobe-3.0 EPSF-3.0」となっている.実は,つい最近まで「%!PS-Adobe-3.0」という誤った出力をしていた.後者の出力の場合,Ghostscriptのバージョンが9.10なら動作するが9.15だと読み込めないという謎の事象に見舞われた.本記事の投稿が遅れた最大の理由がこれである.

epstopdfのMSYS対応

graphicxのドライバをpdftexにしたところ,指定した覚えのないディレクトリがエラーメッセージに出るという不可解な現象に遭遇した.中身を開いて調べたところ,epstopdfがMSYSに対応していないことが原因だと発覚した.仕方がないので自分で直し,フォーラムに投げた.現在は,Akira Kakutoさん,KUROKI Yusukeさん,およびKarl Berryさん*4のご尽力により,改良されたMSYS対応コードが本家に取り込まれたはずである*5

関連研究

ZRさんによって,このような記事がすでに発表されている.これはpdfTeXの機能を用いて,直接pdfファイル中に画像を埋め込む手法になっている.Ghostscriptに悩まされない分,綺麗な手法といえるが,pdfTeX依存になるため,TeXエンジンを複数使い分ける場合などに支障が出る.

今後の課題

Base64デコーダの実装が今後の課題にある.今回の使用目的に対してならいくつか実装アプローチがある.完全展開可能なデコーダTeX上に実装するか,PostScript側にデコーダを実装するかである.どちらもそれなりに苦行なので,そもそもbinaryimage.styに需要があるなら実施してもいいかもしれない*6

*1:某奥底に先を越されたのはGSの謎仕様のせい.

*2:命名規則が酷いのは仕様

*3:これが初Pythonであることを告白しておく

*4:著名人しかいない怖さ

*5:なお,全部rungsでもいいのではないかという提案を投げるのは私の仕事になっているが未実施である.仕事が早いのも見習いたい点である

*6:少なくとも,作者である私は使用する予定はない

TeXだってテスト書きたい

これはTeX & LaTeX Advent Calendar 2014,14日目の記事です.
13日目はneruko3114さん,15日目はh-kitagawaさんです.

はじめに

TeXでマクロを記述するにあたり,問題になるのはマクロが想定した動作をするか否かである.他のプログラミング言語*1と比較しても,TeXキチガイとしか言いようのな文法と評価規則をもち,思ったような動作をさせるのに苦労する.LaTeXのパッケージを公開するなら,せめてテストくらいしておきたい.特に,マクロでよくわからないことを実装しようとする私のような人間には切実な問題である.実は,これをサポートするパッケージがある.今回は,そのパッケージqstest.styを紹介する.

qstest.styとは

CTANにも登録されているLaTeXパッケージで,LaTeXにunit testを提供する.2007年のTUGboat記事がある.

基本的な使い方は,次のソースコードのようになる.

\documentclass{article} % 実際は何でもよい
\usepackage{qstest}
\IncludeTests{*}
\LogTests{lgout}{*}{*}
\begin{document}
  \begin{qstest}{Example}{label}
     \Expect*{\the\numexpr1+2}{3}
  \end{qstest}  
\end{document}

まず,qstest.styを使うために\usepackage{qstest}とする.

\IncludeTestsは実際に実施するテストに付加されたキーワード*2を指定する.テスト用に別ファイルを用意するなら*,すなわちすべてのテストを実施するように指定するのがよいだろう.テストを別ファイルに定義しておき,時間のかからないテストだけ実施するようなことも可能である.

次の\LogTestsはログファイルへの出力設定を行う.第一引数はログファイルの拡張子で,この例の場合,ログは\jobname.lgoutというファイルに出力される.第二引数は成功したテストのうち,ログへ出力するもののキーワード,第三引数は失敗したテストのうちログへ出力するもののを指定する.大抵の場合,すべてのログを記録するため,\LogTests{lgout}{*}{*}としておけばよいだろう.

テストケースはqstest環境に記述する.環境に与える引数は,テストケース名とキーワード*3である.

qstest環境中では,値の確認を\Expectを使って行う.\Expectは\Expect{確かめたい値}{予想する結果}のように使用する.要はassertである.引数を展開したい場合は,開き波括弧の前に*をつける.先に挙げた例でも,\the\numexpr1+2を展開するために*を使用している.もちろん,予想する結果の方を展開することもできる.ただし,展開には\edefを使用しているため,代入などの副作用をもっている場合には,注意が必要である.

値の確認に関しては,\Expect以外にもマクロが用意されている.確かめたい長さが特定の範囲に含まれるかを確認する\InRangeなど,TeXに特化したものもあるので,興味のある人はマニュアルを読んでみるといいだろう*4

先の例を実行すると,ログファイルに次の出力を得る*5.これは,Exampleテストをパスしたことを表している.

Passed: Example


テストにパスしなかった場合も示しておこう.次の例を考える.

\documentclass{article}
\usepackage{qstest}
\IncludeTests{*}
\LogTests{lgout}{*}{*}
  \begin{document}
    \begin{qstest}{FailExample}{label}
    \Expect*{\the\numexpr1+2}{hoge}
  \end{qstest}
\end{document}

これを実行すると,コンソールに予想していた値と実際の値が出力される.

! Package qstest Error: Failed: FailExample
 \Expect: \the \numexpr 1+2
 <hoge
 >3.

この後,実行を続けるか否かの入力を求められる*6ので,ENTERを押して実行を続け,ログファイルに次の出力を得る.

Failed: FailExample
 \Expect: \the \numexpr 1+2
 <hoge
 >3

副作用のみが重要なマクロに対する知見

さて,\Expectで展開しようとした場合,\edefを使うというということは先に述べた.これにより,\Expectでは内部で\defなどの代入を伴うプリミティブを単純にテストできない.LISP on TeXのテストコードを記述しようとした際,これは大きな問題だった.ここで,私はLISP on TeXのマクロのほとんどが空のトークンに展開され,副作用のみが重要であるという特徴を利用した.

LISP on TeXのコードでは大きすぎるため,次の例を考える.マクロ\hogeは二引数をとり,\fugaに第一引数と第二引数の和を代入することを目的としたマクロである.当然,展開結果には空のトークン列であることを想定している.

\newcount\fuga
\newcommand{\hoge}[2]{
  \fuga=#1
  \advance\fuga by #2
}

これをテストするために

\Expect*{\hoge{4}{2}\the\fuga}{6}

と記述することはできない.\fugaへの代入や\advenceが展開できないため,次のようにテストをパスできないことがログに残る.

Failed: Example
 \Expect: \hoge {4}{2}\the \fuga 
 <6
 > \fuga =4 \advance \fuga by 2 0

ここで,\fugaに和が代入されていること,\hogeは空のトークン列に展開されるという性質をテストするには,次のようにすればよい.

 \setbox0=\hbox{\hoge{4}{2}\xdef\fugafuga{\the\fuga}}
 \Expect*{\fugafuga}{6}
 \Expect*{\the\wd0}{0.0pt}

わざと水平ボックス内でマクロを展開し,結果を大域環境に出してやる.そして,出した結果が想定した結果であるかを\Expectでチェックする.空のトークン列に展開されているかどうかは作った水平ボックスの幅が0.0ptかどうかを確認すればよい.この手のマクロは空白トークンを入れてしまいおかしくなることがほとんどであるため,これで十分である.

さて,実際にテストしてみよう.次のようなログを得ることができる.

Failed: Example
 \Expect: \the \wd 0
 <0.0pt
 >3.33333pt

あれ……

そう,\hogeを定義する際,開き波括弧の後の改行をコメントアウトしていないため,空白トークンが出力されてしまっているのである.\hogeを次のように修正して再度テストする.

\newcount\fuga
\newcommand{\hoge}[2]{%
  \fuga=#1
  \advance\fuga by #2
}

テストを実行して次の結果を得る.

Passed: Example

世界が平和になった.

課題

副作用を持ちつつトークン列を出力する場合,どうしたらいいのかまだ知見が得られていない*7

*1:比較対象として妥当かは議論しないこととする

*2:正確にはキーワードのパターン

*3:ストファイルを別で作成している場合,特に入れるものがないので,私はテストするマクロのコントロール・シーケンスを入れるようにしている

*4:そんなに長くない

*5:誰かJenkins用のXMLに変換してくれないかなー(棒

*6:LaTeXのいつものあれ

*7:\unhboxとかでいいのかなぁ

LISP on TeX v1.3

久しぶりの更新はLISP on TeXバージョンアップな話.更新点は次の2つ.

  1. 環境のバグ修正
  2. one shot continuationsの実装

詳細を示す.

環境のバグ修正

これは,Twitterここで示されたバグ.要はローカル変数のスコープがおかしくなってしまっている.

簡単な例を示すと,

\lispinterp{((\lambda (\n) ((\lambda (\n) \n) :1)) 'hoge')}

の結果が:1でなく'hoge'になる.原因はv1.2で追加した,環境をマクロに展開する処理.旧コードは次の通り.

\def\@lisp@expand@env@last{\@lisp@expand@env@last}

\def\@lisp@expand@environment#1#2{%
  \ifx#1\@lisp@expand@env@last
    \let\@@next\relax
  \else
    \expandafter\def\csname @lisp@env@\string#1\endcsname{#2}%
    \let\@@next\@lisp@expand@environment
  \fi
  \@@next}

環境\argi{val1}\argii{val2}...を順次展開していくコードになっているが,環境は内部から順に並んでいるので,この順で展開すると外側の束縛のほうが優先されるという……

そこで,これを次のように修正

\def\@lisp@expand@environment#1#2#3\@lisp@expand@env@last{%
  \def\@@tmp@lisp@env{#3}%
  \def\@@tmp{\expandafter\def\csname @lisp@env@\string#1\endcsname{#2}}%
  \ifx\@empty\@@tmp@lisp@env
    \let\@@next\relax
  \else
    \def\@@next{\@lisp@expand@environment#3\@lisp@expand@env@last}%
  \fi
  \expandafter\@@next\@@tmp}

\@@tmpを定義して展開順序を制御するようにした.

one shot continuationsの実装

スタックを陽に扱っていない関係上,一級継続は実装が困難だが,one shot continuations位なら実装できる.実際,TUG2013で実装宣言していたので,このv1.3に導入した*1

one shot continuationsは,要はJavaなどの言語における例外処理みたいな機能を提供するもの.次のように利用する.

(\callOCC (\lambda (\c) ...))


\callOCCがone shot continuationに必要な継続オブジェクトを生成する関数である*2.\callOCCは一引数関数を引数に取り,その関数を継続オブジェクトに適用する.継続オブジェクトもまた一引数関数で,呼び出すと,以降に書いてある式は評価されず,先の式の評価結果が継続オブジェクトを適用した値になる.なお,継続オブジェクトが適用されなかった場合,クロージャの評価結果が\callOCCの評価結果になる.

コード例を示そう.

\documentclass{article}
\usepackage{lisp-on-tex}
\begin{document}
  '\lispinterp{
    (\callOCC (\lambda (\c)
      (\begin
        (\texprint 'executed')
        (\c ())
        (\texprint 'not executed'))))
  }'
\end{document}

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

(\texprint 'not executed')が実行されていないことがわかる.もっと複雑な例を見たい場合,v1.3にnqueen.texを追加したので,そちらを見てほしい.

補足

ある程度継続を知っている人向け.この機能は一級継続でないため,継続オブジェクトは\callOCCの実行から抜けると無効になる.抜けた後に継続オブジェクトを何かに適用した場合,その動作は保証されない.

*1:実際は5月末くらいに実装が終わっていたが,テストコードの整備をしていたため,CTANへのアップロードを保留していた.今回のバグを受けて,ついでにアップロードしたといった方が正しい.

*2:本当は\call/1ccとか使いたいのだが,/も1もカテゴリーコードの関係上使えない

LISP on TeXを作る(評価器編2)

前回の続き.evalの本丸である関数適用の説明に入ろう.

consセルの評価

関数適用が起こるタイミングとはすなわち,consセルが評価されるタイミングである.まずは,consセルのデータ構造を確認しておこう.consセルは,次の形をもつトークン列である.

\@tlabel@cons{\carXX\cdrXX}

\carXXがCAR,\cdrXXがCDRを表す制御綴りであり,展開するとLISP on TeXにおけるオブジェクトの形式をしたトークン列を得ることができる.

consセルの評価規則は簡単で,

  1. CARを評価して,適用可能なオブジェクトを得る
  2. CDRを引数として,さきほど得られたオブジェクトに適用する
    • 関数やクロージャの場合は各引数が評価される.マクロの場合はそのまま引き渡す

だけである.consセルの評価規則の実装,\@eval@consを確認しよう.

\def\@eval@cons#1#2#3{\@@eval@cons#1{#2}#3}
\def\@@eval@cons#1#2#3#4{%
  \expandafter\@eval#1{#3}\@temp@i
  \def\@temp@ii{}% init
  \expandafter\@flatten@args#2\@temp@ii
  \expandafter\expandafter\expandafter\@@select@apply@pre\expandafter\@temp@i\@temp@ii\@{#3}#4}
\def\@@select@apply@pre{\expandafter\@@select@apply}

\@eval@consの引数は,順に「CAR」,「CDR」,「現在の環境(大域環境との差分)」,「制御綴り」である.第4引数の制御綴りに評価結果が格納される.

まず,\@eval@consでは,現在の環境のもとでCARを評価する.\expandafter便利.評価結果は,\@temp@iに格納される.次に,\@flatten@argsで引数をTeXで扱いやすい形に整形する.現時点での引数部分,すなわちCDRは単一の制御綴りであり,すべての引数を得るには順次それを展開していかないといけない構造になっている.正直言って,TeX側で扱うには不便である.\@flatten@argsでは,評価前の引数を列を次の形(整形形式)に整形する.

\csI{valI}\csII{valII}...\csN{valN}

ここで,\csI, \csII, …, \csNは引数1, 引数2, …, 引数Nの型ラベル,valI, valII, …, valNがデータである.\flatten@argsのコードを示す.

\def\@flatten@args#1#2#3{%
  \ifx#1\@tlabel@cons
    \let\@flatten@next\@@flatten@next
  \else\ifx#1\@tlabel@nil
    \let\@flatten@next\@@flatten@fin
  \else
    \errmessage{LISP on TeX [internal apply]: Invalid args}%
  \fi\fi
  \@flatten@next#2#3}

\def\@@flatten@next#1#2#3{%
  \expandafter\expandafter\expandafter\def
  \expandafter\expandafter\expandafter#3%
  \expandafter\expandafter\expandafter{\expandafter#3#1}%
  \expandafter\@flatten@args#2#3}

\def\@@flatten@fin#1{}


要は,consセルを順次たどっていっているだけである.ここで,\@flatten@argsはconsセル全体が行儀の良いリストか否かの判定も行っている.整形した結果は.\@flatten@argsの第3引数に与えられた制御綴り,\@eval@consでは\@temp@iiに格納される.ここで,引数はまだ評価されていないことに注意する.

最後に,\@eval@consはCARの評価結果(\@temp@i)を引数(\@temp@ii)に適用する.すなわち,次のトークン列になるように展開される.

\@apply@XXX{valCAR}\csI{valI}\csII{valII}...\csN{valN}\@{env}\targetReg

\@apply@XXXはCARを評価結果が持つ適用のためのマクロ,valCARはCARを評価した結果のデータ,\csI{valI}\csII{valII}...\csN{valN}が引数,\@は引数の終端,envは環境,\targetRegは適用結果を格納するための制御綴りである.

組み込み関数の適用

残りは,組み込み関数,クロージャ,マクロ,およびスペシャルフォームの適用がどのように実装されているのかを確認するだけである.まずは,組み込み関数から見ていく.

適用規則に入る前に,次のコードから,組み込み関数のデータ構造を確認しておこう.

\@tlabel@func{\cs}

\csが組み込み関数のTeX実装で,次のように呼び出されることを前提として実装されていなければならない.

\cs\targetReg\csI{valI}\csII{valII}...\csN{valN}\relax\relax

\targetRegが適用結果格納を格納する制御綴り,\csI{valI}\csII{valII}...\csN{valN}が評価済みの引数である.可変長引数に対応するため,引数列のラストは\relax\relaxが付加されている.当然,\relaxは展開されても何の効果もないため,実装側はこれを無視してもよい.例えば,LISP on TeXの等価判定である\=は次のように実装されている.

%equality
\addassoc\@globalenv\={\@tlabel@func{\@lisp@equal}}
\def\@lisp@equal#1#2#3#4#5{%
  \gdef#1{\@tlabel@bool{f}}%
  \ifx#2#4%
    \def\@@temp@eqchecki{#3}%
    \def\@@temp@eqcheckii{#5}%
    \ifx\@@temp@eqchecki\@@temp@eqcheckii\gdef#1{\@tlabel@bool{t}}\fi
  \fi}

\@lisp@equalがその実体で,LISP on TeX側での第1引数(#2#3)と,第2引数(#4#5)を\ifxで等価判定し,その結果に応じて#1に偽(\@tlabel@bool{f})もしくは真(\@tlabel@bool{t})のいずれかが格納されるようになっている*1

組み込み関数の適用規則,\@apply@funcは次のように実装されている.

\def\@apply@func#1#2\@#3#4{%
  \def\@temp@i{}%
  \@apply@eval@args\@temp@i{#3}#2\relax\relax
  \expandafter\@apply@func@next\expandafter{\@temp@i}{#1}{#3}#4}
\def\@apply@func@next#1#2#3#4{\@@apply@func{#2}#1\@{#3}#4}

まず,\@apply@funcは,\@apply@eval@argsを用いて,LISP on TeXの意味での引数(#2)を評価し,整形形式にする.その結果は\@temp@iに格納される.\@apply@funcは最終的に次のトークン列に展開される.

\@@apply@func{\cs}\csI{valI}\csII{valII}...\csN{valN}\@{env}\targetReg

\csは組み込み関数の本体,\csI{valI}\csII{valII}...\csN{valN}が評価済みの引数,\@は引数の終端,envは環境,\targetRegは適用結果を格納するための制御綴りである.形式はほぼ\@apply@funcと同じだが,引数列が評価されている点で異なる.このように適用規則を分解したのは,\apply関数の実装のためである.\applyでは引数を評価しないで関数適用する必要がある,そのため,\applyでは\@@apply@XXXを直接呼ぶようになっている.

\@@apply@funcの実装を見てみよう.

\def\@@apply@func#1#2\@#3#4{\gdef\@@tco{#1#4#2\relax\relax}\aftergroup\@@tco}

要は,先に示した

\cs\targetReg\csI{valI}\csII{valII}...\csN{valN}\relax\relax

と言う形に展開されるのだが,ここではそれを\@@tcoというマクロに定義して,それを\aftergroupの引数にしている.これは,末尾呼び出しの最適化のためである.\aftergroupは現在のグループの終了時に,指定されたトークンを指定した順で出力するためのプリミティブである.評価器編1で,LISP on TeXのコールスタックはグループで表現されていることを説明した.すなわち,展開中のトークン列は

\gdef\@@tco{#1#4#2\relax\relax}\aftergroup\@@tco\endgroup

となっている.\aftergroupにより,関数適用はグループの終了直後,すなわち\endgroupの終了後に実行されることになる.これにより,無駄に\endgroupが末尾に溜まっていくことを防いでいる.

これらにより,目標としていたトークン列が展開され,組み込み関数の適用が行われる.

クロージャの適用

クロージャの場合も,組み込み関数とほぼ変わらない.まず,次のコードでクロージャのデータ構造を確認しておこう.

\@tlabel@closure{{bind}{env}\bodyLabel{bodyValue}}

bindは仮引数を表すトークン列,\bodyLabel{bodyValue}がクロージャの本体,envがクロージャ作成時の環境(大域環境との差分のみ)である.bindは次の形式を持つ.

\csI\csII...\csN:\csRem

ここで,\csI, \csII, …, \csN, \csRemが仮引数に使われているシンボルである.\csRemはリストにバインドされる特別なシンボルであり,仮引数の形式が単一のシンボルもしくは行儀の悪いリストの時に使用される.使用されない場合,すなわち仮引数リストが行儀の良いリストだった場合,\@@unusedという特別なシンボルが与えられる*2

さて,クロージャの適用規則である\@apply@closureを見てみよう.

\def\@apply@closure#1#2\@#3#4{%
  \def\@temp@i{}%
  \@apply@eval@args\@temp@i{#3}#2\relax\relax
  \expandafter\@apply@closure@next\expandafter{\@temp@i}{#1}{#3}#4}
\def\@apply@closure@next#1#2#3#4{\@@apply@closure{#2}#1\@{#3}#4}

\@apply@funcと同様に,\@apply@eval@argsで引数を評価し.\@@apply@closureを展開する.

\@@apply@closureの実装は次のようになっている.

\def\@@apply@closure#1#2\@#3#4{\@@apply@closure@next#1#2\@#4}
\def\@@apply@closure@next#1#2#3#4#5\@#6{%
  \def\@temp@env{}%
  \@@apply@create@env\@temp@env#1#5\relax\relax
  \expandafter\gdef\expandafter\@@tco\expandafter{%
    \expandafter\@@eval@envcs\expandafter{\@temp@env#2}#3{#4}#6}%
  \aftergroup\@@tco}

まず,\@@apply@create@envを使って実引数割り当てを行っている.すなわち,仮引数を表すトークン列と実引数から,整形形式のトークン列を得る*3.\@@apply@funcと同様,\aftergroupを利用して末尾呼び出しの最適化を行いつつ,これは次のトークン列に展開される.

\@@eval@envcs{env1env}\bodyLabel{bodyValue}\targetReg}

env1は仮引数を実引数に束縛した環境,envがクロージャ作成時の環境(大域環境との差分のみ),\bodyLabel{bodyValue}がクロージャの本体,\targetRegが評価結果を格納するための制御綴りである.これは更に,次のように展開される.

\@eval\bodyLabel{bodyValue}{env1env}\targetReg

これにより,クロージャの本体が適切な環境で評価される.

マクロの適用

次に,マクロの適用規則を見ていこう.その前に,マクロのデータ構造を確認する.

\@tlabel@closure{{bind}\bodyLabel{bodyValue}{env}}

実はクロージャと全く同一の構造を持っている.なお,envが入っているが,\defmacro自体の実行タイミングが大域環境の直下しかないので,空になっていることに注意する*4

これまでで,\apply@XXXの目的が「引数を評価する」ことに,察しのいい読者なら気づいただろう.引数を評価する必要のないマクロでは,\@apply@macroと\@@apply@macroは同一になっている.

\let\@apply@macro\@@apply@macro

\@@apply@macroは,\@@apply@closureと同様の動きをする.コードを示す.

\def\@@apply@macro#1#2\@#3#4{\@@apply@macro@next#1#2\@{#3}#4}
\def\@@apply@macro@next#1#2#3#4#5\@#6#7{%
  \def\@temp@env{}%
  \@@apply@create@env\@temp@env#1#5\relax\relax
  \expandafter\gdef\expandafter\@@tco\expandafter{%
    \expandafter\@@eval@envcs\expandafter{\@temp@env#2}#3{#4}#7%
    \expandafter\@eval#7{#6}#7}%
  \aftergroup\@@tco}

まず,\@@apply@create@envで仮引数を束縛する.その後,その環境でマクロの本体を評価して,マクロの仮引数をすべて実引数に置き換える.更にその結果を評価して,マクロの展開が修了する.なお,ここでも末尾呼び出しの最適化がかかる.

スペシャルフォームの評価

最後はスペシャルフォームの評価である.LISP on TeXでは,各スペシャルフォームに別々の型ラベルを与えている.すなわち,スペシャルフォームSに対し\@tlabel@Sが定義されている.すべてのスペシャルフォームについて説明するのは冗長なので,ここでは\if,\define,\quoteの実装を示す*5

\if

\ifの評価規則\@apply@ifは次のように実装されている.

\def\@apply@if#1#2#3#4#5#6#7\@#8#9{%
  \@eval#2{#3}{#8}#9%
  \expandafter\@apply@if@next#9#4{#5}#6{#7}{#8}#9%
  \aftergroup\@@tco
  }
\def\@apply@if@next\@tlabel@bool#1#2#3#4#5#6#7{%
  \let\@@next\relax
  \ifx#1t%
    \let\@@next\@apply@if@next@t
  \else\ifx#1f%
    \let\@@next\@apply@if@next@f
  \else
    \errmessage{LISP on TeX [if]: Invalid boolean. It's BUG. Please report.}%
  \fi\fi\@@next{#1}{#2}{#3}{#4}{#5}{#6}{#7}}
\def\@apply@if@next@t#1#2#3#4#5#6#7{\gdef\@@tco{\@eval#2{#3}{#6}#7}}
\def\@apply@if@next@f#1#2#3#4#5#6#7{\gdef\@@tco{\@eval#4{#5}{#6}#7}}

まず,第1引数が評価されて\@apply@if@nextが展開される.\@apply@if@nextでは,第1引数の評価結果であるbool型オブジェクトの中身を見て,真なら第2引数を,偽なら第3引数を評価する.なお,第1引数の評価結果であるオブジェクトがbool型でない場合,TeX側でエラーになる.

\define

\defineは,第2引数を評価して,大域環境に{第1引数→第2引数の評価結果}を追加するだけである.コードを示す.

\def\@apply@define#1\@tlabel@symbol#2#3#4\@#5#6{%
  \@eval#3{#4}{#5}#6% define does NOT use local environment
  \expandafter\addassoc\expandafter\@globalenv\expandafter#2\expandafter{#6}%
  \gdef#6{\@tlabel@nil{}}}

まず,第2引数(#3#4)を評価する.このとき,大域環境との差分(#5)は空であるはずだが,LISP on TeXではそれを確認していない.その後,{第1引数→第2引数の評価結果}を\@globalenvを用いて大域環境(\@globalenv)に追加する.\define全体の評価結果はnilになることが,この実装からわかる.

\quote

\quoteは第1引数をそのまま返すだけである.コードも簡単で,次のようになっている.

\def\@apply@quote#1#2#3\@#4#5{\gdef#5{#2{#3}}}

終わりに

これをもって,LISP on TeXの実装のほとんどが説明された.誰でもLISP on TeXが実装できるようになったわけである.

現在,LISP on TeXではqstestパッケージを使ったテストコードの整備を進めている.諸事情により,これが終了し次第,one-shot continuationの実装に入る予定である.また,GCも実装可能な目処が立ってしまったので,実装するかもしれない.

2014/05/18 クロージャオブジェクトの表現が間違っていたので修正

*1:この実装,実は引数が規定よりも多かった時に何の処理もしていない.おそらく,型ラベルが展開されて予期しない結果を得ることになる.現状の改善点のひとつだが,どうせエラーなので私の中での優先度は低い.

*2:すなわち,LISP on TeXでは仮引数かつそれにリストが割り当てられる場合,\@@unusedというシンボルを使用することができない.これも修正対象ではあるが,割りと面倒なので放置していた.

*3:割りと複雑なマクロにしてしまったので詳細は省略する

*4:実際は,大域環境直下以外の実行を禁止しているわけではない.やったら謎の環境で展開されるマクロが完成する.

*5:実装が複雑なスペシャルフォームは\lambdaなどだが,それらの解読は読者への課題とする(マテ

LISP on TeXを作る(評価器編1)

LISP on TeXネタの最終消費作業*1.今回は評価器編.ここが本丸と言いたいところだったが,あまりに分量が多かったため,評価器編は2回に分割する.前編はevalの基本構造,後編は関数適用がメインである.

データ構造と評価の基本構造

再確認になるが,LISP on TeXで用いられるデータはすべて,次の構造をしている.

\@tlabel@hoge{contents}

ここで,制御綴り\@tlabel@hogeは型の情報を示すラベル(型ラベル)である.波括弧の中身はデータ本体である.
\@tlabel@hogeを展開すると,次のトークン列

\@eval@hoge\@apply@hoge\@@apply@hoge

を得ることができる.最初の制御綴りが,型hogeの値に対するevalを実現しているマクロ,後者2つがapplyを実現するためのマクロになっている.

評価器の起動

データ構造がわかったところで,実際にLISP on TeXのオブジェクトがどのように評価されていくのかを示す.まずは,評価器の起動部分である.評価器の起動マクロ\lispevalは,各種初期化処理を実行した後,\@eval@hogeを取得し展開するだけである.コードを示す.

\def\lispeval#1#2{% #1 : \cs -> S-exp, #2 : target register
  \gdef\@temp@write@buffer{}%
    \expandafter\@lisp@expand@environment\@globalenv\@lisp@expand@env@last\@lisp@expand@env@last% 
    \expandafter\@eval#1{}#2%
  \@temp@write@buffer}

\def\@eval#1#2#3#4{
  \begingroup
    \@lisp@expand@environment#3\@lisp@expand@env@last\@lisp@expand@env@last
    \expandafter\@@select@eval#1{#2}{#3}#4%
  \endgroup}

\lispevalの引数#1は制御綴りで,展開するとLISP on TeXのオブジェクトになることが期待される.#2も制御綴りで,\lispeval全体の展開が終了したとき,#2には#1を展開したものの評価結果が格納される.

次に,\lispevalの詳細を説明していく.

まず,\lispevalでは\@temp@write@bufferを空列に初期化する.この制御綴りは,その名の通りトークン出力を行うためのバッファとしての役割を持つ.すべての評価が終わった後,\@temp@write@bufferが展開され,LISP on TeXより出力されたトークンが展開されていく.このような処理にしているのは,むやみなマクロ展開により,LISP on TeXの評価に使うトークンを破壊されないようにするためである*2

次の行は,グローバル環境をTeXのマクロとして展開する.例えば,LISP on TeXの変数\hogeが整数の42に束縛されている*3場合,この行を実行することにより,\@lisp@env@\hoge という制御綴り*4を展開すると\@tlabel@int{42}を得ることができるようになる.

そして,#1を展開して補助マクロ\@evalを展開する.\@evalの引数#1は評価対象の型ラベル,#2はデータ本体,#3は評価中の環境*5のうちグローバル部分との差分,#4は評価結果格納先の制御綴りである.

\@evalは,中身全体を\begingroup...\endgroupで囲んでいる.これは,LISP on TeXのコールスタックの実装になっている.TeXには「ローカルな定義」と「グローバルな定義」という便利な機構があって,「ローカルな定義」の場合,定義したものは現在のグループを抜けると破棄される.局所的に同じ名前の制御綴りを使いたいということの多い言語処理系実装のためにあるといっていい機能である(迫真 最後に\@@select@evalを展開して,各データ型の評価規則へと進む.\@@select@evalは型ラベルを展開して得られる3つの制御綴りのうち1つめ,すなわちeval用のものだけを残し他を捨てるためのマクロである.

各データ型の評価用のマクロは,次のようなコンテキストで展開される.

\@eval@hoge{contents}{env}\target

contentsはデータ本体,envは評価中の環境のうちグローバル部分との差分,\targetは評価結果を格納するための制御綴りである.ここはほとんど\@evalと変わらない.

自己評価型フォームの評価

整数型や文字列型など,ほとんどのデータ型は自己評価型フォームである.すなわち評価すると,環境にかかわらずそれ自身が得られる.これは以前にも説明しているが,実に簡単なコードで実現できる.int型を例にとってコードを示す.

\def\@eval@int#1#2#3{\gdef#3{\@tlabel@int{#1}}}

これは,#3をint型ラベル(\@tlabel@int)を付与した#1で定義することを示す.\def(ローカルな定義)ではなく\gdef(グローバルな定義)にしているのは,この代入をスタックの外へ持ち越すためである.

シンボルの評価

シンボルの評価も,環境を見に行くだけで大したことはない.コードを示す.

\def\@eval@symbol#1#2#3{%
  \expandafter\global\expandafter\let\expandafter#3\csname @lisp@env@\string#1\endcsname
  \ifx#3\relax % not found
    \errmessage{LISP on TeX [evaluation of a symbol]: unbound variable...}%
  \else
    \expandafter\@expand@if@mutable#3#3%
  \fi}

ここでは,\lispevalと\@evalによって,現在の環境で定義されている任意の変数\hogeがマクロ\@lisp@env@\hogeというマクロとして利用可能であることを利用している.すなわち,#3に\@lisp@env@\hogeの中身をグローバルに代入している.次の\ifxは変数が未定義であった場合にエラーメッセージを表示するためのものである.else節?で使われている\@expand@if@mutableは参照型*6を展開するためのものである.あまり気にする必要はない.

(続く)

(補足)環境の表現

環境がどのような構造をしているかについて簡単に述べておく.環境は次のようなトークン列になっている.

\vari{val1}\varii{val2}...

変数と値の列になっていて,この例では\variがval1に,\variiがval2に束縛されていることを示す.

*1:実はいろいろあって拡張はする予定はある.one shot continuationとかGCとか

*2:実際の所,簡単に破壊できるのだが……

*3:よく「束縛」の言葉の使い方を間違えるので,これであっているか自信はない

*4:わかりにくいが\hogeに\stringを適用したトークン列を使うのでこれでひとつの制御綴りになる.

*5:環境の説明は補足として末尾に記載している

*6:LISP on TeXの内部的な型のひとつ.表面からは見えないが\defineMや\letMで導入される.この型はあとから中身を変更可能で,\setB(Schemeのset!相当)を実現するのに使う.