RubyVM::AbstractSyntaxTreeについて調べてみた

社内で行っているRubyの勉強会のネタを探しに3.2.0のリリースノートを見ていたところ、`RubyVM::AbstractSyntaxTree`の変更についての記載があり、これについて調べたのでまとめました。

https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

> - `parse`, `parse_file`, `of` へ `error_tolerant` オプションが追加されました
> - `parse`, `parse_file`, `of` へ `keep_tokens`オプションが追加されました

:::note warn
`RubyVM::AbstractSyntaxTree`についてまとめますが、このモジュールは実験的であり、一般に使用が推奨されているものではありません
:::

## RubyVM::AbstractSyntaxTreeについて

`RubyVM::AbstractSyntaxTree` はRuby2.6で入ったRubyのパーサーで、Ruby本体のデバッグや静的解析等で使うことを想定されています。

Rubyのパーサーとしては、他にはRipper(1.9で標準ライブラリに入った)や[parser gem](https://github.com/whitequark/parser)がよく使われるようです。

ちなみに、rubocopはもともとRipperを使っていたようですが、現在はparser gemを使っています。[^2]
Ruby 3.2で標準ライブラリに入った [syntax_suggest](https://github.com/ruby/syntax_suggest) も内部でRipperをつかっています。
(syntax_suggestについては [RubyKaigi 2021のセッション](https://rubykaigi.org/2021-takeout/presentations/schneems.html) が面白かったのでぜひ見てみてください)

[^2]: [RuboCop作者がRubyコードフォーマッタを比較してみた: 中編（翻訳）｜TechRacho by BPS株式会社](https://techracho.bpsinc.jp/hachi8833/2023_03_30/74320/embed#?secret=AGex2clHLW)

### MRIにしか実装されていない

Rubyにはいくつか処理系があるのですが、`RubyVM::AbstractSyntaxTree`はMRIにしか実装されていません。

そもそも、`RubyVM`クラス自体MRIのみのようですし、以下のように一般ユーザーが使うことは想定されていません。

[class RubyVM (Ruby 3.2 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/RubyVM.html)

> Ruby の 内部情報へのアクセス手段を提供するクラスです。デバッグ用、プロトタイピング用、研究用などのとても限定された用途向けです。一般ユーザーは使うべきではありません

[module RubyVM::AbstractSyntaxTree (Ruby 3.2 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/RubyVM=3a=3aAbstractSyntaxTree.html)

> このモジュールは実験的であり、安定したAPIではないため、予告なしに変更される可能性があります

こちら([Feature #14844: Future of RubyVM::AST? - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/14844))のIssueでは、RubyVM::AbstractSyntaxTreeについて、「Rubyとしてこのライブラリをどうしていくか」であったり、「Ripper等の他のパーサーとの比較」などが議論されています。
より深く知りたい人はこちらのIssueを読んでみてください。

ちなみに、RubyVMモジュールには他に以下のようなモジュールがあり、

```ruby
# ruby -v => 3.2.1

> RubyVM.constants
=> [:INSTRUCTION_NAMES, :InstructionSequence, :OPTS, :DEFAULT_PARAMS, :MJIT, :AbstractSyntaxTree, :YJIT]
# InstructionSequence(ISeq)は命令列のバイトコード
```

RubyVM::AbstractSyntaxTreeには以下のようなメソッドが定義されています。

```ruby
> RubyVM::AbstractSyntaxTree.methods(false)
=> [:node_id_for_backtrace_location, :parse, :of, :parse_file]
```

### RubyVM::AbstractSyntaxTreeが追加された背景

追加された背景はRubyKaigi 2018の [Yuichiro Kaneko](https://twitter.com/spikeolaf) さんの発表で話されていました。

https://rubykaigi.org/2018/presentations/spikeolaf.html#jun01

Ruby2.5で実装されたbranch coverageを実装するために、Code locationが取得できるような変更が加わりました。
今までは行情報は取れていたものが、columの情報がとれるようになりました。 (beginとendの情報)

https://github.com/ruby/ruby/blob/master/doc/NEWS/NEWS-2.5.0

<details><summary>実行結果</summary>

```ruby
$ ruby --dump=p -e '"str".upcase'                                                                                                                                                                    [main]
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 2, line: 1, location: (1,0)-(1,12))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_CALL (id: 1, line: 1, location: (1,0)-(1,12))*
#     +- nd_mid: :upcase
#     +- nd_recv:
#     |   @ NODE_STR (id: 0, line: 1, location: (1,0)-(1,5))
#     |   +- nd_lit: "str"
#     +- nd_args:
#         (null node)
```

token (v3.2)

```ruby
$  ruby --dump=y -e '1 + 2' | grep 'Shifting'
Shifting token "integer literal" (1.0-1.1: 1)
Shifting token '+' (1.2-1.3: )
Shifting token "integer literal" (1.4-1.5: 2)
Shifting token '\n' (1.5-1.6: )
Shifting token "end-of-input" (1.6-1.6: )
```

</details>

Code locationはRuby3.1のerror_highlightにも利用されています。

https://techlife.cookpad.com/entry/2021/12/07/103547

そして、このCode locationのCのAPIをtestするために`ext/-test-/ast/ast.c`に実装されたのですが、公開すると役に立つ人もいるかも? という話になり2.6.0-preview2で公開されたようです。
なので、元々この機能を作りたかったというよりは、副次的に作られたもので、今後の具体的な開発計画があるわけではないようです。

https://github.com/rmosolgo/ruby/commit/46463af983aee8b3e17635e796140c24a0469859

## 　【補足】 Rubyコードのパースについて

3.2の変更内容に入る前に、前提情報としてRubyコードを実行するまでのパースについて補足します。

Rubyのコードは、以下のように処理、変換されます。

- 字句解析でTokenに
- 構文解析でASTに
- ASTをInstructionSequence (ISeq/バイトコード/YARV命令列)に
- (JITコンパイル)

パーサーについては、`parse.y`を元に、Bisonを使って`parse.c`が生成されます。[^1]
[^1]: [Rubyのしくみ](https://www.amazon.co.jp/Ruby%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF-Ruby-Under-Microscope-Shaughnessy/dp/4274050653)

BisonはLALRパーサジェネレータです。Yacc(Yet Another Compiler Compiler)形式の`.y`拡張子を使って記述します。パーサーと言っていますが、字句解析も行います。

## Ruby3.2での変更内容について

開発者の Yuichiro Kaneko さんのブログにまとめられています。

https://yui-knk.hatenablog.com/entry/2022/11/23/171608



- RubyVM::AbstractSyntaxTree `parse`, `parse_file`, `of` へ `error_tolerant` オプションが追加されました
    - [Feature #19013: Error Tolerant Parser - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/19013)
        - (Issueを見ると、一種の実験的な意味合いもありそう)
    - [[Feature #19013] Error Tolerant Parser by yui-knk · Pull Request #6512 · ruby/ruby](https://github.com/ruby/ruby/pull/6512)
- RubyVM::AbstractSyntaxTree `parse`, `parse_file`, `of` へ `keep_tokens`オプションが追加されました
    - [Feature #19070: Enhance keep_tokens option for RubyVM::AbstractSyntaxTree parsing methods - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/19070?tab=changesets)
    - [[Feature #19070] Enhance keep_tokens option for RubyVM::AbstractSyntaxTree parsing methods by yui-knk · Pull Request #6770 · ruby/ruby](https://github.com/ruby/ruby/pull/6770)

error_tolerantはSyntax Errorがあったときでも、一部のASTを取得できるようにするオプションです。
実装中、基本的にはプログラムは不完全な状態で、Syntaxは正しくない時間が大半です。その状態でもASTが取得できることでLSP等でコードの補完を行うことなどに活かすこともできるようになります。

keep_tokensはNodeからtokenを取得できるようにするオプションです。 Issueで例示されているもののように、ASTの段階では抜け落ちていたtokenの情報が取れるようになるようです。

> Implementation for Language Server Protocol (LSP) sometimes needs token information. For example both m(1) and m(1, ) has same AST structure other than node locations then it's impossible to check the existence of , from AST. However in later case, it might be better to suggest variables list for the second argument. Token information is important for such case.

`RubyVM::AbstractSyntaxTree`はつくられた経緯からわかるように、パースには`parser.y`の処理が使われています。

<details><summary>ソースコード一部抜粋</summary>

```ruby:ast.rb
  #  call-seq:
  #     RubyVM::AbstractSyntaxTree.parse(string, keep_script_lines: false, error_tolerant: false, keep_tokens: false) -> RubyVM::AbstractSyntaxTree::Node
  #
  #  Parses the given _string_ into an abstract syntax tree,
  #  returning the root node of that tree.
  #
  #    RubyVM::AbstractSyntaxTree.parse("x = 1 + 2")
  #    # => #<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-1:9>
  #
  #  If <tt>keep_script_lines: true</tt> option is provided, the text of the parsed
  #  source is associated with nodes and is available via Node#script_lines.
  #
  #  If <tt>keep_tokens: true</tt> option is provided, Node#tokens are populated.
  #
  #  SyntaxError is raised if the given _string_ is invalid syntax. To overwrite this
  #  behavior, <tt>error_tolerant: true</tt> can be provided. In this case, the parser
  #  will produce a tree where expressions with syntax errors would be represented by
  #  Node with <tt>type=:ERROR</tt>.
  #
  #     root = RubyVM::AbstractSyntaxTree.parse("x = 1; p(x; y=2")
  #     # <internal:ast>:33:in `parse': syntax error, unexpected ';', expecting ')' (SyntaxError)
  #     # x = 1; p(x; y=2
  #     #           ^
  #
  #     root = RubyVM::AbstractSyntaxTree.parse("x = 1; p(x; y=2", error_tolerant: true)
  #     # (SCOPE@1:0-1:15
  #     #  tbl: [:x, :y]
  #     #  args: nil
  #     #  body: (BLOCK@1:0-1:15 (LASGN@1:0-1:5 :x (LIT@1:4-1:5 1)) (ERROR@1:7-1:11) (LASGN@1:12-1:15 :y (LIT@1:14-1:15 2))))
  #     root.children.last.children
  #     # [(LASGN@1:0-1:5 :x (LIT@1:4-1:5 1)),
  #     #  (ERROR@1:7-1:11),
  #     #  (LASGN@1:12-1:15 :y (LIT@1:14-1:15 2))]
  #
  #  Note that parsing continues even after the errored expression.
  #
  def self.parse string, keep_script_lines: false, error_tolerant: false, keep_tokens: false
    Primitive.ast_s_parse string, keep_script_lines, error_tolerant, keep_tokens
  end
```

```ast.c
static VALUE
ast_s_parse(rb_execution_context_t *ec, VALUE module, VALUE str, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens)
{
    return rb_ast_parse_str(str, keep_script_lines, error_tolerant, keep_tokens);
}

static VALUE
rb_ast_parse_str(VALUE str, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens)
{
    rb_ast_t *ast = 0;

    StringValue(str);
    VALUE vparser = ast_parse_new();
    if (RTEST(keep_script_lines)) rb_parser_keep_script_lines(vparser);
    if (RTEST(error_tolerant)) rb_parser_error_tolerant(vparser);
    if (RTEST(keep_tokens)) rb_parser_keep_tokens(vparser);
    ast = rb_parser_compile_string_path(vparser, Qnil, str, 1);
    return ast_parse_done(ast);
}
```

```c:parser.y
VALUE rb_parser_new(void)
{
    struct parser_params *p;
    VALUE parser = TypedData_Make_Struct(0, struct parser_params, &parser_data_type, p);
    parser_initialize(p);
    return parser;
}

void rb_parser_keep_script_lines(VALUE vparser)
{
    struct parser_params *p;

    TypedData_Get_Struct(vparser, struct parser_params, &parser_data_type, p);
    p->keep_script_lines = 1;
}

void rb_parser_error_tolerant(VALUE vparser)
{
    struct parser_params *p;

    TypedData_Get_Struct(vparser, struct parser_params, &parser_data_type, p);
    p->error_tolerant = 1;
    p->end_expect_token_locations = rb_ary_new();
}
```

</details>

---

## 今後

RubyVM::AbstractSyntaxTreeに具体的な開発計画があるわけではないようですが、Rubyのパーサー自体をerror tolerantに、よりポータブルに、より拡張しやすくする計画があるようです。

https://yui-knk.hatenablog.com/entry/2022/12/11/154502

今年のRubyKaigi2023でも関連したセッションがあるので、興味のある方は見てみてください。

https://rubykaigi.org/2023/presentations/spikeolaf.html#may11

https://rubykaigi.org/2023/presentations/kddnewton.html#may12

## 参考

- [Ruby 3.2.0 リリース](https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/)
- [Ruby 3.2のParser目玉機能 - かねこにっき](https://yui-knk.hatenablog.com/entry/2022/11/23/171608)
- [Feature #19013: Error Tolerant Parser - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/19013)
- [[Feature #19013] Error Tolerant Parser by yui-knk · Pull Request #6512 · ruby/ruby](https://github.com/ruby/ruby/pull/6512)
- [Feature #19070: Enhance keep_tokens option for RubyVM::AbstractSyntaxTree parsing methods - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/19070?tab=changesets)
- [[Feature #19070] Enhance keep_tokens option for RubyVM::AbstractSyntaxTree parsing methods by yui-knk · Pull Request #6770 · ruby/ruby](https://github.com/ruby/ruby/pull/6770)
- [Rubyのしくみ](https://www.amazon.co.jp/Ruby%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF-Ruby-Under-Microscope-Shaughnessy/dp/4274050653)
- [RNode with code positions - RubyKaigi 2018](https://rubykaigi.org/2018/presentations/spikeolaf.html#jun01)