調査結果を書いただけで、あまり良い解決方法は見つけられていない・・・。
調べたきっかけ
RubyKaigi 2019でRubyVM::AbstractSyntaxTree.ofメソッドを 悪用 活用している方々の発表を聞いて、自分でも試してみようとirbでメソッドを使ったら、無慈悲にもエラーメッセージが表示された。
irb(main):001:0> pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 }) Traceback (most recent call last): 5: from /Users/sinsoku/.rbenv/versions/2.6.2/bin/irb:23:in `<main>' 4: from /Users/sinsoku/.rbenv/versions/2.6.2/bin/irb:23:in `load' 3: from /Users/sinsoku/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>' 2: from (irb):1 1: from (irb):1:in `of' Errno::ENOENT (No such file or directory @ rb_sysopen - (irb))
「自分の使い方が悪い・・・?」「いや、空気を読んで動いてくれないRubyが悪い」
という気持ちになったので、ソースコードを読んで直せないか調べてみた。
RubyVM::AbstractSyntaxTree.of のコードの概要
どうやらast.cの中のrb_ast_s_ofの関数が呼ばれているっぽい。
VALUE path, node, lines; int node_id; const rb_iseq_t *iseq = NULL; if (rb_obj_is_proc(body)) { iseq = vm_proc_iseq(body); if (!rb_obj_is_iseq((VALUE)iseq)) { iseq = NULL; } } else { iseq = rb_method_iseq(body); } if (!iseq) return Qnil; path = rb_iseq_path(iseq); node_id = iseq->body->location.node_id;
コード片(ブロックで渡されたコード)を受け取って、iseqを作って、node_idを取り出している。
if (!NIL_P(lines = script_lines(path))) { node = rb_ast_parse_array(lines); } else if (RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0) { node = rb_ast_parse_str(rb_e_script); } else { node = rb_ast_parse_file(path); } return node_find(node, node_id);
これらの分岐は「ソースコード全体をASTにしたnodeを取得する」処理です。 つまり、 ソースコード全体のASTからコード片と同じnode_idのASTを探して返す ような実装になっています。
コード片だけではファイル内の行列数が分からないため、ソースコード全体のASTを一度作る必要があるのかな。
エラーの原因
エラーメッセージの「Errno::ENOENT (No such file or directory @ rb_sysopen - (irb))」で分かるように、 rb_ast_parse_file
が例外を出しています。
これを何かしらの方法で避ける必要がありそうです。
SCRIPT_LINES__ を使う処理
ソースコード全体のASTを取得する処理は以下の3つの分岐になっています。
- SCRIPT_LINES__ で取得した配列を使う処理
ruby -e
に対応するための処理- ソースコードのファイルを読み込む処理
script_lines関数のコードを読むと分かるように、 SCRIPT_LINES__
の定数からソースコードの配列を取得しています。
つまり、evalやirbで渡した文字列をSCRIPT_LINES__に配列として突っ込めば、良い感じに動きそうです。
eval を上書きしてみる
module AbstractSyntaxTreeOfPatch REPL_FNAME = ["(eval)", "(irb)"].freeze def eval(expr, bind = nil, fname = "(eval)", lineno = 1) return super unless REPL_FNAME.include?(fname) defined = Object.const_defined?(:SCRIPT_LINES__) Object.const_set(:SCRIPT_LINES__, {}) unless defined Object::SCRIPT_LINES__[fname] = [expr] super.tap do if defined Object::SCRIPT_LINES__.delete(fname) else Object.send(:remove_const, :SCRIPT_LINES__) end end end end Object.prepend(AbstractSyntaxTreeOfPatch) # コード内 pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 }) # eval pp eval('RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })') binding.irb # irbで `pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })` を試す puts
こんな感じでevalを上書きすることで、それっぽいASTが返ってくるようになった。
いろんな問題
byebugやpryもevalを使っているだろうし、evalを上書きすれば他も簡単に対応できる....と思っていた。 世の中はそんなに甘くなかった。
- byebugはC言語でevalを呼んでいるので、ruby上でevalを上書きしても意味がなかった
- pryもevalを上書きしただけではダメだった
- あまりコードを追っていないので原因は不明
- evalを上書きすると、irbの
help
でエラーになるuninitialized constant AbstractSyntaxTreeOfPatch::ExtendCommand
が起きた
良い感じに直せたら、ruby/rubyにプルリクを作れるかも・・・?とか思ったけど、色々な問題が起きるので厳しい感じ。
まとめ
あまり良い成果は出ていないけど、とりあえず最近調べていた事をブログにまとめておいた。
irbやevalでASTを作れると、色々と捗りそうなんだけど難しい。
別解: irbでRubyVM::AbstractSyntaxTree.ofを動かす方法
@hanachin_さんがTracePointを使って動かす方法を書かれているので、興味ある方はそちらを読んでみると良いかと思います。