Lrama の Ruby Parser を Ruby で組みたてたい v 0.2.0
前回は、Ruby の
parse.y を読み込み、Action を Ruby 側へ移植して、Ruby のソースを parse できる
ところまで進めました。
今回は、その Parser の結果をどう返すかを整理します。ここで作ったものをコードの中では
AST と呼んでいますが、Ruby の構文全体を表す AST を完成させたわけではありません。
この時点で必要だったのは、Class や Module と、それに伴う Method の定義を取り出し、
木構造として返すことでした。
Class / Module の定義を追加したのは、59728ac です。そこへ Method の定義を追加した ab6028f を重ねています。その後、Ruby のソースから standalone frontend を生成する形を d91c60b で整理しました。
Parser として利用するための入口
最初の Lrama::Ruby は token 列を直接受け取る Parser でした。
これは文法や Parser の動作を確認するには十分ですが、Ruby の Parser として利用するには
少し低い API です。
Ruby のソースを渡して、Parser が必要な Lexer や Builder を内部で使い、結果を返す形にします。
frontend = Arpaka.compile(
ruby_source: "/path/to/ruby",
class_name: "RubyParser"
)
tree = frontend.parse(
"class Example\n def call\n end\nend\n",
filename: "example.rb"
)
呼び出し側が token を作ったり、Lexer の状態を管理したりする必要はありません。
ここで必要なのは Parser の内部を直接操作することではなく、Ruby のソースを渡して
結果を受け取る入口です。frontend.parse の戻り値である tree が、Parser が構築した
定義木です。この例なら、Example という Class と、その中の call という Method を
辿れる結果を返します。
この結果を置く名前として、実装では先に AST という名前空間を用意しました。これは、
この時点ですでに Ruby 全体の AST を設計し終えていたという意味ではありません。Class や
Method 以外の構文をあとから追加できる場所を、Parser の戻り値として先に決めておきたかった
からです。実際の v0.2.0 で返している情報は、定義を中心にした小さな木構造です。
何を木構造にするのか
Parser の結果をそのまま Action の一時的な値として返すのではなく、まず定義の構造を 木にします。対象にしたのは、Class や Module の入れ子と、そこに定義された Method です。
Source
└── Class Example
└── Method call
この木を使えば、アプリケーション側はソース全体の式を再解釈しなくても、どの Class の中に どの Method が定義されているかを辿れます。今回必要だったのは、式の細部をすべて保持する ことではなく、まずこの定義構造を Parser の結果として返すことでした。
どこまでやるのか
今回は Ruby 全体の構文を完全に表すことを目標にしません。Class、Module、Method の定義を 取り出せるところから実装し、まだ必要でない式の細部は対象外にしました。
そのため、次の二つは分けて考えます。
- Parser が入力を文法的に受け付けられるか
- 受け付けた入力から、定義木を構築できるか
入力を parse できても、Class や Method の構造を期待した形で返せるとは限りません。 現在の実装で扱えない構文は、曖昧な値にして返すのではなく、未対応の構文として扱います。 何が木に含まれていないのか分からなくなると、利用側で結果を使えないからです。
Rails のソースや Ruby Trick の入賞作品も Parser に渡して、定義木をどこまで構築できるのかを 確認しています。これは Ruby 全体との互換性を保証するチェックではなく、単純なサンプルだけ では見つからない文法上の問題を見つけるための確認です。
互換性は後から考える
Ruby Parser として使う以上、既存の Ruby Parser と結果を比べたくなります。ただ、この時点で 必要だったのは、どの AST 表現を正解にするかを決めることではありませんでした。
RubyVM::AbstractSyntaxTree.parse は比較対象にはできますが、Ruby の内部実装に依存した
表現です。Parser の結果として何を返すかが固まっていない段階で、RubyVM AST のクラス構造を
そのまま契約にするつもりはありませんでした。
この時点では、互換性を自動で確認する機構もリポジトリにはまだありません。まず Parser を 動かし、定義木を返せるようにする。その後で、どの Parser のどの表現と比較するのかを考える 順番です。
まとめ
今回は、Ruby のソースを受け取る Parser の入口を用意し、Class、Module、Method の定義を 木構造として返せるようにしました。
- Parser の内部を直接操作せず、
parse(source, filename:)を入口にする - Class / Module と Method の定義を木構造として取り出す
ASTという名前は将来の拡張のために先に置く- Ruby 全体の AST や互換性を、この段階で固定しない
Parser を動かすことと、Parser の結果をどう表現するかは別の問題でした。次は、この定義木を どこまで拡張するのか、そして AST と呼ぶ範囲をどこで決めるのかを考えます。