はじめに
プログラミング言語を作ってみたいと思ったことはありませんか? あるいは、どうして単なるテキストファイルに過ぎないソースファイルがプログラムとして実行できるのか、仕組みを知りたいと感じたりしたことはありませんか? 仕組みを知るには実際に作ってみるのが一番です。結局、どっちにしてもプログラミング言語を作ることになりますね。
ところが仮にそう思ったとして、プログラミング言語についての本を手に取ると延々と字句解析(ソースファイル中の文字列を定数や演算子などの分解すること)について説明しているのでせっかく湧いた興味がみるみるうちに醒めてしまう、そんな経験はありませんか。もちろん、「本物」のプログラミング言語を作るにはそこでますます熱意が湧いてくる必要があるのでしょう。でも、単に興味があるだけならもっと簡単に、いきなりプログラミング言語そのものを作れたほうが良いじゃないですか。
というわけで、字句解析と構文解析(字句解析の結果から文の構成を調べること)抜きでいきなりプログラミング言語をプログラムしてみましょう。これらの解析処理は人間がソースファイルを記述する時点で済んでいることにしてしまえば良いからです。具体的にはプログラミング言語のソースファイルをXMLで記述し、SAXパーサから要素や文字を読み込んだ時点で直接、構文解析木を作成します。その結果、すぐに実行可能なプログラミング言語が実装できます。
対象読者
ここでは、実装言語としてJavaを利用します。
対象読者へのもうひとつの「はじめに」
この記事のプログラムはJavaによる以下の実装技術の実例として構成しています。
XMLの利用
SAXパーサを直接利用してプログラム固有のオブジェクト構造(ここではInterpreterパターンのインスタンス)を生成します。
各種デザインパターンの適用
記事中で多少の解説を試みますが、Interpreterパターンは当然として、Strategyパターン、Factoy Methodパターン、Abstract Factoryパターン、Singletonパターン、Builderパターンなどの実装例としても参照できます。
必要な環境
J2SE 1.4以上を想定します。サンプルはjavacに「-source 1.4」をオプション指定してコンパイルしたものを1.5.0上で実行して動作確認をしています。
その他に用意しておいたほうが良いもの
- ant 1.6以上またはNetBeans4.0以上
- JUnit 3.8.1
目標
実装範囲
仮にもプログラミング言語なのですから、変数、代入、制御構文としてif―then―else
とwhile
を実装しましょう。また、Javaで開発するのですから、Javaのオブジェクトも利用できた方が良いですね。後はプログラミング言語自身のデバッグのためにもコンソール出力機能は用意しましょう。
以上のことから、とりあえず、次のJavaのプログラムに相当する処理を実行可能なプログラミング言語の実装を目標とします。
public class Sample { public static void main(String[] args) { if (args.length == 0) { System.out.println("no args"); } else { int i = 0; while (args.length > i) { System.out.println(args[i]); i++; } System.out.println("done"); } } }
言語デザイン
どういう言語にするかデザインするのは本来一番おもしろい部分の1つですが、ここでは実装方法を示すことが目的ですので、できる限りにシンプルにします。また、細かいことは決めないで、とにかく実装範囲を楽に実装することを優先しましょう。とりあえず、ここでは次のXML(sample.xml)を実際のソースファイルとして想定します。
<?xml version="1.0" encoding="shift_jis"?> <oreprog> <if> <condition type="eq"> <call name="args" method="size"/><value type="int" value="0"/> </condition> <block> <print><value type="string" value="no args"/></print> </block> <block> <var type="int" name="i" value="0"/> <while> <condition type="gt"> <call name="args" method="size"/><value name="i"/> </condition> <block> <print><call name="args" method="get"><value name="i"/> </call></print> <var type="int" name="i"> <add><value name="i"/><value type="int" value="1"/></add> </var> </block> </while> <print><value type="string" value="done"/></print> </block> </if> </oreprog>
XMLの要素名を制御構文や演算子として対応させていることが、目標のプログラムと読み比べると理解できると思います。要素名と構文木のノードを直接対応させることで、XMLの読み込みと同時に構文木を生成できるようにしてあるわけですね。
なお、本記事では以降、ここで作成するプログラミング言語をルート要素名から「oreprog(オレプログ)」と呼ぶことにします。
ファイル構成
ダウンロードしたファイルはzipで圧縮してあります。展開すると「intp」というディレクトリを頂点としたディレクトリ階層ができます。すぐに実行できるようにコンパイル済みのクラスファイルも添付してあります。また、ソースファイルはすべてシフトJISでエンコードしています。
intpディレクトリ
build.xml
antのビルドスクリプトです。
intp\src\com\example\progディレクトリ
OreProg.java
プログラム本体です。ファイル数の抑制のために小さなクラスはstatic
内部クラスとして実装しているので、本記事では主としてこのソースファイルについて解説します。
ValueExpression.java
直定数(value
要素に対応)処理クラスです。
リフレクションを利用して文字列からオブジェクトを生成します。サポートしているのは、int
、long
、String
(xmlへの記述時はstring
と小文字になります)、コンストラクタの引数に1つのString
を取るオブジェクトの生成です。
VariableExpression.java
変数(var
要素に対応)処理クラスです。ValueExpression.java
を継承しています。
CallExpression.java
オブジェクトのメソッド呼び出し(call
要素に対応)処理クラスです。
リフレクションを利用して与えられたオブジェクトのメソッドを呼び出します。プログラムを短くするために、メソッドの検索については非常に限定された機能しか実装していません。
ConditionalExpression.java
条件判断(condition
要素に対応)処理クラスです。
intp\src\com\example\exprogディレクトリ
ExProg.java
OreProg.java
を継承し、oreprogに独自の言語拡張を実装するサンプルプログラムです。
元のOreProg
に手を入れずに、新たな構文要素としてint
型の定数を導入しています。
intp\testディレクトリ
sample.xml
sample.xmlです。
test.xml
筆者がテストに利用したxmlです。
extest.xmll
ExProg
のテストに利用したxmlです。
intp\test\com\example\prog\OreProgTest.java
JUnitを利用したテストプログラムです。value
、 var
、call
、if
、condition
、block
、while
要素をテストします。
intp\build\classes
ビルド後のクラスファイルを格納してあります。antのデフォルトターゲットは、このディレクトリへクラスを生成します。
サンプルの実行
コマンドラインで、zipを展開したディレクトリ(intp)に入り、
set CLASSPATH=[展開したディレクトリ名]\intp\build\classes;%CLASSPATH% java com.example.prog.OreProg test/sample.xml [引数 ...]
と入力することで実行できます。なお、CLASSPATH
環境変数の設定はご利用の環境に合わせて調整してください。以下はMac OS Xでの実行例です。
$ cd /var/tmp/intp $ export CLASSPATH=/var/tmp/intp/build/classes:$CLASSPATH $ java com.example.prog.OreProg test/sample.xml no args $ java com.example.prog.OreProg test/sample.xml a b c a b c done
引数で与えた文字列が順次プリントされ、最後に「done」と出力されることが確認できます。
なお、J2SE 1.4.2で実行する場合(J2SE 1.4.2_08で確認)は以下のように-Dオプションを使用してSAXパーサの指定が必要となります。
$ java -Dorg.xml.sax.driver=org.apache.crimson.parser.XMLReaderImpl com.example.prog.OreProg test/sample.xml a b c
指定しない場合、
Exception in thread "main" org.xml.sax.SAXException: System property org.xml.sax.driver not specified
という例外となります。