Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: 2024年までのPrismパーサーの長い歴史を振り返る(翻訳)

概要

CC BY-NC-SA 4.0 Deedに基づいて翻訳・公開いたします。

CC BY-NC-SA 4.0 Deed | 表示 - 非営利 - 継承 4.0 国際 | Creative Commons

日本語タイトルは内容に即したものにしました。
syntax treeは「構文ツリー」と訳しています。lexerは英ママとしています。

Ruby: 2024年までのPrismパーサーの長い歴史を振り返る(翻訳)

Ruby 3.3.0では、Prismと呼ばれる新しい標準ライブラリがCRubyに追加されました。PrismはRuby言語を解析するパーサーであり、Cライブラリ版(CRubyでもオプションで利用可能)とRubyライブラリ版(Ruby gemとして利用可能)の両方が公開されています。Prismプロジェクトは多くの開発者の何年にもわたる労力が注ぎ込まれており、Shopify、CRubyのコアコントリビュータを始め、その他にも多くのRuby実装者やRubyツール開発者によるコラボレーションの成果が結集されています。

本記事では、Prismプロジェクトの概要、すなわちPrismプロジェクトの存在理由、現況、および将来について解説します。また、Rubyパーサーや中間表現、ツールのエコシステムについてより広範囲の洞察も得られます。おそらく既にご存知のプロジェクト(Ripperなど)や、まだあまり馴染みのない新しいプロジェクト(Lrama)なども登場します。

忙しい人向けの結論を先にまとめておきます。
何らかの理由でRubyコードを解析する必要が生じたら、今後はPrismライブラリをお使いください。CRubyコアチームが今後どのような決定を下そうと、このライブラリはRubyパーサーAPIの決定版として恒久的に存続することが保証されています。Prismはドキュメントも充実していてエラー許容性もあり、すべての主要なRuby実装に移植可能で、将来の改善への明確な道筋があります。

Ruby言語のコンパイラフロントエンド1には長くて複雑な細切れのストーリーがあります。ここには多くのプロジェクトが含まれており、それぞれが独自の(時には互いに相容れない)目標を掲げていました。現状を理解するには、Rubyのフロントエンドの歴史を振り返り、Rubyが長年の間にどのように進化を遂げたかについて知る必要があります。理解すべき内容が多いので、なるべく簡潔な説明を心がけるつもりです。

🔗 歴史

Rubyが作られたのは1993年初頭のことでした。この時代の言語設計者たちは、Yacc(Yet Another Compiler Compiler)と呼ばれるツールを用いてパーサーを生成する方法を広く採用していました。Yaccは、言語構文を記述する文法ファイル(.yで終わる)を受け取って、その言語で用いるパーサーを生成します(ここでは.cで終わるC言語ファイル)。Matzもこのアプローチを採用したので、Ruby 0.01に組み込まれていた最初のパーサーはYaccで生成されたものでした。

🔗 1994-01-07: CRuby 0.06

バージョンが明確に記載されている最も古いchangelogエントリは、CRuby 0.06でした。このときのパーサーはYaccで生成されたC言語ファイルでした。

現在のRubyフロントエンドは、このときのRubyが「ツリーウォーク(tree-walk)」インタプリタであったことが基本となっています。つまり、構文ツリーが生成された後で、Rubyランタイムがこのツリーをたどってコードを実行するという意味です。この方式は、現在のCRubyランタイムであるYARVがバイトコードインタプリタであるのとは対照的です。構文エラーも警告もその他の診断情報も、すべてパーサー自身によって生成され、パーサーはランタイムと密結合していました。生成される構文ツリーは、分析や変換の利便性を念頭に置いておらず、明らかにスピーディな実行を目指して設計されていました。

ここが非常に重要なポイントであり、時間をかけて検討する価値があります。この影響は現在のCRubyの構文ツリーにも見られます(ruby --dump=parsetree -e [SOURCE]を実行すれば、解析後のツリーを表示できます)。

以下のコード例が構文ツリーではどのように表現され、実際のソースコードとどう異なるかを考えてみましょう。

  • for left, right in elements do end
  • def foo = return :bar
  • puts "World!"; BEGIN { puts "Hello!" }
  • /foo #{bar}/o

インタプリタやコンパイラで使われる構文ツリーの表現は、ソースコードからかけはなれた形になっていても実行効率が高ければ意味があります。しかしそうした構文ツリーは、それ以外の用途では大幅に扱いにくくなります。

🔗 1995-12-21: CRuby 0.95

1995年末にリリースされたRuby 0.95では、リポジトリのルートディレクトリにToDoファイルが置かれていて、そこには「手書きのパーサー(再帰降下)」というエントリが新たに付け加えられていました。手書きの再帰降下パーサーとLALRパーサーで生成されたパーサーとの違いについては既に過去のPrismアナウンス記事でも述べたので、ここでは繰り返しません。なお私たちは、Rubyのような言語では手書き再帰降下パーサーがベストの選択であると信じており、この初期バージョンを見る限り(少なくとも当時の)Matzは同意していたようです。

🔗 2000-10-01: nodeDump

2000年になると、Dave Thomasが、RubyのAST(Abstract Syntax Tree: 抽象構文木)をたどってドキュメントを生成するnodeDumpプロジェクトを発足させました。私たちの知る限りでは、これはCRubyランタイムの外部からRubyの構文ツリーにアクセスして操作するという最初の試みでした。このプロジェクトはRubyが1.9になったことで廃止され、現在はメンテナンスされていませんが、Rubyが公開された初期の頃から、既にRubyパーサーAPIを求める声があったことには触れておく価値があります。

🔗 2001-09-10: JRuby

2001年には、Jan Arne PetersenがJRubyでRubyの再実装を開始しました。これは、Ruby 1.6コードを直接移植したJVM(Java仮想マシン)で動作します。JRubyは現在も健在で、今日にいたるまでproductionシステムで利用されています。JRubyのパーサーは、(Yaccへの入力として使われる)parse.y文法ファイルのコピーをJava向けに書き直したものが使われています。それ以来、JRubyでは同じプロセスを通じてRuby文法の変更を手動で取り入れてきました。

もうひとつ重要な点は、CRuby用の文法ファイルが変更されると、そのたびにJRubyの文法ファイルも変更されるということです。これはJRubyに限った話ではなく、独自開発されたRubyパーサー(初期から数えて14個)でも同じことを繰り返さなければならなかったのです。この作業への投資には大変な時間と労力を要します。

にわかには信じがたいことですが、今やJRubyの最新リリースでは、CRuby 3.3.0(本記事執筆時点の最新版)の構文をほぼすべてサポートしています。ここで「ほぼ」と言っているのは、JRubyのパーサーとCRubyのパーサーには細かな違いが初期の頃から存在しているからです。JRubyは長年にわたって最も網羅的な代替Rubyパーサーであり続けていますが、さまざまな相違点を100%同じにするのは非常に難しいことです。

🔗 2001-10-20: Ripper

2001年のRuby 1.7あたりの時代に、Aoki MineroがRipperライブラリの最初のバーションをリリースしました。Ripperは、ユーザーが独自の構文をビルドできるイベント駆動型パーサーでした。Ripperは、Rubyの文法ファイルをコピーして、ユーザー定義のメソッドを呼び出すイベントをディスパッチするアクションを変更する形で動作していました。これはもともと独立したプロジェクトとして存在していましたが、その後メンテナンスが困難になることが判明し、それから3年後、最終的にCRubyに統合されました。

Ripperは現在も標準ライブラリとして存在しています。かれこれ20年経ってもドキュメントの冒頭に"Ripper is still early-alpha version."という警告文が書かれたままですが、今も以下のような多くのプロジェクトでパーサーとして選択されています。

こうしたプロジェクトでRipperが選択された理由は無数にありますが、中でも「標準ライブラリにあるパーサーはRipperしかなかった」「最新バージョンのCRubyを正確に解析できる保証があるパーサーはRipperしかなかった(IRBのようなツールではある意味唯一の制約でしょう)」の2つが目立ちます。

🔗 2003-08-04: CRuby 1.8.0

2003年にリリースされたCRuby 1.8.0は、ツリーウォーク方式のインタプリタが使われてる最後のCRubyマイナーバージョンでした。CRuby 1.8.0のToDoファイルには新たに「パーサーAPI」というエントリが付け加わりました。おそらくこれは、既にエコシステムで求められてきた需要に応えるためのものでした。当時の開発者たちは、Ruby構文ツリーの上にさまざまなツールを構築したくてもできなかったからです。

一方、他の言語のエコシステムは先行していました。
Perlは2001年からPPIモジュールがあり、Perlランタイムがなくても開発者たちは構文ツリーにアクセス可能になっていました。
CPythonは早くも1990年にdisモジュールが同梱されていて開発者がバイトコードにアクセス可能になっていましたし、2005年にはastも同梱されました。
高品質のツールを作成するために構文ツリーやバイトコードにアクセスできることは、どの言語エコシステムでも非常に重要でしたが、Rubyはこの点において後(おく)れを取っていました。

🔗 2004-09-17: CRuby (e77ddaf

2004年になるとRipperがCRubyに統合され、それとともにCRubyがYaccからその精神的な継承者であるGNU Bisonに乗り換えました(e77ddaf)。BisonはYaccと互換性があり、リエントラント性など多くの改良が盛り込まれていました。RipperもBisonの上に構築されていたので、Ripperの統合をサポートするためにBisonに乗り換える必要があったのです。

ここで触れておきたい重要な点は、CRubyをソースコードからコンパイルするために、システムにBisonをインストールしておかなければならなくなったということです。従来はYaccをインストールしておかなければならなかったのがBisonに変わったので、変更点としては大きくありませんでしたが、インストールの手間を増やすこの問題がつい最近まで存在していたことには言及しておく価値があります2

🔗 2004-11-10: ParseTree

また、2004年にはRyan DavisがParseTreeというライブラリをリリースしました。これによって、Rubyプリミティブ(配列、文字列、シンボル、整数など)に変換するC拡張を用いてCRubyの構文ツリーにアクセス可能になりました。ParseTreeはCRuby 1.8の構文ツリーに強く依存しており、構文ツリーを効果的にRuby構造にミラーリングしていました。ParseTreeプロジェクトはRuby 1.8からRuby 1.9への移行で生き残れませんでしたが、ruby_parser gemの精神的な先祖でした。

🔗 2007-11-14: ruby_parser

2007年になると、Ryan DavisがParseTreeでの作業を打ち切って、CRuby 1.8ブランチと1.9ブランチの両方で動くruby_parser gemに置き換えました(当時の1.9は、YARVバイトコードインタプリタを擁する息の長いブランチでした)。ruby_parserプロジェクトはParseTreeと異なり、文法ファイルをコピーしてアクションをRubyに書き直し、それをraccパーサージェネレータに入力するという手法を採用していました。

ruby_parserプロジェクトは今日も存在していて、Rubyエコシステムの多くのツールがruby_parserを基礎としています(flogdawnscannerfastererなど多数)。これは、CRubyパーサーのエコシステムにおける初めての真の細分化であり、パーサーとしてRipperruby_parserのどちらかを選べるようになりました。

🔗 2007-12-25: CRuby 1.9.0

そして2007年、ついにCRuby 1.9がリリースされました。
CRuby 1.9で特筆すべきは、YARVバイトコードインタプリタが組み込まれたことで、Ripperを標準ライブラリとして含む最初のRubyバージョンでした。RipperがCRubyに統合されたことで、CRubyでは「Bisonで生成するパーサー(YARVバイトコードにコンパイルする)」と「Ripperパーサー(標準ライブラリとして公開される)」の2つのパーサーをメンテナンスするようになりました。

2つのパーサーの要件を合流させるために、Ripperは既存の文法ファイルのプリプロセスステップとして位置づけられました。文法ファイルのアクション内では、Ripperが実行するアクションを記述するために特殊なDSL(ドメイン固有言語)をコメント形式でC言語内に記述していました。そして、これらのコメントを抽出してBisonにそのまま渡せるクリーンな文法ファイルを生成するツールも作成されました。複雑なことをしているように見えるのは、実際に複雑だからです。Ripperがこのような形でセットアップされているため、文法ファイルを変更するとRipperのセマンティクスが誤って変更されてしまう可能性があり、この注意点は現在も存在しています。

🔗 2012-04-19: mruby

2012年、Ruby 1.9がISOで国際標準として認定された4日後、mrubyと呼ばれるRubyの新しい実装がスタートを切りました。mrubyは、メモリが少ない小型デバイスに適した「軽量版」Rubyを意図していました。

mrubyのパーサーは、CRubyの古いパーサーのコピーとして始まりましたが、間もなくLISPのS式スタイルに合わせた独自の構文ツリーが開発されました。CRubyの新しい機能をmrubyに取り入れるべく構文ツリーを更新する取り組みが行われてきましたが、この取り組みは困難であり、場合によっては望ましくないことが判明しました。

mrubyで重要なのは、埋め込み用にポータブルな設計となっている点です。つまり、mrubyは他のライブラリのサブモジュールとして機能できることを意味しています。mrubyのパーサーは他の言語からもアクセス可能なので、他のプロジェクト(特にArtichoke Ruby)の強化に用いられています。

🔗 2013-04-15: parser

1年後の2013年には、あのparser gemが作られました。parserもご多分に漏れず、CRubyの文法ファイルを取り込んでから、アクションをRubyに書き換えていました。parserは、ragelで生成したlexerと、raccで生成したパーサージェネレータを用いることで、Ruby構文ツリーにアクセスするRuby APIを提供しました。

parserプロジェクトは、Rubyの前バージョンの解析セマンティクスと正確に一致させるための不断の努力が続けられていたこともあって(parserがサポートするRubyバージョンごとに異なる文法ファイルがチェックインされていました)、広く人気を獲得しました。parserは使い勝手がよかったので、rubocopなどのツールがRipperからparserに乗り換えました。
parserは、CRubyパーサー自体を除けば、今日のエコシステムで最も広く使われているRubyパーサーであり、現在使われているほとんどの静的分析ツールの基礎となっています。

parserプロジェクトは大きな成功を収めましたが、これによってコミュニティがさらに細分化されたことにご注意ください。開発者はRipperでもruby_parserでもparserでも好きなパーサーを選べるようになりましたが、当時開発されていた静的分析ツールは最終的に互いのコードを再利用できなくなり、仕方なくコミュニティは複数のプロジェクトで同じようなロジックを再実装することになりました。さらに困ったことに、CRubyコードベースは独自のパーサーだけを使っていたため、CRuby外部のエコシステムから生まれたツールのさまざまな改良がリファレンス実装から失われることになりました。

これらのツールでは、CRubyの構文変更に対応するため、CRubyのparse.yファイルの変更がコミットされるたびに自分たちのリポジトリでissueを自動オープンするツールを開発しました。本記事で述べた2001年のJRubyと同じようなことが起きていたのです。

🔗 2013-10-26: TruffleRuby

2013年になると、Oracle LabsもRubyの新しい実装を開始しました。これは自己最適化ASTインタプリタの部分評価に基づいており、そのためにTruffle ASTインタプリタフレームワークと、Graal JITコンパイラというJavaをベースとした2つのテクノロジーが投入されました。このTruffleRubyはJavaベースなので、当初はJRuby+Truffleと呼ばれるランタイムバックエンドの代替としてJRubyに参加しました。ただし、プロジェクト間の設計が乖離するにつれてTruffleRubyも再び単独プロジェクトとして独立し、JRubyのパーサーをforkして独自のコアライブラリに適応しました。

TruffleRubyは、これまで登場したRuby実装の中で最大のパフォーマンスを誇り、これを実現するためにさまざまなJITコンパイラやGraalVMエコシステムのパワーを活用しています。TruffleRubyはproduction環境では広く使われてはいないものの、TruffleRubyに導入された概念はRubyコミュニティに多大な影響を及ぼしています。その中で特に注目に値するのは、おそらくTruffleRubyのコントリビュータたちが、Ruby言語の総合テストスイートであるRuby Spec Suiteを劇的に進歩させたことでしょう。

🔗 2017-02-26: typedruby

2017年には、段階的静的型付けを導入するtypedrubyプロジェクトが作成されました。typedrubyは、Rubyコードを解析するためにruby_parserのlexerを書き直し、CRuby 2.4.0の文法ファイルをコピーしてアクションをC++で書き直しました。

typedrubyプロジェクトの開発はまだそれほど活発ではありませんが、ここで開発されたパーサーが最終的にC++を用いる別プロジェクトであるSorbetにベンダリングされたことについては言及する価値があります。SorbetはStripeによって開発されたRuby向けの段階的静的型チェッカーで、現在も最大規模のRubyコードベースによるproductionシステムで積極的に利用されています。

🔗 2018-01-15: CRuby(0f3dcbdf

2018年になると、パーサー用のテストを書くときに有用なASTモジュールが導入されました(0f3dcbdf)。ASTモジュールは最終的にRubyVM::AbstractSyntaxTreeにリネームされ、Ruby 2.6.0で実験的機能としてリリースされました(この機能には、将来変更される可能性や安定性が保証されていないといった多くの注意書きが添えられていました)。CRubyパーサーがコミュニティのプロジェクトではなくCRuby内でpublic APIとして公開されたのは、これが初めてでした。

警告が効いたのか、この機能を元に開発されたプロジェクトはあまりありませんが、最も注目すべき例外はerror_highlight gemです。error_highlightは、エラーが発生したソースコードのスニペットをエラーとともに表示することで、エラーメッセージを改善するコアライブラリです。

🔗 2019-04-17: CRuby(9738f96

2019年に、CRubyはパターンマッチングの概念を導入しました(9738f96)。これは、12年前のRuby 1.9以来、Ruby言語に取り入れられた最大級の新構文でした。parse.yには、新しいパターンマッチング機能をサポートするために714行ものコードが追加されました。

これは本質的に、新しい構文をサポートするためにエコシステム内の他のパーサーもすべて更新が必要になるということです。はからずも、これによってエコシステム内のほとんどのパーサーが長年にわたって謳ってきた「100%の互換性」が終わりを告げることになりました。それらのパーサーのほとんどは、新機能の最も一般的なケースだけをサポートするサブセットから取り組むことになりました。各種パーサーの取り組みを以下のタイムラインにまとめました。

🔗 2019-11-12: Natalie

同じく2019年に、Tim MorganがNatalieプロジェクトを作成しました。Natalieは、プリコンパイル済みのC++によるRuby実装です。Natalieのパーサーは、ruby_parserでRyan Davisが開発した構文ツリー構造を利用して手書きで作成されました。やがて時間が経過するとともに、Natalieのパーサーはnatalie_parserと呼ばれる独自プロジェクトに抽出されました。

🔗 2020-01-18: PicoRuby

2020年には、PicoRubyと呼ばれるmrubyの別実装が作成されました。PicoRubyプロジェクトは、Raspberry Pi Picoなどのマイクロコントローラボード上での動作に適した、フットプリントの小さい最小限の実装を目指していました。PicoRubyのパーサーは、mrubyからコピーしたものをプロジェクトのニーズや要件に合わせて変更したものを使っていました。

🔗 2022-09-12: Prism

2022年後半になると、タイムラインのこの時点で、本記事のトピックであるPrismの最初のコミットが行われました。このトピックについて詳しくは後述します。

🔗 2023-05-12: CRuby(a1b01e77

2023年にKaneko Yuichiroがa1b01e77をコミットし、CRuby用の新しいパーサージェネレータとしてLramaが追加されました。LramaはBisonパーサージェネレータの再実装であり、Rubyで書かれています。Lramaは、過去20年間にわたって使われてきたのと同じparse.yを受け取って、Bisonが生成するのと同じparse.cを生成しました。これによって、CRubyパーサーが作成されたときから存在していた問題がついに解決され、CRubyをソースコードからコンパイルするときにYaccBisonを自分のシステムにインストールしておく必要がなくなりました。しかも、Bisonの細かなバージョン違いのせいでパーサーが壊れる心配もなくなりました。

Lramaは、CRubyをメンテナンスするうえで大きな進歩となりました。パーサー生成パイプライン全体がCRubyの制御下に置かれるようになったので、従来のBisonでは不可能だった方法で文法ファイルを変更可能になったのです。その具体例は、Ripperのリファクタリング(#9923)や、文法ファイルに?演算子を導入するterms?#10050)や'\n'?#10051)などのコミットで確認できます。

この変更は、マイナス面なしというわけにはいかない点に注意することが重要です。本記事をここまで注意深く読み進めていれば、エコシステム内にある他のパーサーのほとんどが本家のparse.yの変更点を自分たち独自の文法ファイルに手動で反映する方法に依存していたことを思い出すでしょう。このコミットの時点では、以下のプロジェクトに含まれているパーサーに加え、これらのプロジェクトに依存するツールも影響を受けることになります。

🔗 2023-06-12: CRuby(b481b673

2023年の中頃、Kaneko YuichiroによるCRubyへのコミットb481b673によって、"ユニバーサル"パーサーという概念が導入されました(詳しくはissue #19719を参照)。このアイデアは、既存のCRubyパーサーを単独のライブラリに段階的に抽出するというものでした。

ユニバーサルパーサーは、コンシューマ(consumer)側がパーサーに必要なすべての機能を実装するコールバックインターフェイスを提供する形で実現されました。時間をかけてコールバックの個数を削減し、最終的にはコールバックの個数をできるだけゼロに近づけることを目標に掲げていました。

ユニバーサルパーサーの作業は現在も継続中です。本記事執筆時点では、このインターフェイスはCRubyでオプションとして利用可能ですが、エコシステムの他のプロジェクトではまだ採用されていません。

🔗 その他のプロジェクト

この歴史をできるだけ簡潔にするため(あまり簡潔にはなりませんでしたが)、多数のプロジェクトをあえて省略しました。以下はほんの一部ですが、それ以外にもおびただしいパーサーや構文ツリーが開発されており、Rubyフロントエンドのエコシステム全体の発展に大きく寄与しています。

プロジェクト パーサー
Cardinal 手書きパーサー(Parrotで書かれている)
IronRuby CRubyパーサーのコピー(.NETで書き直されている)
MacRuby CRubyパーサーのコピー(Objective-cで書き直されている)
Rubinius MelbourneパーサーはCRubyパーサーのコピーをRubyで書き直している
Ruby Intermediate Language 手書きパーサー(OCamlで書かれている)
Syntax Tree Ripperの上で構築される構文ツリー
Topaz CRubyパーサーのコピー(RPythonで書き直されている)

🔗 Prism

2022年初頭にPrismが構想された当時の私たちは、多種多様な要件を抱えて細分化されたエコシステムの現状に直面していました。Rubyフロントエンドの歴史とエコシステムの現状を考慮すると、単にパーサーを書くだけではエコシステムが選択できるパーサーをさらに増やすだけで終わってしまう危険があることが目に見えていたので、すべてを一気に解決する必要がありました。

標準はいかにして普及するか
(ACアダプタや文字エンコーディングやインスタントメッセージングなどの現状を見よ)

シチュエーション: 14の標準が競い合っている。
「14も?おかしいってそれ!あらゆるユースケースをカバーできる普遍的な1つの標準を開発しなきゃ」「そうよそうよ!」
そして...
シチュエーション: 15の標準が競い合っている。

Standards

私たちがこれらの問題を解決したい理由はいくらでもありますが、最大の理由は「メンテナンスコストがあまりに大きすぎる」ことです。
Shopifyは、2022年初頭の時点でCRubyに加えて、TruffleRubyparserrubocoppackwerkなどのダウンストリームプロジェクトで必要)、Sorbetにも投資していました。Shopify社内には、TruffleRubyの構文更新に積極的に取り組んでいる開発者たちもいれば、最近だとSorbetにパターンマッチングを導入するために数か月かかりきりだった開発者たちもいました。
つまり私たちにとって、これらすべてのプロジェクトで利用可能なパーサーを1つに集約する可能性を検討する意義は極めて大きかったのです。

コミュニティをひととおり棚卸しした結果、エコシステム内に存在する多種多様なパーサーのメンテナー全員と緊密に連携を取り合ってどんなニーズがあるかを把握し、それらのニーズをすべて満たさなければならないことが判明しました。それを達成しない限り、パーサーはユニバーサルとは言えません。さらに、既存のあらゆるプロジェクトが新しいパーサーに移行するための明確な移行パスを作成する必要もあり、これも並大抵の苦労ではありませんでした。最終的に以下のリストができました。

懸念事項 説明
互換性 新しいパーサーが、同じコードを既存のパーサーと完全に同じ方法で解析できなければ、見向きもされないだろう。
メンテナンス性 メンテナンスや更新を楽に行えるパーサーは、エコシステムのあらゆるプロジェクトで切望されている。
パフォーマンス 既存のパーサーより遅いパーサーを採用するプロジェクトなど存在しない。
エラー許容性 新しいパーサーがIDEツールの基盤となるには、エラー許容性を備えてなければならない(SorbetRuby LSPを含むあらゆる実装の必須要件)
移植性 現時点で積極的にメンテナンスされているパーサーの記述言語はC、C++、Ruby、Rust、Javaである。Java以外はすべてFFI3経由で機能できるが、Javaプロセスがネイティブ関数呼び出しを何度も繰り返さなければならないソリューションはよくない。代わりに、単一のFFI呼び出しで構文ツリーを取得可能で、かつJavaプロジェクトでも利用できるシリアライズ形式を開発する必要がある。
リエントラント性 マルチスレッド環境で実行可能にするには、パーサーをリエントラント4可能にする必要がある。
識別可能性 新しいパーサーは、解析全体でノードを一貫して識別可能な構文ツリーを生成する必要がある。これは、ソースコードを再解析してエラーの正確な位置を突き止めるerror_highlightの必須要件。
小さいフットプリント mrubyPicoRubyなどの実装に適するには、フットプリントを小さくする必要がある。
移行パス 既存のあらゆるプロジェクトが新しいパーサーに移行するための明確な移行パスを提供する必要がある。

この大規模かつ野心的な要件リストを元に、私たちは作業を開始しました。そしてたっぷり1年をかけた後に以下の成果を得られました。

  • 関係者すべてのニーズを満たす新しい構文ツリーを設計した
  • rubygems.orgにあるあらゆるRubyコードを字句(lexical)レベルでCRubyパーサーと正確に同じ形で処理できる新しいパーサーを作成した
  • 新しいパーサーをエコシステム内のほとんどのプロジェクトに統合する作業が始まった

この時点で、私たちの取り組みの成果であるcc7f765f(#7964)をCRubyにマージするための最初のプルリクをオープンしました。2023年6月下旬のことです。

CRubyはRuby 1.9からバイトコードインタプリタになっていたので、次のステップは、既存のCRubyコンパイラと同じバイトコードインストラクションを生成可能にすることです。そして私たちは作業を開始しました。

それと並行して、既存のプロジェクトをPrismに移行しやすくするための移行シナリオの作成にも取り掛かりました。私たちは、Prismの構文ツリーを既存の他のパーサーに変換する「変換」レイヤの開発を開始しました。これには、Ripperruby_parserparserも含まれます。

🔗 そして現在

私たちがPrismで取り組んできた歴史によって、今日という日を迎えることができました。2年にもおよぶ取り組みを経て、CRubyを取り巻いているエコシステムの状態は以下のようになりました。

プロジェクト 現状
Natalie 昨年末にかけて、NatalieはPrismへの移行を完了しました(ツイート)。これはPrism採用の一番乗りであり、Timは多くのバグ発見に力を貸してくれたおかげで私たちの側でバグを修正できました。
JRuby 今年始めに、JRubyはPrismへの移行を完了しました(リリース記事)。ここもPrismを先行採用した企業の1つであり、Prism開発中に導入された数々の破壊的変更をすべて克服した結果、今では解析時間が最大で3倍に高速化されました。
TruffleRuby 同じく今年始めに、TruffleRubyもPrismへの移行を完了しました(ツイート)。発表によると、Prismは従来パーサーの約2倍高速であるとのことです。
PicoRuby 本記事執筆時点で、PicoRubyの作者はPrism"ユニバーサル"パーサーの両方について実験を進めているところです(ツイート)。移行はまだ完了していませんが、来月のRubyKaigiで調査結果を発表する予定です。
ruby_parser 今年はじめに、私たちはruby_parser用の変換レイヤの作成を終えました。この変換レイヤは、Prismへの移行ツールの基礎として利用可能であり、解析時間の大幅な高速化も見られます。
parser こちらも今年はじめに、私たちはparserの構文ツリー用の変換レイヤの作成を終えました。これはrubocopでただちに採用されたほど成功し、rubocopの最新バージョンではオプションでPrismを選択可能になっています。rubocopの作者は、今後パーサーをPrismに直接乗り換えることを検討していると#12600のコメントで述べています。packwerkを始めとする他のツールでもこの変換レイヤが使われ始めています。
Ripper ごく最近になって、Ripperのイベントストリーム用の変換レイヤの作成を完了しました。この変換レイヤの作成はこれまでで最も複雑でしたが、おかげでこの「実験的」ツールであるRipperをPrismに移行可能になりました。
Sorbet 私たちの成果で得られたメリットを活かすため、SorbetプロジェクトのパーサーをPrismパーサーに移行することを検討しています(現在は評価作業中)。

Prismは、他にも多数のオープンソースライブラリや実装でも採用されたり実験されたりしています。その中のいくつかを以下にリストアップします。

  • Garnet: TypeScriptで書かれたRuby実装(WASMバインディング経由で利用可能)

  • Opal: Ruby->JavaScriptへのトランスレーター(Ruby gemまたはWASMバインディング経由で利用可能)

  • Rails: 以下のようなさまざまな取り組みがマージされた。
  • Ruby LSP: Ruby向けのLSP(language server protocol)実装であり、Syntax Treeパーサーから移行した(#1025

Prismプロジェクトが始まって以来、Shopifyの内外では他にも多数のクローズドソースツールも開発されています。そうしたツールの中には、RubyやC++やRustやJavaScriptで開発されたものもありますが、いずれもPrismが提供する豊富なバインディングを利用しています。

後は、PicoRubyが(潜在的にはmrubyも)今後Prismを採用することを決めれば、CRubyを除いた世界中のあらゆるRuby実装およびRubyパーサーでPrismが使われることになります。

🔗 CRubyと今後

当然ながら、最も困難な作業はRubyのリファレンス実装であるCRubyをPrismに移行することです。CRubyがPrismに乗り換えるのを難しくしている要因にはさまざまなものがあります。

🔗 変更の規模

Prismでは、多くの関係者から寄せられたさまざまな懸念事項を念頭に置いて、独自の構文ツリーをゼロから設計しました。これによって必然的に、CRubyが生成するインストラクションシーケンスと一致するコンパイラを独自に作成しなければならなくなります。本記事執筆時点のPrismコンパイラのコードは9000行に相当します。パーサーとコンパイラを総入れ替えするという大仕事で、あらゆるエッジケースを確実にカバーするのは困難です。これほど大規模な変化に不安を覚えるのは当然であり、理解できる話です。

🔗 互換性

私たちはPrismを絶え間なく進歩させ続けていますが、それでもPrismからわずかに逸脱するエッジケースがいくつか残っています。これらの大半は、Prismパーサーではなく、TracePointCoverageといったモジュール内のコンパイラに関連しています。私たちはこの齟齬を解消するために積極的に取り組んでいますが、作業には時間がかかります。

本記事執筆時点では、テストの失敗率は105/21813、specでは42/32601となっています。これらは次期バージョンのCRubyまでに解決する自信があります(すぐ解決とはいかなくても)。それまでは、社内CI環境でShopifyのコアモノリスに--parser=prismを渡してテストし、すべてのテストとspecにパスさせるとともに、発生する可能性のあるリグレッションエラーを確実にキャッチ可能にする予定です。

🔗 内部での競い合い

既に述べたように、CRubyにはRubyフロントエンドエコシステムを改善するための「"ユニバーサル"パーサー」という取り組みも存在しています。Prismはエコシステム内の他のプロジェクトのほとんどでパーサーおよび実装として採用済みですが、CRuby開発者のメンテナンス負荷を軽減するために"ユニバーサル"パーサーの取り込みも継続しています。PrismはライブラリとしてCRubyにマージ済みですが、Prismプロジェクトと"ユニバーサル"パーサーは互いに競い合う形になっており、Prismはまだデフォルトのパーサーとしては採用されていません。

これは残念ながら、Prismと"ユニバーサル"パーサーのどちらかに決定されるまではCRubyの開発そのものが難しくなることを意味しています。文法やコンパイラに対する変更は、1回は既存のパイプラインに対して、もう1回はPrismに対して、計2回行わなければなりません。
CRuby開発者以外の人々にとって幸いなことに、Matzは将来への不安を和らげるため、今後Rubyの公式パーサーAPIがPrismになることに同意しました。つまり、今後Prism"ユニバーサル"パーサーのどちらがCRubyの公式ソリューションとして採用されようと、開発者向けAPIが一本化されるということです。これによって、開発者が今日にでもPrismに対して開発が可能になり、かつCRubyが最終的にどちらを内部ソリューションとして選択するかという懸念が払拭されることになるので、Rubyエコシステムにとって重要な勝利となります。

🔗 まとめ

本記事によって、PrismLrama"ユニバーサル"パーサー、そしてさらに広大なRubyフロントエンドのエコシステムが互いにどういう関係にあるかが、ある程度明確になることを願っています。

私たちは、コミュニティ全体が1つの構文ツリーを中心として結集してきた進歩と、それがもたらす可能性に大いに興奮しています。
解析を行うときの「信頼できる単一の情報源」がコミュニティで共有されることで、エラーメッセージの改善や、コードナビゲーション用の共有インデックスなどの恩恵をコミュニティ全体で受けられるようになります。
私たちはPrismの作業全般で、Rubyのフロントエンドエコシステムで将来私たちを待ち受けているものを目にすることと、その一員になれることに大いに興奮しています。

関連記事

Rubyパーサーを一新するprism(旧YARP)プロジェクトの全容と将来(翻訳)

RubyにlramaがマージされてBison依存がなくなった(RubyKaigi 2023)


  1. 訳注: 以降も「フロントエンド」はコンパイラのフロントエンドを指します。Webのフロントエンドのことではありません。 
  2. 訳注: Ruby 3.3.0からはビルド要件からBisonが削除されました(参考) 
  3. Foreign function interface - Wikipedia 
  4. リエントラント - Wikipedia 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。