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環境が更新されない計算機を使うユーザとか

Load to CTAN(CTANに載るまで)

LISP on TeXのCTAN入り

全国のTeX Liveをお使いの皆さん.ようこそ,LISP on TeXへ.

というわけで,LISP on TeXがCTAN*1登録されました.TeX Liveで使えることは,

を見る限り確認できているので,使ってみてくれると嬉しいです.

CTANへの道標

で,ココからが本題.CTANに成果物をアップロードするまでの流れを,ここに残していきます.

1. 成果物を作る

最重要.まずは,Happy TeXingすることから始める.

このとき,READMEを作ることを忘れないこと.READMEには,選択したライセンスに関する注釈を忘れない*2.READMEが日本語のものが見受けられたけど,私はつたない英語で書きました……
参考 : http://www.ctan.org/upload/

READMEはpdf形式でも構わないらしいが,その時は<<パッケージ名>>.pdfもしくは<<パッケージ名>>-doc.pdfにすべし.manual.pdfはやめろとここにある.

LaTeXのパッケージの場合,.dtxファイルや.insファイルを作るのが一般的とされているみたいだが,別に作らなくても通ります..styとかで平気.

2. アップロード形式に固める.

作成が終わったら,固めてCTAN teamへ送ります.ディレクトリ構成は,TDS*3がわかる人はそのように固めて送るといいらしい.わからなければ単一のディレクトリにまとめておけばOK.圧縮形式はzipまたはtar.gz.ただし,TDSの形式で送るときはtds.zip.

3. CTANのアップロードページヘ

さぁ,アップロードの時間だ.アップロードページヘ行き,必要事項を埋めてsubmitします.書く内容は,

  • 成果物の名前,version
  • 氏名,連絡先(メール)
  • 成果の要約
  • アナウンスメッセージ
    • ここは,よくわからないのでとりあえず「Release.」と書いてしまった……
  • 選択したライセンス

が必須です.オプションで,

  • TeXのディレクトリ構成のうち,どこが良いか
  • CTAN teamへのメッセージ

を記述できます.あとは,先ほど固めた成果物とともにアップロードします.

4. 待つ→登録

しばし待ちます.いつの間にか登録されてました.LISP on TeXの場合,登録が一日で完了しました.迅速な対応に感謝です*4
登録されると,ページがいつの間にか出来上がっています.ここで,あまりにも私の英語が汚かったのか,成果の要約が若干修正されていました.お手数おかけしました.
更にしばらくすると,TeX Liveで使えるようになってます.ちなみにこの間,メール等でのやり取りはありませんでした.

READMEが日本語でもいいあたり,実はCTANは身近な存在です.これで日本からのアップロードが増えてくれることを祈っています.

*1:Comprehensive TeX Archive Network

*2:私は忘れかけてた

*3:TeX Directory Standard

*4:ここで,かつ日本語で言っても届かないか……

LISP on TeX におけるパーサのフックな話

生きてます(唐突に).

LISP on TeX更新情報

最近,修士論文も無事に提出できたので,LISP on TeXに機能拡張とかしてみた.

パーサのフック機能.

LISP on TeXの整数型,dimen型,およびskip型にリテラルには,それぞれ決まったプレフィクスをつけることにしている.
しかし,ユーザが新しい型を追加できない*1という問題があった.また,これ以上型を増やすと適当なプレフィクスがなくなってしまうという問題も出てきた.

そこで,汎用的にパーサを操作できる構文を追加することとした.構文は,次で示される.

+{mod::foo}

modはモジュール名,fooはモジュールmodによってパースするトークン列である.

例えば,lisp-mod-stdout.styでは,stdoutモジュールを提供している.

\documentclass{article}
\usepackage{lisp}
\usepackage{lisp-mod-stdout}
\begin{document}
  \lispinterpl{+{stdout::orz}}
\end{document}

と書いてタイプセットすると,

$ latex hatena.tex
This is pdfTeX, Version 3.1415926-2.5-1.40.13 (TeX Live 2012/W32TeX)
 restricted \write18 enabled.
entering extended mode
(./hatena.tex
LaTeX2e <2011/06/27>
Babel <v3.8m> and hyphenation patterns for english, dumylang, nohyphenation, ge
rman-x-2012-05-30, ngerman-x-2012-05-30, afrikaans, ancientgreek, ibycus, arabi
c, armenian, basque, bulgarian, catalan, pinyin, coptic, croatian, czech, danis
h, dutch, ukenglish, usenglishmax, esperanto, estonian, ethiopic, farsi, finnis
h, french, friulan, galician, german, ngerman, swissgerman, monogreek, greek, h
ungarian, icelandic, assamese, bengali, gujarati, hindi, kannada, malayalam, ma
rathi, oriya, panjabi, tamil, telugu, indonesian, interlingua, irish, italian,
kurmanji, latin, latvian, lithuanian, mongolian, mongolianlmc, bokmal, nynorsk,
 polish, portuguese, romanian, romansh, russian, sanskrit, serbian, serbianc, s
lovak, slovenian, spanish, swedish, turkish, turkmen, ukrainian, uppersorbian,
welsh, loaded.
(i:/W32TeX/share/texmf/tex/latex/base/article.cls
Document Class: article 2007/10/19 v1.4h Standard LaTeX document class
(i:/W32TeX/share/texmf/tex/latex/base/size10.clo)) (./lisp.sty (./lisp-read.tex
) (./lisp-arith.tex) (./lisp-string.tex) (./lisp-latexutil.tex)
(./lisp-prim.tex)) (./lisp-mod-stdout.sty)
No file hatena.aux.
orz
(./hatena.aux) )
No pages of output.
Transcript written on hatena.log.
[1]+  Done                    sublime_text.exe hatena.tex

コンソールにorzが表示される*2

また,現在のLISP on TeXでは,fpnumモジュールをlisp-mod-fpnum.styで提供している.これは,TeXのdimenを使って固定小数点数計算を行うためのモジュールで,

+{fpnum::1.0}

と記述すると,内部でfpnum型の1.0として扱われるオブジェクトを生成する.この型のオブジェクトに関する加算,減算,乗算および比較演算もlisp-mod-fpnum.styは提供している.

これを使って,マンデルブロ集合に再挑戦した.前回は,TeX on LaTeXあれに,実行時間,精度共に惨敗したが,fpnumがなかっただけで,今ならいけるはず!

ソースはこんな感じ

\documentclass{article}
\usepackage[dvipdfm,a3paper,margin=1pt,landscape]{geometry}
\newcount\mlength
\newcount\cstate
\newdimen\mandelunit
\mandelunit=0.5pt

\def\w{%
  \ifnum\cstate=1 \global\advance\mlength1
  \else \vrule width \mlength\mandelunit height \mandelunit depth 0pt \global\mlength1 \fi
  \global\cstate1}
\def\b{%
  \ifnum\cstate=-1 \global\advance\mlength1
  \else \hspace*{\mlength\mandelunit}\global\mlength1 \fi
  \global\cstate-1}
\def\r{%
  \ifnum\cstate=1\hspace*{\mlength\mandelunit}\else\vrule width \mlength\mandelunit height \mandelunit depth 0pt \fi
  \global\cstate0 \global\mlength0}

\usepackage{lisp}
\usepackage{lisp-mod-stdout}
\usepackage{lisp-mod-fpnum}
\lispinterpl{%
  (\define \maxloop :20)
  (\define \scale +{fpnum::0.002})
  (\define \isMandell
    (\lambda (\a \b \k \x \y)
      (\lispif (\< \maxloop \k) /t
        (\lispif (\fplt +{fpnum::4.0} (\fpplus (\fpmul \x \x) (\fpmul \y \y)))
          /f
          (\isMandell \a \b (\+ \k :1)
            (\fpplus \a (\fpmul \x \x) (\fpminus (\fpmul \y \y)))
            (\fpplus \b (\fpmul +{fpnum::2.0} \x \y)))))))
  (\define \drawMandell (\lambda (\a \b)
    (\begin
      (\lispif (\isMandell \a \b :0 +{fpnum::0} +{fpnum::0})
        (\texprint '\b') (\texprint '\w'))
      (\immediatewrite))))
  (\define \loopMandell (\lambda (\a \b)
    (\lispif (\fplt \b +{fpnum::-1.0}) ()
      (\begin
        (\drawMandell \a \b)
        (\lispif  (\fplt +{fpnum::0.5} \a)
          (\begin
            (\texprint '\r\\')
            (\immediatewrite)
            (\loopMandell +{fpnum::-1.5} (\fpminus \b \scale)))
          (\loopMandell (\fpplus \a \scale) \b))))))
}

\begin{document}
  \noindent
  \leavevmode\baselineskip=\mandelunit
  \lispinterpl{(\loopMandell +{fpnum::-1.5} +{fpnum::1.0})}
\end{document}

これで勝つる.実行結果はこんな感じ.

実行時間はというと……

real    274m44.480s
user    0m0.000s
sys     0m0.031s

惨敗でしたー*3

追記
前回と同ループ回数,同メッシュでやったら37分だった.また,前回のメッシュ + ループ制限は今回のものでやったら10分で計算終了.

評価ルーチンのブラッシュアップ

これは拡張じゃないけども一応.\if系トークンの使用回数を減らしたし,若干早くなった+軽くなった,はず……

*1:そもそも誰もしないと思うが

*2:ちなみに,+{stdout::xxx}は常にnilを返す

*3:前より遅いのは,精度を上げたから.

アドベントカレンダー二日目 ― \newif\ifnextyear \ifnextyeartrue

TeX & LaTeX Advent Calendar TeX で騒げ、TeX で笑え
この記事は,12/2 担当分です.< 12/1 は ZR さん | | 12/3 はk16.shikano さん>

周りは \expandafter を叫んだりする記事みたいなので,私はマイナーな(?)プリミティブである \aftergroup と LISP on TeX での利用法を一例としてあげようと思う.

そもそも \aftergroup ってなによ

黄色(LaTeX2e マクロ & プラス プログラミング基礎解説)*1にも載っていない*2が,TeX の強力なトークン展開の制御機能ひとつである.\aftergroup は現在のグループが終わった地点での動作をフックできる.使い方は,

\aftergroup<トークン>

で,トークンがグループ終了時に挿入される.複数指定したときは,指定した順序でそのすべてのトークンが挿入される.

これだけでは直感に欠けるので,コード例を.

{\aftergroup\show}\expandafter

実行結果は次のようになる.

$ tex test.tex
This is TeX, Version 3.1415926 (TeX Live 2012/dev/W32TeX)
 encTeX v. Jun. 2004, reencoding enabled.
(./test.tex
> \expandafter=\expandafter.
l.1 {\aftergroup\show}\expandafter

? x
No pages of output.
Transcript written on test.log.

グループの終わり,すなわち \expandafter の前に \show が挿入され,\show\expandafter となりこのような表示がなされる.

で,なにに使うの?

私も最初はよくわからなかった.しかし,TeX で言語処理系を実装するよくある状況*3では非常に有用なことがわかった.末尾呼び出しの最適化である.

末尾呼び出しの最適化とは?

末尾呼び出しの最適化とは,再帰呼び出しを安心してループのように使うために必要な最適化技術である.次の Scheme プログラムを例にして説明する.

(define (fact n acc) (if (= n 0) acc (fact (- n 1) (* acc n))))
(fact 10 1)

階乗を計算する末尾再帰になっているプログラムである.最適化しないナイーブな実装では,関数の呼び出しスタックは

[(fact 10 1)][(fact 9 10)][(fact 8 90)][(fact 7 720)].........[(fact 0 3628800)]

となる.ここで fact 関数の末尾での動作は,呼び出した関数の結果を返すだけというものであり,わざわざスタックを消費する必要がない.そんなときを検知して,スタックを潰すのが末尾呼び出しの最適化である.

で,\aftergroup との関連性は?

TeXインタプリタを作成する場合,この呼び出しスタックをグループで表現するのがよくあるデザインである.グループのネストには限界があるので,末尾呼び出しの最適化は重要な課題となる*4.ここで,\aftergroup である.\aftergroup で「次にやるべき計算」*5をグループが終わったあとに挿入させるようにし,現在のグループを閉じてしまえば,不必要な要素をたたんで,新たな要素をスタックに積むという動作を実現することができる.

具体的に,LISP on Tex では \@@tco という名前のトークンに「次にやるべき計算」を束縛し,\aftergroup\@@tco とすることで末尾呼び出しを最適化している.

おまけ -- TeX におけるフック機能について

TeX には \aftergroup 以外にも,様々な場所をフックできるキチガイ言語である.一覧にしてみよう.

\output 出力ルーチンそのもの*6
\par 段落の間に挿入される.これを書き換えると,段落の終わりをフックできる.しかし,オリジナルの \par を実行しないと段落が変わらないため,初心者にはおすすめしない
\everypar 段落の先頭に挿入されるトークンリスト.行頭に行番号を入れるなどのマクロは,これを使うと実装できる.
\everymath 文章内の数式をはじめる時のフック.
\everydisplay ディスプレイ数式のフック.
\everyvbox 垂直ボックスの開始.
\everyhbox 水平ボックスの開始.
\everycr 表組みでの \cr の後ろ.TeX の表組みプリミティブである \halign と \valign を知ってないと使いづらいかもしれない.
\afterassignment レジスタへの代入やマクロ定義のフック.

*1:TeX プログラミングするときの有名な参考書のひとつ.絶版

*2:索引見て確認した

*3:BASIC とか Grass とか LISP とか

*4:LISP on TeX の初期実装では,処理系が簡単に落ちたという悲惨な歴史もあったりする.

*5:継続みたいなもの.

*6:フック -> 修正