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!相当)を実現するのに使う.

LISP on TeXを作る(パーサ編)

LISP on TeXネタを全部消費して,新たなるTeX芸へ向かうためのソリューションその2.今回はパーサ編.要はS式を解釈して前回のデータ構造編で説明したデータへ変換するマクロがどのように構成されているかを示す.これから説明するマクロは,lisp-read.styにそのほとんどが記述されている.

はじめに

実装説明に入る前に,パーサ全体にかかる注意事項を述べておく.まず,LISP on TeXの文法はLL1であるので,字句解析と構文解析をごちゃ混ぜにした実装になっている.また,一部のマクロを除き,パーサのマクロは

\def\controlsequence#1#2{....}

の形で定義されている.#1は制御綴りで,#2は現在解析中の先頭文字である.これらのマクロは,#1の中身を\cs,パースの結果得られたデータ構造を\@tlabel@xxx{hoge}とすると

\cs\@tlabel@xxx{hoge}

と展開される.ちょうど\csを継続としたCPSで記述されていると思えばいい.このようなまどろっこしい構造にしているのは,こちらのほうがTeXの資源消費を減らせるからである.実は,入力全体をマクロに保存しておき,一文字ずつ取ってくるという構造にしていた時期もあるのだが,それだと大量のLISP on TeXコードを投入したときにエラーを吐くことわかり,急遽この構造にした経緯がある*1

対象とするLISP on TeXの文法をそろそろ明示しないといけないので,READMEから文法を抜粋しておく.

   ::=  
            |  
            |  
            | 
            | 
            | 
            | 
            | 
            | 
    ::= (+) | ( . )
     ::= :[TeX's integer]
  ::= '[TeX's tokens]'
  ::= [a control sequence]
    ::= /t | /f
     ::= ()
::= +{::}
 ::= [TeX's tokens]
  ::= [TeX's tokens]
    ::= @[TeX's skip]
   ::= ![TeX's dimen]

は非終端記号,|は選択,+は非終端記号の長さ1以上の列である.[comment]は終端記号のうち,TeX側に依存するので言及をようにしていないことを示す. ::= ... でその非終端記号における規則が導入される.以上の説明に出現しない記号はすべて終端記号である.

コード解説

型による分岐部分

パーサの起点は\@lispreadで定義は,次のようになっている.

\def\@lispread#1#2{\begingroup\@lispread@main#1#2}

\begingroupでローカルな定義を漏れ出さないようにして\@lispread@mainを呼び出すだけである.\@lispread@mainは次で示される.

\def\@lispread@main#1#2{% Define \@@next, the continuation. 
  \if\noexpand#2(% [Branch 1] CONS cell or NIL
    \def\@@next{\@lispread@cell#1}%
  \else\ifcat\noexpand#2\relax% [Branch 2] A control sequence or a control symbol 
    \ifx#2\@end@lispread % [Branch 2-1] EOI
      \def\@@next{\endgroup#1\@tlabel@exception{!Found End of Input!}}%
    \else
      \def\@@next{\@lispread@symbol#1#2}% [Branch 2-2] Symbol
    \fi
  \else\if\noexpand#2'% [Branch 3] String
      \def\@@next{\@lispread@string#1}%
  \else\if\noexpand#2/% [Branch 4] Boolean
    \def\@@next{\@lispread@bool#1}%
  \else\if\noexpand#2:% [Branch 5] Integer
    \def\@@next{\@lispread@int#1}%
  \else\if\noexpand#2!% [Branch 6] Dimension 
    \def\@@next{\@lispread@dimen#1}%
  \else\if\noexpand#2@% [Branch 7] Skip
    \def\@@next{\@lispread@skip#1}%
  \else\if\noexpand#2+% [Branch 8] call a Reader Module 
    \def\@@next{\@lispread@module#1}%
  \else % Otherwise -- parse error
    \errmessage{LISP on teX [read]: no such type start with \noexpand#2}%
  \fi\fi\fi\fi\fi\fi\fi\fi
  \@@next}

この部分では,先頭の1文字を見て次に呼び出すべきマクロを\@@nextに収める.LISP on TeXでは,先頭の1文字を見るだけで型をほぼ識別できるようにデザインしているため,このようマクロで動く.分岐パターンは次のとおりである.

  • 「(」CONSセルもしくはnil
  • 「\cs」シンボル*2.\ifcatで判断しているので,実際はカテゴリーコード13の文字もシンボルとみなされる.
  • 「'」文字列.
  • 「/」ブール値.
  • 「:」整数.
  • 「!」ディメンジョン.
  • 「@」スキップ.
  • 「+」パーサの拡張機能パーサモジュール)の呼び出し

ここからは,(順不同で)これらのパターンそれぞれに対応したマクロを見ていく.

シンボル,文字列,ブール値

シンボル(文法上は)を読み込むマクロ\@lispread@symbolは,次のように定義されている.

\def\@lispread@symbol#1#2{%
  \endgroup#1\@tlabel@symbol{#2}}

まぁ,簡単.先の\@lispread@mainによって,#2にはシンボルとなるトークンが与えられていることがわかっているので,単にシンボル型のデータ\@tlabel@symbol{#2}を作って継続である#1に渡すだけである.ここで,\@lispreadで\begingroupしているので\endgroupを忘れてはいけない.

文字列()やブール値()もほぼ同様で,次のようなコードになっている.

%% String
\def\@lispread@string#1#2'{%
  \endgroup#1\@tlabel@string{#2}}
%% Boolean
\def\@lispread@bool#1#2{%
  \endgroup#1\@tlabel@bool{#2}}

なお,文字列には「'」を含められないことがこのコードからわかる*3.また,ブール値は「/t」もしくは「/f」なはずだが,ここではそのチェックを入れていない.よって,不正な値(例:「/a」)でもパーサは受理してしまう.この辺りも改善の余地がある.

整数,ディメンジョン,スキップ

TeXの数値や長さに関連するこれらのパーサは,TeXの読み込み処理を利用するようにできている.要は,

を実施しているだけである.コードを示す.

%% Integer
\newcount\@cnt@lispread@int
\def\@lispread@int#1{%
  \gdef\@lispread@int@callback{\@lispread@int@main#1}%
  \afterassignment\@lispread@int@callback
  \global\@cnt@lispread@int}
\def\@lispread@int@main#1{%
  \endgroup\expandafter#1\expandafter\@tlabel@int\expandafter{\the\@cnt@lispread@int}}
%% Skip
\newskip\@cnt@lispread@skip
\def\@lispread@skip#1{%
  \gdef\@lispread@skip@callback{\@lispread@skip@main#1}%
  \afterassignment\@lispread@skip@callback
  \global\@cnt@lispread@skip}
\def\@lispread@skip@main#1{%
  \endgroup\expandafter#1\expandafter\@tlabel@skip\expandafter{\the\@cnt@lispread@skip}}
%% Dimen
\newdimen\@cnt@lispread@dimen
\def\@lispread@dimen#1{%
  \gdef\@lispread@dimen@callback{\@lispread@dimen@main#1}%
  \afterassignment\@lispread@dimen@callback
  \global\@cnt@lispread@dimen}
\def\@lispread@dimen@main#1{%
  \endgroup\expandafter#1\expandafter\@tlabel@dimen\expandafter{\the\@cnt@lispread@dimen}}

ほら簡単.\@cnt@lispread@int,\@cnt@lispread@skip,および\@cnt@lispread@dimenがレジスタで,それぞれへの代入を\@lisp@read@intの末尾で行うようなコードになっている.LISP on TeXの流れに戻る処理は,\afterassignmentでTeXの代入処理をフックすることで実現されている*4

CONSセルおよびnil

CONSセルとnilの処理が実は一番難しい.先にコードを示す.

%% CONS cell or NIL
\def\@lispread@cell#1#2{%
  \if\noexpand#2)% [Branch 1] NIL
    \def\@@next{\endgroup#1\@tlabel@nil{}}%
  \else % Otherwise CONS cell
    \def\@@next{\@lispread@cell@car#1#2}%
  \fi\@@next}
%% first part of CONS cell : read CAR
\def\@lispread@cell@car#1{%
  \def\@lispread@car@reg##1##2{%
    \def\@reg@lispread@car{##1{##2}}%
    \@lispread@cell@dot#1}%
  \@lispread\@lispread@car@reg}
\def\@lispread@cell@dot#1#2{%
  \if\noexpand#2.%
    \def\@@next{%
      \def\@lispread@cell@fincheck####1####2{%
        \def\@reg@lispread@cdr{####1{####2}}%
        \@lispread@fin#1}%
    \@lispread\@lispread@cell@fincheck}% kokonaosu
  \else
    \def\@@next{\@lispread@cell@cdr#1(#2}%
  \fi\@@next}
\def\@lispread@cell@cdr#1{%
  \def\@lispread@cdr@reg##1##2{%
    \expandafter\@read@malloc\expandafter\@reg@tmp\@reg@lispread@car##1{##2}%
    \expandafter\endgroup\expandafter#1\@reg@tmp}%
  \@lispread\@lispread@cdr@reg}
\def\@lispread@fin#1#2{%
  \if\noexpand#2)%
    \def\@@next{%
      \expandafter\expandafter\expandafter\@read@malloc
      \expandafter\expandafter\expandafter\@reg@tmp
      \expandafter\@reg@lispread@car\@reg@lispread@cdr
      \expandafter\endgroup\expandafter#1\@reg@tmp}%
  \else
    \def\@@next{\errmessage{LISP on teX [read]: missing )}}%
  \fi\@@next}
\def\@read@malloc#1#2#3#4#5{%
  \expandafter\gdef\csname car\the\@malloc\endcsname{#2{#3}}%
  \expandafter\gdef\csname cdr\the\@malloc\endcsname{#4{#5}}%
  \expandafter\@@read@malloc\expandafter#1\csname car\the\@malloc\endcsname\csname cdr\the\@malloc\endcsname}
\def\@@read@malloc#1#2{\expandafter\@@@read@malloc\expandafter#1\expandafter#2}
\def\@@@read@malloc#1#2#3{%
  \global\advance\@malloc1
  \def#1{\@tlabel@cons{#2#3}}}

\@lispread@cellではまず,入力の先頭文字が「)」であるか否かを確認する.「)」であればそれはnilなので,継続に「\@tlabel@nil{}」を引き渡して処理を終了する.そうでなければ,それはCONSセルなので,CARを構成するために\@lispread@cell@carを呼び出す.

\@lispread@cell@carでは継続を表すマクロ\@lispread@cdr@regを生成し,再帰的にパーサを呼び出す.\@lispread@cdr@regの表す継続は,「\@reg@lispread@carにパース結果を保存して『.』の有無を調べる処理にこれまでの継続を渡して呼び出す」である.

CAR部分の読み込みが終わると,「.」の有無を調べる処理\@lispread@cell@dotが起動される*5.これは,入力の先頭が「.」であるときは「CDR部分を読み込み,『)』で終端しているかを調べる」とい継続を生成し,パーサを再帰的に呼び出す.そうでない時はリスト表記なので,コード上は「(hoge fuga...)」の形をしているはずである.これは「(hoge . (fuga...))」の省略形であるから,入力の先頭に「(」を付加してから再帰的にパーサを呼び出す*6.このときに渡される継続は,「ここで渡された継続に対応するCONSセルを渡す」である.

\@lispread@finが,先の前者の呼び出しに対する継続,すなわち「『)』で終端しているかの確認とそれまでの継続の呼び出し」に当たる部分である.これは,単に\ifで「)」と入力の先頭を比較して,正しければ「CONSセルを継続に渡す処理」を実行するだけである.

\@read@mallocが\@lispread@cell@dotの後者(「.」が入力の先頭でなかったとき)と\@lispread@finで使っている「CONSセルを作る処理」である.#1に制御綴りをとり,CAR(#2{#3})とCDR(#4{#5})を\carXXと\cdrXXにグローバルに代入して#1を

\@tlabel@cons{\carXX\cdrXX}

に定義する.XXは数値で,\@mallocによりすべてのCONSセルに対して一意の値を振るようにしている.ちょうどXXがメモリアドレスのように振る舞うので,その数値を元にGCを作ることが可能である*7

パーサモジュール

ここが,前回のデータ構造編で言及していた,パーサへのフック機構になっている.まずはコードを示す.

%% Reader Module
\def\@lispread@module#1#2{%
  \@lispread@module@main\@register@lispread@module#2\@@end
  \endgroup
  \expandafter#1\@register@lispread@module}
\def\@lispread@module@main#1#2::#3\@@end{\csname @mod@read@#2\endcsname#1{#3}}

パーサモジュールの呼び出しは,「+{modname::value}」の形をしている.モジュール名fooは,パーサモジュールとして

\@mod@read@foo#1#2

というマクロを定義しておく.\@mod@read@fooは#1に制御綴りを,#2にモジュール呼び出し時のvalueの部分のトークン列を取り,#1をLISP on TeXのデータ構造の要件を満たすトークン列になるように定義する.\@lispread@moduleは,\@mod@read@fooを展開して得られたオブジェクトを継続に引き渡す.

文法上示されていない固定小数点数型は,実はこれを使って実現している.

(評価器編へ続く)*8

*1:keno_ssさんの報告により気がついた.多謝

*2:ただし,入力の終わりに使用される\@end@lispreadは使用できない.

*3:入れるように定義を変更してもいいのだけれど,割りと面倒くさいので放置してた.

*4:TeXは様々な処理をフックするための機構が標準で用意されているよくわからない言語である.参考

*5:ここ直すというコメントが入っているが,なにが問題なのか忘れてしまった.そんなに重要な問題では無いと思うのだが……←プロジェクト管理大事.最近Redmineを試してみている.

*6:入力の末尾に「)」をつけなくても,括弧の対応は正しく取られることに注意する.

*7:実際は,これらのアロケーション機構とともに変更が必要だったり,そもそもポインタをたどることに対応する処理がTeXで実現しづらいので,very hardな実装になるが……

*8:何時頃になるか名言はできないが……

LISP on TeXを作る(データ構造編)

この記事はTeX & LaTeX Advent Calendar 2013の12/25担当分です. < 12/24 は munepiさん || 2014/12/1は\@undefined

はじめに

TeX & LaTeX Advent Calendar 2013のトリはLISP on TeXとなった.

今年はTUG2013での発表という大イベントがあった.そのときはなるべく「急カーブ注意」なことは避けてきたのだが,標識3個くらいまでならむしろやったほうが良かったのではないかと後悔したのが記憶に新しい*1

今日はそのリベンジとして,LISP on TeXの中身を説明していこうと思う*2.今回はデータ構造にのみ言及する.TeXでプログラミングしたいキチガイコアな人からLISP on TeXの型を拡張したいという変態まで,参考になれば僥倖である*3

LISP on TeXのデータ構造の基本形

LISP on TeXにおいて,データ構造は非常に単純な構造になっている.型xxxのデータ...は次で表現される.

\@tlabel@xxx{...}

ここで\@tlabel@xxxは型を表すラベル(以後,型ラベルと呼ぶ)である.例えば,\@tlabel@intは整数型,\@tlabel@stringはTeXトークン列を示す.これらはすべて\ifxにより識別できるようになっている.*4一応注意しておくと,これらはスタイルファイル中の記述なので,「@」のカテゴリーコードは11である.波括弧の中身はデータ本体である.例えば,整数の10(LISP on TeXリテラル表記は:10)はパーサによって\@tlabel@int{10}と認識される.

ここで,型と呼ぶものは普通のプログラミング言語のそれとは異なる.実は,\defineや\lambdaなどのスペシャルフォームもそれぞれ固有の型を持つ.例えば,\defineに束縛されているデータは\@tlabel@define{}となっている.

型ラベルの展開と評価器

ただ単に型のラベルとして制御綴りを使用するのはもったいないので,型ラベルを展開するとeval(式の評価)やapply(関数などの適用)に使用できるマクロが手に入るように構成されている.型ラベル\@tlabel@xxxは展開すると,

\@eval@xxx\@apply@xxx\@@apply@xxx

となるようになっている.

\@eval@xxxは,パターンマッチなしの3引数を取るマクロである.第1引数は型xxxのデータ本体が,第2引数はevalが呼び出されたときの環境(変数とデータの対応)が,そして第3引数には制御綴りが与えられる設計になっている.第3引数の制御綴りはevalした結果の格納先である.\@eval@xxxは第1,2引数をもとに型に対応したevalを実行し,第3引数の制御綴りをグローバルに書き換える*5ことによって評価をおこなう.例えば,整数の評価規則\@eval@intの定義を見ると,

\show\@eval@int
> \@eval@int=macro:
#1#2#3->\gdef #3{\@tlabel@int {#1}}.

のように,制御綴り#3を\@tlabel@int {#1}に定義していることがわかる.

\@apply@xxxと\@@apply@xxxはどちらもapply用に使用されるマクロである.引数には基本,どちらも同じパターンマッチ#1#2\@#3#4が使用される.#1はデータ本体,#2は与えられた引数のリスト,#3は評価時の環境,#4が結果格納先の制御綴りである.ただし,型によっては#2の部分でより細かいパターンマッチが適用される.例えば,\defineの第1引数は絶対にシンボル(\@tlabel@symbol)でないとならないので,その辺がパターンマッチで表現されている.2つの違いは,前者が関数呼び出しで起動されるのに対し,後者がapply関数による関数適用で使用されるという点のみである.すなわち,前者は引数に対して何らかの処理(例:引数の評価)を行うのに対し,後者はそれを行わない.正確な説明は(おそらくやるであろう)評価器編で行う.

余談

この構造のメリット・デメリット

この構造により,LISP on TeXでは\if系トークン(\ifや\ifxなど)の使用を極力避ける仕組みになっている.\if系トークンの挙動は割りと面倒なので,この構造にしておくと何かと実装に便利である.しかし,この構造は同時に評価器のコードを散逸させてしまっているデメリットを持つ.実は,評価器のメイン部分は\@eval@consと各種適用可能な型の\@apply@xxxと\@@apply@xxxなのだが,これらのコードが分離してしまっているため,コードの変更には弱かったりする.

LISP on TeXへの型の追加

先の規則さえ満たせば,ユーザがあとから型を追加することが割と容易になっている.実際,\@eval@xxxは

\@eval@xxx#1#2#3{\gdef#3{\@tlabel@xxx{#1}}}

としておけばevalした結果はそれ自身であるという実装が完成し,applyに関しては,(ユーザがapply可能な型を新設するのは稀なことであろうことから)展開時にエラーメッセージを出力するマクロにしておけば問題ない.実際,LISP on TeXのすべての組み込み型はそのような初期化が行われる.また,パーサへのフック機構も用意してある(参考).説明は(おそらく次回の)パーサ編に回すが,このコードさえ理解してしまえば,ユーザはほぼ自由にLISP on TeXに型を追加できるようになる.

終わりに

なんか急カーブ注意の標識が何本立つのかよくわからない記事になってしまったが,
Happy TeXing and Merry Christmas!
%
%
\Finale

*1:奥村先生に「中身が気になる」と言われたのです

*2:英語でない理由は察して……

*3:範囲狭い……

*4:実装では\ifxで区別しているところと,マクロのパターンマッチ使っているところがあったりするが……

*5:正確に確かめたわけではないが,グローバルでなくとも動く可能性はある

METAFONTでUEC校章

この記事はTeX & LaTeX Advent Calendar 2013の12/7担当分です. < 12/6 は CardinalXaroさん | | 12/8 はkuroky_plusさん>

放置ブログにAdvent Calendarの火を.放置している間に,TUG2013への参加なんかがあった.参加者は感想を投稿している人も多いのだけれど,私はTeX芸人っぽいことがしたいので,それはまた別の機会に.

しかし,今回はTeXネタでなくMETAFONTネタ.電気通信大学校章のMETAFONT化をした話*1 *2

TeXLaTeXを知っていても,「METAFONT? なにそれ,美味しいの?」なんて人もいるかもしれないので,一応説明しておくと,METAFONTはTeXで使用するフォントを作成するための言語である.これの記述とTeXLaTeXで使用するまでの流れを説明をみていく.

まずは,METAFONTソース*3から(ファイル名をmyueclogo.mfとする).今回のUEC校章のものはこんな感じ.

mode_setup;
em#:= 48pt#; cap:=48pt#;

beginchar(65,48pt#,48pt#,0pt#);
path p;
gakusc# = 1/24w;
ressc# = 4pt#;
pickup pencircle scaled ressc#;
%exact res
%p := ((w-sind(6*90)*w)/2,(h-cosd(5*90)*h)/2) for k=1 upto n: ... ((w-sind(6*(360/n*k+90))*w)/2,(h-cosd(5*(360/n*k+90))*h)/2) endfor;
p:= (0,h){curl 0}...{right}(7/8w,0)...{up}(w,1/8h)..tension 1.3 ..{left}(1/4w,h)...{down}(0,3/4h)..tension 1.6 ..(1/2w,0)..tension 1.6 ..{up}(w,3/4h)...{left}(3/4w,h)..tension 1.3 ..{down}(0,1/8h)...{right}(1/8w,0)...{curl 0}(w,h);
draw p;
pickup pencircle scaled gakusc#;
%DAI-GAKU
z1 = (1/2w,7/8h); x2 = x1;
y1-y2 = 1/16h;
y3 = y4 = y5 = y6 = y2;
x3 = 1/4w; x6 = 3/4w;
x3-x4 = x4-x5 = x5-x6;
penpos7(gakusc#,90);
penpos8(gakusc#,90);
penpos9(gakusc#,90);
penpos10(gakusc#,90);
y7 = y8 = y9 = y10;
x7 =x3; x8 =x4; x9 =x5; x10 =x6;
y7 = 11/16w;

penpos11(gakusc#,0);penpos12(gakusc#,0);
z11 =z8l; z12 = z9l;
penpos14(gakusc#,0);penpos15(gakusc#,0);
penpos18(gakusc#,0);penpos19(gakusc#,0);
penpos17(gakusc#,90);
penpos20(gakusc#,90);
x13 = x7; x14 = x8; x15 = x9; x16 = x10;
y13 = y14 = y15 = y16;
x17 = x7; x18 = x8; x19 = x9; x20 = x10;
y17r = y18 = y19 = y20r;
y7l-y13 = y13-y17r;
y20 = 1/2h;
penpos21(gakusc#,90); penpos22(gakusc#,90);
z21 = z14r; z22 = z15l;


penpos23(gakusc#,90); penpos24(gakusc#,90);
z23r = z17l; z24r = z20l;
y25 = y26 = 5/16h;
x25 = x7; x26 = x10;
y27= y28 = y29 = y30 = y23l-gakusc#;
x28 = x18l; x29 = x19r;
x28-x27 = x27-x23;
x24-x30 =x30-x29;
y31 = y32; x31 = x28; x32 =x29;
y23 - y28r = y31-y33;
y33 = y34 = y25;
x33 = x27; x34 = x30;
x35 = x36 = 1/2w;
y35 = y33;
y36 = y37 = 3/16h;
x37 = x33 + gakusc#;

erase fill z3---z25---z26---z6---cycle;

erase draw z3---(x7,y7+gakusc#) withpen pencircle scaled 2gakusc#;
erase draw z6---(x10,y10+gakusc#) withpen pencircle scaled 2gakusc#;
erase draw z3---z6 withpen pencircle scaled 2gakusc#;
draw z1---z2;
draw z3---z6;
draw z3---(x7,y7+gakusc#);draw z4---(x8,y8+gakusc#);
draw z5---(x9,y9+gakusc#);draw z6---(x10,y10+gakusc#);


erase draw z7l---z11l withpen pencircle scaled 2gakusc#;
erase draw z12r---z10l withpen pencircle scaled 2gakusc#;
erase  draw z7l---z17r withpen pencircle scaled 2gakusc#;
erase  draw z10l---z20r withpen pencircle scaled 2gakusc#;
draw z7l---z11l; draw z12r---z10l;
draw z13--z14l; draw z15r---z16;
draw z17r---z18l; draw z19r---z20r;
draw z7l---z17r; draw z10l---z20r;
draw z11r---z22r; draw z21r---z12l;
draw z21l---z19l; draw z18r---z22l;

erase draw z25---z23---z24---z26 withpen pencircle scaled 2gakusc#;
erase draw z27---z33---z34---z30 withpen pencircle scaled 2gakusc#;
erase draw (x35,y35-1/2gakusc#)---z36---z37 withpen pencircle scaled 2gakusc#;
draw z25---z23---z24---z26;
draw z27---z33---z34---z30;
draw (x35,y35-1/2gakusc#)---z36---z37;
penpos28(1/2gakusc#,90); penpos29(1/2gakusc#,90);
pickup pencircle  xscaled gakusc# yscaled 1/2gakusc#;
draw z28r---z29r---z32---z31---cycle;

%penlabels(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
%penlabels(21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37);
endchar;

なげぇ.ネタなので説明はほどほどにする.

最初のmode_setupはおまじないみたいなもので,これによって,解像度の設定やら何やらの「モード」に対する設定を行っている.その下のemやcapはフォントのパラメータの設定である.

次のbeginchar(c, w, h, d);からendchar;までがひとつの文字を定義している部分.cが文字コードで,wは幅,hは高さ,dは深さを表していて,この値はTeX組版するときに使用される.長さに#が付いているのは,「実際の」と言う意味で,解像度に影響されない長さであることを示している.ちなみに,これはTeXで使われる大きさであって,実際の字形がこの幅に収まっていることは保証されない.

幾つか変数を宣言した後,リサジュー図形の描画に入る.「p:=...」の部分がパスの定義で「draw p;」で描画される.記法はTikZとかとも近いので読みやすいのではないだろうか?METAFONTのいいところは,曲線を「..」や「...」で記述できるところである.このとき,METAFONTは空気を読んで滑らかに点と点を結んでくれる.

pのパス定義の部分にコメントアウトがあるが,これは実際に計算させて本当のリサジューを描くものである.METAFONTには三角関数を計算できたりパス定義中にforループを記述できたり*4するので,実は簡単にパラメトリック曲線を記述できる.実際に書いてみたところ,「電通大のリサジューは実際のリサジューではない」というどうでもよい事実に気がついたため,書きなおした歴史がある.

その後は,「大学」と書いてある部分の描画に入る.各点の位置を定義->適当につないで描画を繰り返しているのだが,ポイントはMETAFONTの「=」は「代入」ではなく,「等式」という事実である.実際「y1-y2 = 1/16h;」のような式が記述できる.C語族などに慣れた人にはわかりにくいかもしれない.METAFONTは,与えられた連立方程式を解いて,点の位置を計算してくれるという宣言的に記述が可能な良い言語である.


これを,LaTeXで使うには次のようにすれば良い.まず,TeX組版するために必要なTFMファイルを作成する.方法は,

mktextfm myueclogo

とすればよい.LaTeX文書では

\documentclass{article}
\font\uec=myueclogo
\begin{document}
{\uec A}電気通信大学校章
\end{document}

ちなみに,この方法はLaTeXのフォント管理を突き抜けてTeXのフォント指定法を使うというダーティな方法なので,だれかLaTeX用に仕立て直してもいいのよ?

結果はというと,こんな感じ.

コミュニケーションマークは一応用意してるけど,出来が良くない.

私はこれを使って,レポートの表紙を自作して提出していた.使いたければhttps://bitbucket.org/hak7a3/myueclogo:titl=ここに置いておくので,煮るなり焼くなりするといいと思う.*5

ちなみに,やろうと思っていたTeXネタは,実はTikZで遊ぶものだったのだが,間に合わなかった……(まだ空きが合った気がしますね,Advent Calendar)

*1:UECアドベントカレンダー? 知らない子ですね

*2:これを作成したのは実は学部時代だったりする.公開し忘れていたのでこれを機に公開

*3:の一部

*4:forは式の途中につかえるという仕様

*5:所属していた研究室や関係している所のロゴとかも入っているけど気にしない

TeXと異世界の交わり

研究室のゼミでやった内容の加筆版.名称などはでっち上げなので注意.

はじめに

LISP on TeXがCTANに登録されてから,そろそろ一週間が経つ.TeXLiveやMikTeXに取り込まれるなどもした.更に,CTANのTopicsに「exec-foreign」が追加され,そこにlisp-on-texが配置された*1.しかし,これ以外にも,旧来からTeXと他の言語を組み合わせる試みは多数行われてきた.そこで,ここではそれらの紹介を行う.

まず,TeXと他の言語を組み合わせる手法は,TeXエンジンの中で解決するもの外部の処理系に頼るものに大別することができる*2.それぞれについて説明する.

TeXエンジンのみの中で解決する手法

これは,TeXエンジンやマクロの機能を用いて,TeXとして閉じたまま他の言語と交じり合う方式である.この手法の大きな特徴は,外部環境への依存性が少ないこと.そもそも外界との接触がないから当然だが,複数のOSで動くことを想定する際に,この性質は役に立つ.この手法は,インタプリタエンジン型に区別できる.

インタプリタ

インタプリタ型は,TeXのマクロを用いて,他の言語のインタプリタTeX上に実装してしまう形式である.既存のTeXエンジンの大半で動作するため,古い環境を使わざるを得ないユーザ*3でも簡単に導入できるメリットがある.問題は,実装するともれなくキチガイ呼ばわりされることと,TeXマクロの制約により,高度な文字列処理の実装が面倒であること.前者はともかく,後者は主な使用用途を考えると大問題である.
実装例としては,BASICほむほむ・Grassが挙げられる.もちろん,lisp-on-texもこれ.expl3は微妙な感じがするが,ここではインタプリタ型に分類する.

エンジン型

そもそも,TeXエンジンに他言語の実行機能をつけたもの.エンジンと密接に連携できる性質を持ち,マクロだけでは実現が困難なこともやってのける.実装例は言うまでもなくLuaTeX.この機能を用いて日本語組版の実現を行う取り組み(LuaTeX-ja)も有名.

外部の処理系に頼る手法

既存の言語処理系を用いて,TeXとその処理系を組み合わせる手法.利点は,実装がお手軽なところ.実際,スクリプト書いてLaTeXソース出力するとか位なら利用例は多い.この手法はプリプロセッサ外部ファイル型,および\write18型に分けることができる.

プリプロセッサ

Cプリプロセッサのように,TeXと違うプリプロセッサ言語を定義し,TeXエンジンにかける前に前処理を施す方式のこと.実装例はLISPで記述するものが挙げられる.

外部ファイル型

外部ファイルを通して,TeX処理系と他言語の処理系を連携させる方式.有名な例はもちろんPerlTeX.TeXLiveにも収録されているはず.PerlTeXの作者は論文?TeXと他言語の交わり方についてよく議論しているので,一度読むことをおすすめする.

\write18型

TeXにはなぜか,shell実行機能(\write18)がある.ファイルを通してなんてことをせずに,直接shellを叩いて他の言語とやり取りする方式が\write18型.問題は,悪意あるTeXソースによりシステム破壊が可能になる点.先の論文でもこの問題が指摘されている.CTANにPythonの実装がある.

*1:まさかのLISP on TeXのみのTopic

*2:私感.以下同様

*3:管理者が怠慢で,TeX環境が更新されない計算機を使うユーザとか