So-net無料ブログ作成
検索選択
前の10件 | -

CSV parsing for Standard ML [SML]

ありそうでなかった(と思う)、Standard ML用のCSV読み込みライブラリを作りました。

CSV parsing for Standard ML

単純な使い方テストケースを見るとすぐわかると思います。


Standard ML用の新しいXMLパーサー [SML]

Standard ML用のXMLパーサーを作った。

UXML

SMLの世界にはSMLで書かれたXMLパーサーが(知っている範囲では)すでに2つある。 1つはfxpという、かなり昔からあるもので、これはDTDの検証も行うフルセットのXML1.0準拠もの、 もう1つはSMLNJ-LIBの中にあるXMLストラクチャで、これはかなり非力なものだ。

UXMLは非検証のXMLプロセッサーとしてXML1.0になるべく準拠しようとしている。 現段階ではUTF-8しか受け入れない点と𐀀以上の文字を受け入れない点 (これはUXMLが依存しているSMLNJ-LIBのUTF8ストラクチャの制約)、 そしてXML1.0の整形条件違反のかなりの部分を報告しない(受け入れてしまう)点が 非準拠であるが、現実的な処理では十分に使える状態になっていると思う。

他の2つのXMLプロセッサーにない点として、 名前空間をサポートしている点と、XPathライクなナビゲーションAPIを持つ点がある。

使用するにはタグv.0.1.0を取得するか、Smackageから利用してほしい。 Smackageを使う場合、.smackage/sources.localにuxml git git://github.com/tkob/uxml.gitという行を追加し、SML/NJのCMファイルからは`$SMACKAGE/uxml/v0.1.0/uxml.cmのように参照する。


REPLのセッションをそのまま単体テストにする [Rehearsal]

REPLを持ったプログラミング言語処理系における原始的かつボトムアップな開発手順について考えよう。

まず小さな関数(だとか)を書く。 思った通りにかけているだろうか? そこでREPLを起動してソースをロードし、関数に対して入力を与え、結果を見る。 正常系は問題ない。 そこで異常な入力に対するハンドリングを関数内に追加して、異常処理がうまくいくことを見る。 でもさっきの正常系の処理が変わったりしていないだろうか。 そこでESCに続けてkを何度か押して(またはCtrl+Pを何度か押して)履歴から正常系の入力の式を復元してもう1回確認してみる。 関数の実装がちょっと気に入らないので変更する。 さっきまで動いていた分はまだ正しく動くだろうか。 そこでまた履歴から入力式を復元して叩いてみる。

この試行錯誤の中には「テスト」をしている箇所がある。 でもそれは揮発的だ。 REPLの履歴とプログラマの短期記憶の中だけにあって、しばらくすると消えてしまう。

テスト駆動開発、あるいは単に「単体テストを書く」ことは不揮発なテストによってコードの品質を保とうとする習慣である。 そこではREPLでアドホックにテストをするのではなくてテスティングフレームワークの定める形でテストコードを書く。 テストはことあるごとに自動で繰り返し実行される。 コードの変更が何かを壊していたらすぐにわかるし、いま現在のコードがすべてのテストを通過しているという確証を得ることができる。

では我々はそもそも最初からREPLなんて使うべきではなかったのだろうか。

長期的に見てテスティングフレームワークを使用すべきことは明らかだ。 でもREPLにも良い点はある。敷居の低さだ。 REPL上の作業ではプログラム実行とプログラマの頭の中は実際には 「とにかく入力を与えてみて、結果を見る。その次に結果が妥当かどうかを考え、妥当であればよしとする」 という関係になっている場合が多い。 これはある種の教義を信奉する人にとっては悪い習慣とみなされるだろうが、 ともかく「良い習慣」より敷居が低いことは事実だ。

問題はREPL上の作業とテスティングフレームワークで単体テストを書くこととの間の距離だ。 敷居の低いREPLで行った「テスト」を不揮発にしたくなった段階で、その距離が問題になる。 さっきまでのREPLに打っていた式ではなくて、テスティングフレームワーク上のアサーションを書かなければならなくなるのだ。

Rehearsal は、ここにフィットするテスティングフレームワークだ。 Rehearsalにおける単体テストはREPLのセッションそのものである。 例えばSML/NJでgreet.smlというテスト対象のプログラムを作り、次のようなREPLセッションを行ったとする。

- use "greet.sml";
[opening greet.sml]
val greet = fn : unit -> unit
val it = () : unit
- greet ();
hello!
val it = () : unit

このセッションを不揮発にするには、コピーアンドペーストで次のファイルを作る。

# greet function prints "hello!" to the console
 
```
use "greet.sml";
[opening greet.sml]
val greet = fn : unit -> unit
val it = () : unit
- greet ();
hello!
val it = () : unit
```

これを t/greet.t というファイル名で保存したとすると、テストを再実行するには次のコマンドを打つ。 このコマンドはもちろんMakefileなどに書いておけばよい。

$ rehearsal -command sml -ps1 '- ' -ps2 '= ' t/greet.t
1..1
ok 1 greet function prints "hello!" to the console

RehearsalはREPLがなんであるかは問わない。シェルでもよいしPythonでもScalaでもよい。 出力はPerlのTAPという形式に従っているが、JUnit XML形式の出力ができるのでJenkinsなどに集計させることもできる。

Reharsalのテストスクリプトの文法はMarkdownに合わせている。 だからドキュメントとしてのテストを書いて読みやすい形式に変換することもできるし、 「Markdownで書かれたその言語のチュートリアル」をそのままテストとして実行することもできる。 (例: Ruby, Python

REPLをそのままテストにすることの良い副作用として、プリティプリンタが得られる点がある。 例えばSMLでは(Javaのような言語とは違って)値を文字列に変換する関数は一般的には手に入らず、自分で書かなければならない。 また、SMLのテスティングフレームワークであるSMLUnitでは、 テストが失敗した場合の表示のためにアサートする値に対してプリティプリンタをセットで与えなければならない (期待値と実際の値の比較をエラーメッセージの中に含むために使われる)。 これは合理的ではあるもののREPLと単体テストの間の距離を一層離すことにつながっている。

一方でSMLでもREPLを使うならばそのような面倒くささはない。 REPLは自身のプリティプリンタによって値を表示していて、 プログラマがREPLでテストをするときはREPLが出してくれたものを目で見て妥当性を判断しているからだ。

このようにRehearsalのアプローチはREPLの気軽さを損ねずに自動的な回帰テストの利点を取り入れることができる。 こうした方法が「とにかく自動的な回帰テストを回し始める最初の一歩」を容易にすると考えている。


YokohamaUnit 0.3.0 の新機能 [YokohamaUnit]

YokohamaUnit の 0.3.0 をリリースした。

https://github.com/tkob/yokohamaunit/releases/tag/v0.3.0

このリリースには次の2つの新機能が含まれる。

リソース

Javaユニットテストではテストデータをクラスパス上に配置して(リソース)読み込むことがよくある。

Let url = `getClass().getResource("text1.txt")`.

この書き方は面倒くさい。そこでリソースのための特別な文法を用意した。

Let url = resource "text1.txt".

次のようにも書ける。それぞれリソースをInputStream, File, URIとして返す。

Let ins = resource "text1.txt" as `java.io.InputStream`.
Let file = resource "text1.txt" as `java.io.File`.
Let uri = resource "text1.txt" as `java.net.URI`.

一時ファイル

一時ファイルを作成するための式を追加した。

Let tempFile = a temporary file.
Let tempFile = a temp file.

作られたファイルに対しては自動的にdeleteOnExitが呼ばれる。

その他

内部的にJavaslangを使うことにした。 Javaでタプルや関数的リストなどを使うのにこれまでは独自に実装したものを使っていて、自分で書くのも何だなと思っていた。 Functional Javaを検討したこともあったが、 いまいちJavaの中になじんでくれないAPIだったのでやめていた。 Javaslangのほうが比較的使いやすいように感じる。

使用例

nbmtoolsのテストで、上述の新機能を使っているので参考にしてほしい。

https://github.com/tkob/nbmtools/tree/f0cd916a7fa5d28228d6e05a37144413ea808c18/src/test/docy/nbmtools


リモート参照を含むNetBeansのnbmファイルをオフライン化するツール [Java]

NetBeansのプラグインモジュールをオフライン環境下でインストールするには nbmファイルダウンロードしてくればよいが、 このnbmファイルがリモートのファイルへの参照を含んでいる場合、 単独ではインストールに失敗する。

nbmファイルは実態としてはZIPファイルだが、 その中に.externalというファイルが含まれている場合、 NetBeansが.externalの中に書かれたURLからダウンロードして置き換える仕組みになっているからだ。

一方で、この.externalを置き換えたZIPファイルをあらかじめ用意しておけば、 そのようなnbmファイルでもオフラインでインストールできる。 例えば下記のサイトでそのような方法が紹介されている。

NetBeans80にofflineでJUnitをインストール

しかしこれを手動で行うのは面倒だ。ということで自動で行うツールを作った。

nbmtools: https://github.com/tkob/nbmtools

これを使うと上掲の記事の作業は次のようにすればよい。

なお作ってはみたものの.externalを含むnbmファイルはあまり多くない気もしている。

技術的に特筆すべきことはないが、ScalaとJavaの半々でコーディングし、 テストのためにYokohamaUnitをドッグフーディングしている。 とてもひさしぶりにScalaを書いた。


YokohamaUnit 0.2.0 の新機能 [YokohamaUnit]

YokohamaUnit の 0.2.0 をリリースした。

https://github.com/tkob/yokohamaunit/releases/tag/v0.2.1 (バグフィックスの0.2.1に更新)

このリリースには次の2つの新機能が含まれる。

データ変換

テストを書いていると「フィクスチャをテスト可能なデータ型まで変換する」といった、 テストそのものには本質的でない処理がテストコード中にいくつも紛れ込んでしまうことがある。 例えば resources ディレクトリに配置したデータを InputStream から何らかのデータに変換したりといったようなことだ。

そうするとユーティリティメソッドを使って IOUtils.toString(is) のように書くことになるのだが、 あまり美しくない。

0.2.0 導入された機能を使うと is as InputStream と書けるようになる。 まずユーティリティメソッドを Java か何かで書いて、次のようにアノテーションをつける。

@As
public class DataConverters {
    @As
    @SneakyThrows(IOException.class)
    public static String inputStreamAsString(InputStream is) {
        return IOUtils.toString(is, "UTF-8");
    }
}

そうすると YokohamaUnit のテストからは次のように使えるようになる。

Assert `bais as String` is "hello"
where bais = `new ByteArrayInputStream("hello".getBytes("UTF-8"))`.

これを使うためには YokohamaUnit のオプション-converter <base-package> と指定すると、指定したパッケージ以下(カンマ区切りで複数指定できる)から変換メソッドを見つけてくれる。

不変条件チェック

テスト対象に下記のように Invariant アノテーションをつける。

@lombok.AllArgsConstructor
@Invariant("({ it.count >= 0 })")
@Invariant("({ it.count < 10})")
public class DummyCounter {
    @Getter
    private int count;
 
    public void increment() {
        count++;
    }
}

そうすると YokohamaUnit の4フェーズテストの Setup と Exercise の後に、 テスト内容にかからわず常にこの不変条件をチェックし、満たされないとテストが失敗するようになる。

この機能を有効にするには -contract のオプションを指定する。


SMLZIP: Standard ML用のZIPファイル読み込みライブラリ [SML]

Standard MLからZIPファイルの読み込み・解凍をするためのライブラリを作った。

SMLZIP https://github.com/tkob/smlzip

特徴はPure-SMLである(Cバインディングではない)という点である。 機能は読み込みのみで、現状は最低限のことができる程度だ。 PKZIP形式から読み込みたい場合と、Deflate形式を直接扱いたい場合で 異なるMLB/CMを選択できるようになっている。 Smackage から利用できるようにするためのファイルも配置してみた。

バグ等をみつけたらGithubのissueに報告してほしい。


Proglrの最近の進展 [SML]

SML用のパーサジェネレータProglrの最近の進展について。

字句解析の自動生成

Proglrはml-ulexで生成された字句解析器と結合するようになっていて、 これまでは字句解析は自分で書く必要があった。

しかし、凝った字句解析をしない場合はやはり自動で生成してくれるのが便利だ。 というわけで文法定義からml-ulexのソースを自動生成する機能をつけた。

具体的にはtoken Add "+";のようなトークン定義があった場合は 指定された文字列のトークンを字句解析にも追加する。

token Integer of int;, token Double of real;, token Char of char;, token String of string;, token Ident of string; のような定義があった場合はそれぞれのリテラルのトークンを追加する。

これにより簡単な言語を作る場合はml-ulexのソースをまったく書かなくてもよくなる。

example/calc/calc.cfというサンプルがあるのでそれを参照してほしい。

コメント定義の自動生成

同様にコメントについても字句解析に自動追加できるようになった。 下記のような定義でそれぞれブロックコメントと行コメントを追加できる。 これはBNFCと同様である。

comment "/*" */";
comment "//";

各処理系のための雛形の自動生成

各ML処理系でビルドするためのドライバ、Makefileなども自動生成するようにした。 ml-lptランタイムが同梱されていない処理系のために、 ml-lptランタイムのソースも展開されるようにした。

これにより基本的には文法定義ファイルだけから実行可能ファイルを作るまでのすべてが生成可能となった。

MLton, Poly/ML, Alice ML, MLKit, Moscow ML では数行のコマンドラインでサンプルプロジェクトがビルドできるようになっている。 起動方法はREADMEを参照してほしい。

一方でProglr自身をビルドするための処理系としては、 当面MLtonとPoly/MLに注力することにした。 SML#向けのファイルも残っているが、動かないものと思ってほしい。

ちなみにAlice MLのビルドの作法を調べていたのだが、 リンクに関してはまだあまりわかっていない。 コンパイルをすると.alcというオブジェクトファイルができ、 これは依存する.alcがしかるべき位置に配置されていればそのまま実行できる。 依存するものも含めて1ファイルにするためにalicelinkを使う…と思っているのだが、 alicelinkを使ってリンクしたつもりのファイルだけで実行しようとすると やはり依存性のエラーが出てしまう。 このためREADMEではリンクせずに実行するような手順を書いている。

Graphvizファイルの生成

文法定義から作られるオートマトンを視覚化するためのdotファイルを生成できるようにした。 -a 出力ファイル名 というオプションを与えると生成できる。

依存するツール

Proglrが依存している外部ツールを挙げる。 基本的には昔ながらのUnixのツールであるため、 利用できないということはないと思う。

  • m4: ファイル生成時にテンプレート機能として使う
  • Perl: ml-ulexの出力結果にパッチを当てるのに使っている(Alice ML互換にするためのパッチ)
  • Expect: Poly/MLでビルドするときにインタラクティブシェルを制御するのに使う
  • DejaGnu: Proglrのテスト(make check)で使う

JCUnitのテストケースジェネレータを切り出して使う [Java]

JCUnit はJUnitの(サードパーティーの)テストランナーの1つで、 テストの入力となるフィールドにアノテーションをつけておくと、 そのアノテーションを元にしてPairwise法を使ってテストデータを自動生成して実行してくれる。

それはそれで便利なのだが、 今回はJCUnitのPairwiseのアルゴリズムを JCUnitのテストランナー経由ではなくて直接切り出して使いたいと思ったので、 その方法を調べた。

import com.github.dakusui.jcunit.constraint.ConstraintManager;
import com.github.dakusui.jcunit.constraint.constraintmanagers.NullConstraintManager;
import com.github.dakusui.jcunit.core.Param;
import com.github.dakusui.jcunit.core.factor.Factor;
import com.github.dakusui.jcunit.core.factor.Factors;
import com.github.dakusui.jcunit.generators.IPO2TupleGenerator;
import com.github.dakusui.jcunit.generators.TupleGenerator;
 
public class Main {
    public static void main(String[] args) {
        ConstraintManager cm = new NullConstraintManager();
        Factor os = new Factor.Builder().setName("OS")
                .addLevel("Windows")
                .addLevel("Linux")
                .build();
        Factor browser = new Factor.Builder().setName("Browser")
                .addLevel("Chrome")
                .addLevel("Firefox")
                .build();
        Factor bits = new Factor.Builder().setName("Bits")
                .addLevel("32")
                .addLevel("64")
                .build();
        Factors factors = new Factors.Builder().add(os).add(browser).add(bits).build();
        TupleGenerator tg = new TupleGenerator.Builder()
                .setTupleGeneratorClass(IPO2TupleGenerator.class)
                .setConstraintManager(cm)
                .setFactors(factors)
                .setParameters(new Param[0])
                .build();
        tg.forEach(tuple -> System.out.println(tuple));
    }
}

テストケースジェネレータとなるのはTupleGenerator型のオブジェクトである。 TupleGenerator.Builderのfluentなコンストラクタでオブジェクトを作る。

IPO2TupleGeneratorは生成されるTupleGeneratorの実装で、 IPOというのはPairwiseのアルゴリズムの一種のようだ。

ContaraintManagerは変数の組み合わせの制約を指定するためのもののようだが、 今回は何も制約をつけないのでNullConstraintManagerを与えている。

Factorsはテストケースの元となる一連の変数(Factor)の集まりである。 Factorは変数名と変数がとりうる値(level)から成る。

ParamはTupleGeneratorの実装固有のパラメタである。 ここでは何も指定しない(デフォルト)ので空の配列を与えている。

このParamというクラスは実際にはアノテーションクラスで、 普通はJUnitテストクラスに書かれたアノテーションがそのまま来るようだ。 もし今回のような使用方法でParamを指定するとしたら、 ちょっとまどろっこしい書き方をすることになると思う。

TupleGeneratorはIterable<Tuple>を継承しているので、 ここではforEachメソッドで生成結果を取得している。 TupleはMap<String, Object>を継承していて、キーが変数名、値が変数の値となる。

上記のコードの実行結果は次のようになる。

{Bits=32, Browser=Chrome, OS=Windows}
{Bits=64, Browser=Firefox, OS=Windows}
{Bits=64, Browser=Chrome, OS=Linux}
{Bits=32, Browser=Firefox, OS=Linux}

2つの値を持つ変数3つの組み合わせは単純に積を取ると8通りとなるが、 ここでは4通りのみ生成されている。 その一方で、任意の2つの変数の組み合わせはすべて登場するようになっている。


Tcl 8.6 のインラインアセンブラを使って簡易言語を作る [Tcl]

Tcl 8.6から::tcl::unsupported::assembleというコマンドでインラインアセンブラが使えるようになっていた。 これを使うとTclの仮想マシンのバイトコードに対するアセンブラをTclコード中に書くことができる。

unsupportedという記載からもわかるように公式なドキュメントはないが、 故意にエラーを出すことによってエラーメッセージから使い方を忖度することができる。

例えば ::tcl::unsupported::assemble help とすると命令の一覧が出てくる。 (helpというサブコマンドがあるわけではなくて、存在しない命令を与えた時のエラー)

% ::tcl::unsupported::assemble help
bad instruction "help": must be push, add, append, appendArray, appendArrayStk, appendStk, arrayExistsImm, arrayExistsStk, arrayMakeImm, arrayMakeStk, beginCatch, bitand, bitnot, bitor, bitxor, concat, coroName, currentNamespace, dictAppend, dictExists, dictExpand, dictGet, dictIncrImm, dictLappend, dictRecombineStk, dictRecombineImm, dictSet, dictUnset, div, dup, endCatch, eq, eval, evalStk, exist, existArray, existArrayStk, existStk, expon, expr, exprStk, ge, gt, incr, incrArray, incrArrayImm, incrArrayStk, incrArrayStkImm, incrImm, incrStk, incrStkImm, infoLevelArgs, infoLevelNumber, invokeStk, jump, jump4, jumpFalse, jumpFalse4, jumpTable, jumpTrue, jumpTrue4, label, land, lappend, lappendArray, lappendArrayStk, lappendStk, le, lindexMulti, list, listConcat, listIn, listIndex, listIndexImm, listLength, listNotIn, load, loadArray, loadArrayStk, loadStk, lor, lsetFlat, lsetList, lshift, lt, mod, mult, neq, nop, not, nsupvar, over, pop, pushReturnCode, pushReturnOpts, pushResult, regexp, resolveCmd, reverse, rshift, store, storeArray, storeArrayStk, storeStk, strcmp, streq, strfind, strindex, strlen, strmap, strmatch, strneq, strrange, strrfind, sub, tclooClass, tclooIsObject, tclooNamespace, tclooSelf, tryCvtToNumeric, uminus, unset, unsetArray, unsetArrayStk, unsetStk, uplus, upvar, variable, verifyDict, or yield

pushという命令について知りたければ同様にエラーを出してみる。

% ::tcl::unsupported::assemble push
wrong # args: should be "push value"

この調子で調べていくと基本的な命令については何となくわかってくる。 そこでTclバイトコードをターゲットとした簡単な言語を作ってみた。

コンパイラはSMLで、パーサジェネレータとしてProglrを使って作る。

以前の記事 をOCaml+BNFC+JavaからSML+Proglr+Tclに変えて行ったものと思えばよい。 ソースコードの全体はGistにアップロードした。

文法定義

文法定義は下記の通り。

token Add "+" ;
token Sub "-" ;
token Mul "*" ;
token Div "/" ;
token LParen "(" ;
token RParen ")" ;
token Eq "=" ;
token Comma "," ;
token Semi ";" ;
token FunKw "fun" ;
token LetKw "let" ;
token InKw "in" ;
token IfKw "if" ;
token ThenKw "then" ;
token ElseKw "else" ;
token Integer of int;
token Ident of string;
token String of string;
 
Grm. Grm ::= [Top] ;
separator Top ";" ;
 
Fun. Top ::= "fun" Ident "(" [Param] ")" "=" Exp ;
Exp. Top ::= Exp ;
 
separator Param "," ;
Param. Param ::= Ident ;
 
Let. Exp ::= "let" Ident "=" Exp "in" Exp ;
Cnd. Exp ::= "if" Exp "then" Exp "else" Exp ;
 
separator Exp "," ;
 
Add. Exp1 ::= Exp1 "+" Exp2 ;
Sub. Exp1 ::= Exp1 "-" Exp2 ;
 
Mul. Exp2 ::= Exp2 "*" Exp3 ;
Div. Exp2 ::= Exp2 "/" Exp3 ;
 
App. Exp3 ::= Ident "(" [Exp] ")" ;
Int. Exp3 ::= Integer ;
Str. Exp3 ::= String ;
Var. Exp3 ::= Ident ;
 
coercions Exp 3;

言語は関数定義と式の連続であり、式中には条件分岐とローカル変数と四則演算と関数呼び出しが書ける。

ProglrはGLRだが、この文法はLALR(1)になっているはずである。 これを確認するには、最初のtokenの行を省くとBNFCの文法定義と互換性があるので、 BNFCに食べさせてocamlyaccに通す。衝突が報告されないことでLALR(1)であることを確認できる。

Proglrでは字句解析についてはml-ulexで行うのだが、割と自明なので掲載を省く。

これをProglrに通すとパーサーと抽象構文木のデータ型のSMLソースコードが生成される。

コンパイラ

メインとなるProglrのドライバは次のように書く。

fun main () =
  let
    val strm = Lexer.streamifyInstream TextIO.stdIn
    val sourcemap = AntlrStreamPos.mkSourcemap ()
    val ast = hd (Parse.parse sourcemap strm)
  in
    check ast;
    compile ast
  end

何故Parse.parseのhdを取っているのかといえば、Parse.parseが構文木のリストを返すからだ。 これはProglrが一般にCFGを扱うからで、CFGの文法は多義的でありうる。

checkは未定義のローカル変数の使用と、ローカル変数の名前の衝突をチェックする。 後者についてはシャドウイングされるものとみなして名前の付け替えをしてもいいと思うが、 今回は単にチェックするだけにした。

fun nameOf (Param (_, name)) = name
 
fun mem (x, []) = false
  | mem (x, y::ys) = x = y orelse mem (x, ys)
 
fun check (Grm (span, tops)) = List.app (fn top => checkTop (top, [])) tops
and checkTop (Fun (span, name, params, body), env) =
      checkExp (body, map nameOf params)
  | checkTop (Exp (span, exp), env) = checkExp (exp, env)
and checkExp (Let (span, name, value, body), env) = (
      checkExp (value, env);
      if mem (name, env) then raise Fail ("dup var: " ^ name)
      else checkExp (body, name::env))
  | checkExp (Cnd (span, cond, t, f), env) =
      (checkExp (cond, env); checkExp (t, env); checkExp (f, env))
  | checkExp (App (span, rator, rands), env) =
      List.app (fn rand => checkExp (rand, env)) rands
  | checkExp (Add (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Sub (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Mul (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Div (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Int (span, int), env) = ()
  | checkExp (Str (span, str), env) = ()
  | checkExp (Var (span, var), env) =
      if mem (var, env) then () else raise Fail ("unknown var: " ^ var)

Tclの仮想マシンはJavaVMと同様のスタックマシンである。 したがってコンパイルは前回の記事と大体同様である。

local
    val n = ref 0
in
    fun newLabel () = "label" ^ Int.toString (!n) before n := !n + 1
end
 
fun println s = (print s; print "\n")
 
fun compile (Grm (span, tops)) = List.app compileTop tops
and compileTop (Fun (span, name, params, body)) = (
      println ("proc " ^ name ^ " {" ^ String.concatWith " " (map nameOf params) ^ "} {");
      println ("::tcl::unsupported::assemble {");
      compileExp body;
      println ("}");
      println ("}"))
  | compileTop (Exp (span, exp)) = (
      println ("::tcl::unsupported::assemble {");
      compileExp exp;
      println ("}"))
and compileExp (Let (span, name, value, body)) = (
      compileExp value;
      println ("store " ^ name);
      println "pop";
      compileExp body)
  | compileExp (Cnd (span, cond, t, f)) =
      let
        val falseLabel = newLabel ()
        val trueLabel = newLabel ()
      in
        compileExp cond;
        println ("jumpFalse " ^ falseLabel);
        compileExp t;
        println ("jump " ^ trueLabel);
        println ("label " ^ falseLabel);
        compileExp f;
        println ("label " ^ trueLabel)
      end
  | compileExp (App (span, rator, rands)) = (
       println ("push " ^ rator);
       List.app compileExp rands;
       println ("invokeStk " ^ Int.toString (length rands + 1)))
  | compileExp (Add (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "add")
  | compileExp (Sub (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "sub")
  | compileExp (Mul (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "mult")
  | compileExp (Div (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "div")
  | compileExp (Int (span, int)) = println ("push " ^ Int.toString int)
  | compileExp (Str (span, str)) = println ("push {" ^ str ^ "}")
  | compileExp (Var (span, var)) = println ("load " ^ var)

実行

コンパイラはtalという実行ファイルになるようにした。 Tcl上での実行を簡単にするためにシェルスクリプトtalexecを書く。

#/bin/sh
 
TEMP=$(mktemp)
tal < $1 > $TEMP
tclsh $TEMP
rm -f $TEMP

ソースファイルを次のように書くと、

fun f(x) =
  if x then x * f(x - 1)
  else 1;
 
puts(f(10))

実行結果はこうなる。

$ talexec fact.tal
3628800

ここでf関数はTclのプロシージャとして定義される。 putsはTclの組み込みのコマンドである。 関数呼び出しの構文で任意のTclコマンドを呼び出すことができる。

使用した命令

命令 説明
push value valueをスタックに積む
pop スタックから1つ取り出す
store varname スタック最上位の値を変数varnameに格納する。スタックは変更されないので注意。
load varname 変数varnameの値をスタックにロードする
jumpFalse label スタック最上位の値が偽のときlabelにジャンプする
jump label 無条件ジャンプ
label name ラベルの定義
invokeStk count スタックの内容でプロシージャ(コマンド)を呼び出す。countは引数の数+1
add スタックの2要素を取り出し、和をスタックに置く
sub スタックの2要素を取り出し、差をスタックに置く
mult スタックの2要素を取り出し、積をスタックに置く
div スタックの2要素を取り出し、商をスタックに置く

感想

今のところTclのVMに関してあまり特殊なことや優位性があるようには思われないので、 一般的な言語処理系のターゲットとする価値があるかというとなさそうである。 アセンブリにオリジナルソースファイルの行番号を埋め込む方法が無いようである点も不利である。

しかしTclの中でDSLをコンパイルして使いたかったり、 Tclと何らかの密な連携を必要とする場合はインラインアセンブラを使う手もあるだろう。


前の10件 | -

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。

×

この広告は1年以上新しい記事の更新がないブログに表示されております。