この記事は何?
来たる 8/18(月) にKashiwa.rbの勉強会が開催されます。
Kashiwa.rb #14 ワイガヤグループワーク会 - connpass
自分は「Parser や AST を触ってみる」あたりに取り組むつもり。なのだけど、パッと Parser を動かして AST を確認できるものがいくつもあるみたいなので事前に整理をしておこうと思った。例えばで以下のものがある。
RubyVM::AbstractSyntaxTree#parse
$ ruby -e 'pp RubyVM::AbstractSyntaxTree.parse("x = 1 + 2")'
(SCOPE@1:0-1:9 tbl: [:x] args: nil body: (LASGN@1:0-1:9 :x (OPCALL@1:4-1:9 (INTEGER@1:4-1:5 1) :+ (LIST@1:8-1:9 (INTEGER@1:8-1:9 2) nil))))
ripper
$ ruby -rripper -e 'pp Ripper.sexp("2 + 2")'
[:program, [[:binary, [:@int, "2", [1, 0]], :+, [:@int, "2", [1, 4]]]]]
ruby_parser
$ ruby -rruby_parser -e 'pp RubyParser.new.parse "1+1"' s(:call, s(:lit, 1), :+, s(:lit, 1))
parser
$ ruby -rparser/ruby34 -e 'pp Parser::Ruby34.parse("2 + 2")'
s(:send,
s(:int, 2), :+,
s(:int, 2))
rubocop-ast
$ ruby -rrubocop-ast -e 'pp RuboCop::AST::ProcessedSource.new("2 + 2", RUBY_VERSION.to_f).ast'
s(:send,
s(:int, 2), :+,
s(:int, 2))
Prism
$ ruby -rprism -e 'pp Prism.parse("2 + 2").value'
@ ProgramNode (location: (1,0)-(1,5))
├── flags: ∅
├── locals: []
└── statements:
@ StatementsNode (location: (1,0)-(1,5))
├── flags: ∅
└── body: (length: 1)
└── @ CallNode (location: (1,0)-(1,5))
├── flags: newline
├── receiver:
│ @ IntegerNode (location: (1,0)-(1,1))
│ ├── flags: static_literal, decimal
│ └── value: 2
├── call_operator_loc: ∅
├── name: :+
├── message_loc: (1,2)-(1,3) = "+"
├── opening_loc: ∅
├── arguments:
│ @ ArgumentsNode (location: (1,4)-(1,5))
│ ├── flags: ∅
│ └── arguments: (length: 1)
│ └── @ IntegerNode (location: (1,4)-(1,5))
│ ├── flags: static_literal, decimal
│ └── value: 2
├── closing_loc: ∅
└── block: ∅
前提情報: この記事執筆時点のRuby最新バージョン
3.4.5
CRubyのParserの変遷
以下記事にまとまっていた。
ざっくり書くと Yacc → Bison → Lrama + Prism という解釈を得た。
prismとparse.yそれぞれの出力を確認してみる
まとめ記事にコマンドのサンプルがあったので実行してみる。
$ ruby --parser=prism --dump=parsetree -e "2 + 2"
@ ProgramNode (location: (1,0)-(1,5))
+-- locals: []
+-- statements:
@ StatementsNode (location: (1,0)-(1,5))
+-- body: (length: 1)
+-- @ CallNode (location: (1,0)-(1,5))
+-- CallNodeFlags: nil
+-- receiver:
| @ IntegerNode (location: (1,0)-(1,1))
| +-- IntegerBaseFlags: decimal
| +-- value: 2
+-- call_operator_loc: nil
+-- name: :+
+-- message_loc: (1,2)-(1,3) = "+"
+-- opening_loc: nil
+-- arguments:
| @ ArgumentsNode (location: (1,4)-(1,5))
| +-- ArgumentsNodeFlags: nil
| +-- arguments: (length: 1)
| +-- @ IntegerNode (location: (1,4)-(1,5))
| +-- IntegerBaseFlags: decimal
| +-- value: 2
+-- closing_loc: nil
+-- block: nil
$ ruby --parser=parse.y --dump=parsetree -e "2 + 2" ########################################################### ## Do NOT use this node dump for any purpose other than ## ## debug and research. Compatibility is not guaranteed. ## ########################################################### # @ NODE_SCOPE (id: 4, line: 1, location: (1,0)-(1,5)) # +- nd_tbl: (empty) # +- nd_args: # | (null node) # +- nd_body: # @ NODE_OPCALL (id: 3, line: 1, location: (1,0)-(1,5))* # +- nd_mid: :+ # +- nd_recv: # | @ NODE_INTEGER (id: 0, line: 1, location: (1,0)-(1,1)) # | +- val: 2 # +- nd_args: # @ NODE_LIST (id: 2, line: 1, location: (1,4)-(1,5)) # +- as.nd_alen: 1 # +- nd_head: # | @ NODE_INTEGER (id: 1, line: 1, location: (1,4)-(1,5)) # | +- val: 2 # +- nd_next: # (null node)
Ripper
- https://github.com/ruby/ruby/tree/master/ext/ripper
- 古くから(Ruby 1.7)標準ライブラリとして存在
- yard, prettier, rubyfmt などで利用されている
RubyVM::AbstractSyntaxTree#parse
- https://docs.ruby-lang.org/ja/latest/method/RubyVM=3a=3aAbstractSyntaxTree/s/parse.html
- Ruby 2.6.0 で実験的機能としてリリースされたもの、というのをまとめ記事で理解した
ruby_parser, parser
- https://github.com/seattlerb/ruby_parser
- https://github.com/whitequark/parser
- これらはコミュニティによるプロジェクト
Racc
- https://github.com/ruby/racc
- LALR(1) parser generator
- pure Ruby
- ruby_parser, parser 両方とも racc で生成したパーサージェネレーターを使っている
- lrama も 内部パーサーを生成するツールとして利用している
Lrama
- https://github.com/ruby/lrama
- Pure Ruby LALR parser generator
- CRuby に組み込まれているパーサー(ジェネレーター)
Prism
- https://github.com/ruby/prism
- CRuby に組み込まれているパーサー
- 歴史とか設計は以下記事を読むと良さそう
メジャーなツールが採用している Parser と現在の状況
RuboCop
- https://github.com/rubocop/rubocop
- 利用しているのは parser だが、それを拡張した rubocop-ast を利用している
- parser において全てのノードは単一の
Parser::AST::Nodeクラスで表現される - rubocop-ast では Builder を拡張して node_type ごとのサブクラスが定義されている
- parser において全てのノードは単一の
- RuboCop 1.75 から Ruby 3.4 構文のサポートのため Prism が依存に追加されている
- ref: https://koic.hatenablog.com/entry/rubocop-supports-ruby34-syntax
- Prism の translation layer によって parser と互換性のある AST が出力される
- つまり RuboCop の実装を大きく変更することなく Prism が導入されている
- https://github.com/rubocop/rubocop-ast/blob/v1.43.0/lib/rubocop/ast/processed_source.rb#L309
Brakeman
- https://github.com/presidentbeef/brakeman
- 利用しているのは ruby_parser
- v6.2.1 から Prism が追加されている
- https://github.com/presidentbeef/brakeman/releases/tag/v6.2.1
- これも translation layer で既存Brakeman実装を大きく変えずに導入できていそう
Ruby LSP
- https://github.com/Shopify/ruby-lsp
- 利用しているのは Prism
Prism の translation layer
- Prism は ripper, ruby_parser, parser それぞれと互換性のある AST 出力が可能
- これによって既存ツールは実装を大きく変えずに Prism を導入することができる
俯瞰してみて出てきた疑問・気になっていること
- lramaとPrismがCRubyにおける標準的なParserという位置付けであることが分かったが実装が2つ存在する
- 共存させている意図は何?
- 今後それぞれはどうなっていくと予想される?
- Prism の translation layer によって標準Parserに大統一することもできそうな気がするが現実世界はそんなにシンプルじゃなさそう
- 既存 Parser の開発状況や新しい文法への対応はどうなる?いつまでされる?
- その既存 Parser に依存したツール群はどうする? ripper, ruby_parser, parser の出力する AST の形式に依存したまま?
- Ruby の Parser 初学者がここら辺を学ぶ場合の話
- どういう順番に見ていくのが良さそう?
- 追いかけるべきものは?逆に追いかけなくても良さそうなやつある?
あたりを勉強会で会話してみたいなってなりました。
そもそも自分がParserやAST周りに興味を持っている背景のメモ
- Rubyの開発体験を改善・向上するようなものに興味関心がある
- RubyLSP, RuboCop, Brakeman, Steep, RBS, etc...
- これらツールの基盤として大体 Parser, AST があるイメージ
- なので「Parser や AST がどうやって実現されているか」よりも「Parser や AST をどう活用できればより開発体験が向上できるか」みたいなことの方が気になっているのかもしれない
- Parser が新しくなることでエンドユーザー目線で何が解決されるか、とかそういったこと
- 「エラートレランス」というキーワードで説明される Prism の話は具体性と納得感があった
そういった観点でも勉強会で会話したいなってなりました。