katsyoshi/arpaka - GitHub

前回は、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 と呼ぶ範囲をどこで決めるのかを考えます。