プログラミング言語をつくってみる④ - クラス

クラスができました。


抽象木をもう一段階変換する

ここまでサクサクと作ってきたのですが、ジェネレートされたパーサから得られる抽象木は、処理の際に、毎回 Chidlren の中身を確認する必要があるなど、実行に不向きです。

非効率でもいいかとも思っていたのですが、実行処理があまり直感的ではないため、デバッグが大変になってきました。また、単純に変数や定数にアクセスするだけでも、 [expr] - [factor] - [incdec] - [primary] - [variable] と巡らないと行けないため超非効率かつ、ステップ実行でエラー探しも大変です。とはいえ、文法定義のときに処理のことまで考えながら作るのもそれはそれで大変です。

そこで、一回、もうちょっと直感的な抽象木に変換してから、それを実行するような方式にしました。変換後には、子が一つだけで何もしないような余分な階層をなくしたり、定数と変数の参照を区別したり、全て statement で管理していたものを文の種類ごとに別のクラスのオブジェクトにするようにしました。これで、実行中に余分な分岐もなく、少し効率的に実行できるようになったと思います。

もう一つ、構造変更によって、expr (中身は二項演算)の左辺と右辺の直下が定数かどうかの判断もしやすくなったため、定数の畳込みも実装しました。実装方法は、expr を管理するクラスの左辺と右辺のオブジェクトが定数を表すものなら、expr を計算してしまって、定数のオブジェクトに置き換えてしまう形にしました。この実装ですと、このパーサでは、

a + 6 + 5
(a + 6) + 5
と解釈されるため畳み込みが行われませんが、まぁ、これは諦めました。

クラスを追加する

文法を次のように変更し、クラスの構文を追加しました。そのほか、null を導入したりと微修正しています。

追加したのは、member 、new_object 、def_class 、class_body 、def_constructor くらいです。ドットでメンバアクセスできるようにしました。また、コンストラクタは、クラス名ではなく "constructor" で定義するようにしました。これは、構文解析が楽かな?という理由もありますが、クラス名を変更したときに毎回コンストラクタの名前も変更しないといけないのは面倒なので、このようにしています。(C# ならリファクタリング効きますが、C++ ですと IDE が変更し逃したりしていつも大変ですので…)

継承は多重継承は考えず、単一継承のみで、構文は C# 風に ":" で指定できるようにしました。

クラスの定義とオブジェクトの生成

def_class の評価の際に、環境に型情報を記録するようにしました。型情報には、メンバ変数名と初期化の抽象木、メンバ関数やコンストラクタを記録します。

オブジェクトの生成は、new 型名 () で行えるようにしました。オブジェクトの生成時、メンバ変数をパラメータに登録して初期化を行います。一方、メンバ関数は登録せず、その代りに、オブジェクトからメンバ関数が呼ばれたときに、型情報の方に記録した関数の抽象木を実行するようにしました。

オブジェクトを表すクラスは、SsEnvironment を継承しており、オブジェクトの Outer にグローバル環境を設定しています。そのようにすることで、クラスのメンバ関数の処理では、this などのキーワードなしに、オブジェクトのパラメータを参照できるようにしました。ついでに、"this" キーワードを調べると、そのオブジェクト自身を返すようにしました。

メンバの呼び出し

メンバ関数の呼び出しは以下の感じです。左から順に評価していっています。ただし、メンバを参照するときは、評価時の第二引数に対象オブジェクトを渡し、そちらからメンバを探すような処理にしました。ここは、もう少しきれいにならないかと思いますが、まぁ、動いているので良しとします。

コード1: メンバの呼び出し関連

メンバ変数への設定

値の設定は若干大変でした。. でつないだ最後から 2 番目の評価値(オブジェクト)の一番最後の名前のメンバに値を設定しないといけません。これまでは、設定先の名前を返す関数を用意して、その名前 + env で設定していたのですが、設定先も変わるため、設定情報を格納したオブジェクトを返すような方式にしました。

エラーを表示する

今作っている処理系では、例外が発生するとそれに関連するトークンも返してくれるようになっています。また、トークンには、行番号と列番号が記録されています。そこで、その情報を使って、CodeMirror 上のトークンをハイライトする機能を作りました。

結構簡単で、CodeMirror の markText 関数を使うだけでした。

コード2: エラー箇所をハイライトするコード
図1: エラー箇所を赤くハイライト

CodeMirror には、行番号をクリックするとブレークポイントを設置する(表示をする)など、IDE を作るベースとなる機能が揃っているようです。簡単な IDE くらいはなら、CodeMirror を使って作ると簡単そうです。

配列(インデクサ)とプロパティ(C# とかにあるやつ)

クラスができたので、組み込みクラスの一つとして配列を実装しました。作っている処理系では、JavaScript の関数もクラスのメンバ関数として登録できますので、組み込みクラスを作ること自体は簡単です。また、これまで作ってきた機能のみで、例えば、Get(index) 、Set(index, Value) といったメソッドを定義すれば十分に配列として機能してくれます。ただ、arr[i] といった表記ができませんので、それを定義できる構文を追加しました。C# でいうインデクサです。

構文は迷いましたが、func のかわりに _index_get と _index_set と書けば定義できるようにしました。C# 風に、例えば、

_index 名前 { get{} set{} }
とかでも良かったのですが、修正が面倒だったたとこと、あまりあの構文が好きではないため、関数定義風の文法にしました。ついで、外部からメンバ変数風にアクセッサにアクセスできる C# 風のプロパティも作りました。(プロパティ、結構好きなんですよね)

コード3: インデクサとプロパティの文法追加

次は実装です。インデクサの Get 方向は、登録されているインデクサ用関数を探しそれに引数を渡して呼び出すだけで OK です。値の設定は、メンバ変数への設定のところで作った機構を利用し、対象のインデクサ関数を呼び出すようなオブジェクトを返すことで対応しました。プロパティも大体同じような方法で実装しました。以下、その辺りのコードです。

コード4: インデクサとプロパティ

言語の機能ができたので、組み込み配列を環境に登録しました。以下のような感じで登録可能です。ちょっと書くことが多いですが、まぁまぁだと思います。

ここまで作れば、以下のようなコードが実行可能です。

コード5: テストコード
コード6: テストコードの実行結果

配列の初期化がまだですが、ここまでできれば簡単そうなので省略と…(文法を決めかねているのもあります)

まとめ

抽象木もスッキリし、クラスの実装も終わり、欲しかったインデクサとプロパティもできたので、個人的にはかなりいい感じになってきたと思います。ここまで作ってみると、言語の作り方もだいぶわかってきました。

色々と使って不具合が無いかを確認してから、そろそろこのバージョンは一旦終了しようかと思います。その次は、実用に向けて、静的型付け言語の制作に取り掛かりたいです。

ここまでの進捗は、こちらに置いてあります。