今年の後半は久しぶりにRubyに機能を追加したりしており、Ruby 3.2に3つの機能(もしくは変更)をいれたので紹介したい。Ruby 3.2リリースまであと一ヶ月くらいあるので、現時点でBugなどをみつけたら教えてほしい。
名前の通りtokenの情報を保持するようにし、あとでNodeからtokenを取得できるようにするためのオプション。RubyVM::AbstractSyntaxTreeの.parse,.parse_file,.ofの3メソッドで使うことができる。とりだすときはRubyVM::AbstractSyntaxTree::Nodeの#tokensもしくは#all_tokensでtokenを取得できる。
root =RubyVM::AbstractSyntaxTree.parse("x = 1 + 2",keep_tokens:true)root.tokens# => [[0, :tIDENTIFIER, "x", [1, 0, 1, 1]], [1, :tSP, " ", [1, 1, 1, 2]], ...]root.tokens.map{_1[2]}.join# => "x = 1 + 2"
#tokensの戻り値は要素が4つのArrayになっており、それぞれ順番に
keep_tokensといいつつ、commentなどの終端記号でないものも保持しているので便利に活用できると思う。rubyのtestとしてtestディレクトリ以下のすべての.rbファイルに対してall_tokensで取得できるコードをつなげあわせたものとそのファイルの内容が一致することを確認している。
root =RubyVM::AbstractSyntaxTree.parse("# comment 1class C def m # comment for a method endend",keep_tokens:true)puts root.tokens.map{_1[2]}.join# =># comment 1classCdefm# comment for a methodendend
#tokensは自身とchildrenのNodeがもつtoken全てを返すので、自身のtokenだけが欲しい場合はchildren#tokensを除外する必要がある。
root =RubyVM::AbstractSyntaxTree.parse("1 + 2",keep_tokens:true)opcall = root.children[-1]opcall.tokens.map{_1[2]}.join# => "1 + 2"(opcall.tokens - opcall.children.grep(RubyVM::AbstractSyntaxTree::Node).flat_map(&:tokens)).map{_1[2]}.join# => " + "
ヒアドキュメントに関しては注意が必要でヒアドキュメントを復元するにはtHEREDOC_BEGを目印に#all_tokensを探索する必要がある。これは現状ヒアドキュメントに対応するNODE_STRの位置情報が<<~EOSとして実装されており、#tokensがnodeのlocation情報に依存しているため。以下のコードのようにヒアドキュメントは2つの異なるRangeにまたがることがあるので、location情報側で調整する場合にはこれをどう表すかを決める必要がある。ぱっと思いつくアイデアとしては以下のふたつがあるので、他にアイデアやご意見があればhttps://bugs.ruby-lang.org/ などによせていただきたい。
[<<~EOSの範囲、abc ~ EOSの範囲]をもつようにする#tokensのなかでtHEREDOC_BEGを目印にして、tSTRING_CONTENTとtHEREDOC_ENDを探して#tokensの戻り値に含めるroot =RubyVM::AbstractSyntaxTree.parse(<<~RUBY,keep_tokens:true)str = <<~EOS +"ABC" abc 123EOSRUBYroot.tokens# =>[[0,:tIDENTIFIER,"str", [1,0,1,3]], [1,:tSP,"", [1,3,1,4]], [2,:"=","=", [1,4,1,5]], [3,:tSP,"", [1,5,1,6]], [4,:tHEREDOC_BEG,"<<~EOS", [1,6,1,12]], [8,:tSP,"", [1,12,1,13]], [9,:+,"+", [1,13,1,14]], [10,:tSP,"", [1,14,1,15]], [11,:tSTRING_BEG,"\"", [1,15,1,16]], [12,:tSTRING_CONTENT,"ABC", [1,16,1,19]], [13,:tSTRING_END,"\"", [1,19,1,20]]]root.all_tokens# =>[[0,:tIDENTIFIER,"str", [1,0,1,3]], [1,:tSP,"", [1,3,1,4]], [2,:"=","=", [1,4,1,5]], [3,:tSP,"", [1,5,1,6]], [4,:tHEREDOC_BEG,"<<~EOS", [1,6,1,12]], [5,:tSTRING_CONTENT," abc\n", [2,0,2,6]], [6,:tSTRING_CONTENT," 123\n", [3,0,3,6]], [7,:tHEREDOC_END,"EOS\n", [4,0,4,4]], [8,:tSP,"", [1,12,1,13]], [9,:+,"+", [1,13,1,14]], [10,:tSP,"", [1,14,1,15]], [11,:tSTRING_BEG,"\"", [1,15,1,16]], [12,:tSTRING_CONTENT,"ABC", [1,16,1,19]], [13,:tSTRING_END,"\"", [1,19,1,20]], [14,:nl,"\n", [1,20,1,21]]]root.tokens.map{_1[2]}.join# => "str = <<~EOS + \"ABC\""
関連するbugsのチケットやPRは以下のとおり
SyntaxErrorかどうかという情報だけでは困る、もっと多くの情報がほしいということが稀によくある。そのようなときに使えるオプションとしてerror_tolerantを実装した。このオプションもkeep_tokensと同様にRubyVM::AbstractSyntaxTreeの.parse,.parse_file,.ofの3メソッドで使うことができる。通常ではSyntaxErrorが出てしまうようなケースでも、error_tolerant: trueを渡すことでASTを取得することができる。
root =RubyVM::AbstractSyntaxTree.parse(<<~RUBY)def m a = 10 ifendRUBY# =><internal:ast>:33:in`parse': syntax error, unexpected`end' (SyntaxError) from ../test.rb:1:in `<main>'
root =RubyVM::AbstractSyntaxTree.parse(<<~RUBY,error_tolerant:true)def m a = 10 ifendRUBYpp root# =>(SCOPE@1:0-4:3tbl: []args:nil body: (DEFN@1:0-4:3mid::m body: (SCOPE@1:0-4:3tbl: [:a] args: (ARGS@1:5-1:5pre_num:0pre_init:nilopt:nilfirst_post:nilpost_num:0post_init:nilrest:nilkw:nilkwrest:nilblock:nil)body: (BLOCK@2:2-4:3 (LASGN@2:2-2:8:a (LIT@2:6-2:810)) (IF@3:2-4:3 (ERROR@4:0-4:3)nilnil)))))
error_tolerantを有効にすると以下の3つの点で通常のparserと挙動が変わる。
SyntaxErrorが出なくなる"入力を補ってparseを行う"というのは大きくわけて2つのパターンがあって
1). scriptを最後まで読み終わったときにendが不足している場合にendを補う
例えば以下のコードのようにendが一つ足りないコードがあったときに、一番最後に自動でendを挿入してASTをつくる。
describe"1"do describe"2"do describe"3"do it"here"doendendend
2).endをキーワードとして扱う
最後にendを挿入するだけでは解決できない問題として、以下の例がある。以下では3~4行目はfoo.end というようにメソッド呼び出しとして解釈されてしまう。そしてこのままだと後続のbarメソッドがmodule Zではなくclass Fooに対するメソッド定義になってしまう。このような事態を回避するために、error_tolerantが有効な場合はendをキーワードとして切り出すようにlexerの挙動を変えている。なおコードのindentを確認してキーワードとして切り出すか決めるようにしている。
moduleZclassFoo foo. enddefbarendend
その他にBisonのerror tokenの位置を調整して、復旧可能なコードを増やした。
そのほかの例はtest_error_tolerant_からはじまるテストケースをみてほしい。
関連するbugsのチケットやPRは以下のとおり
Bisonの便利機能を使いたかったのでversionをあげていいですかとというチケットをきり、required versionを3.0にあげた。macOSだと標準ではいっているBisonが2.3だったりするので、もしRubyをコードからビルドしていてこのようなエラーにあたったらbrewなどでBisonのversionをあげてほしい。
parse.tmp.y:12.10-14: require bison 3.0, but have 2.3
これにより--dump=y したときにNodeのタイプが表示されるようになったので、parse.yをいじる人にとってよりわかりやすくなったのではないかと思う。
# BeforeReducing stack by rule 639 (line 5062): $1 = token "integer literal" (1.0-1.1: 1)-> $$ = nterm simple_numeric (1.0-1.1: )# AfterReducing stack by rule 641 (line 5078): $1 = token "integer literal" (1.0-1.1: 1)-> $$ = nterm simple_numeric (1.0-1.1: NODE_LIT)
関連するbugsのチケットやPRは以下のとおり
https://github.com/ruby/ruby/commit/230267d1a8f2b8245e911513926c06299ddeebc8 Bisonのminimum versionを宣言するコミット
Error tolerantの機能をもっと充実させていきたい。現状では限定的かつ大局的な修復しかできていないので、例えばobj.m(arg1,という入力にたいして)を補うなどのようにもっと細かい単位での修復ができるようにしていきたい。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。