Skip to content

Instantly share code, notes, and snippets.

@yui-knk
Created May 26, 2018 09:34
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yui-knk/169fc1ae1fc8c88547cc6ee2bca4e351 to your computer and use it in GitHub Desktop.
Save yui-knk/169fc1ae1fc8c88547cc6ee2bca4e351 to your computer and use it in GitHub Desktop.

RNode with code positions

RubyKaigi 2018の2日目、15:50に"RNode with code positions"というお話をします。当日どういった話をするか、書いておきたいと思います。

"RNode"と"code positions"とは

表題にある"RNode"。これはRubyのAST(Abstract Syntax Tree: 抽象構文木)のためのデータ構造(構造体)のことです。"code position(s)"というのは、その名の通り、コードに関する位置情報のことです。

実はRuby 2.5からRubyが内部的に保有する位置情報が増えました。 お手元のRuby 2.5と2.4で以下のコードを実行して、その結果を比較してみてください。

$ ruby --dump=p -e '"str".upcase'

おそらく、こんな差分が出るとおもいます。

@@ -1,8 +1,8 @@
 #     +- nd_body:
-#     |   @ NODE_CALL (line: 1)
+#     |   @ NODE_CALL (line: 1, code_range: (1,0)-(1,12))
 #     |   +- nd_mid: :upcase
 #     |   +- nd_recv:
-#     |   |   @ NODE_STR (line: 1)
+#     |   |   @ NODE_STR (line: 1, code_range: (1,0)-(1,5))
 #     |   |   +- nd_lit: "str"
 #     |   +- nd_args:
 #     |       (null node)

Ruby 2.5系ではcode_range: (1,0)-(1,12)という情報が増えていることが分かります。 上の例では

  • "str"というのがNODE_STR(文字列リテラル)で、それは1行目の0番目から、1行目の5番目の部分に相当する
  • "str".upcaseというのがNODE_CALL(メソッド呼び出し)で、それは1行目の0番目から、1行目の12番目の部分に相当する

ということがわかります。 行番号の情報しか保有していなかったRuby 2.4系に比べて、Ruby 2.5系では開始位置/終了位置の行番号/オフセット情報(カラム番号)を保有するようになっています。

なぜこの情報が必要か

このような情報が必要になった背景には、同じくRuby 2.5から導入された

  • Branch coverage
  • Method coverage

という機能があります。

Branch coverageというのは、その名のとおり分岐に関するカバレッジです。

def eq_zero(i)
  (i == 0) ? :t : :f
end

というコードは分岐を1つ含んでいます。このメソッドのテストがあったとして、そのテストを評価するとき、例えば:tを返すケースと、:fを返すケースの両方があるかどうかを確認したいとしましょう。Ruby 2.4までの場合、"ある行を実行したか"というカバレッジしかとれませんでした。なので、eq_zero(0)のケースがテストされているだけで、このメソッドの行単位でのカバレッジは 1/1、つまり100%になってしまうわけです。Branch coverageとして計測すれば、thenの方しか実行されていないことがわかるので 1/2、つまり50%であることがわかるという仕組みです。

coverageに関する詳しい情報は 遠藤さんの去年の発表 をみてください。

Branch coverageに関して、細かい位置情報がどうして必要になるかというと、それは以下の2つの理由によります。1つめはガバレッジ測定結果の可視化に関するものです。

def eq_zero(i)
  (i == 0) ? :t : :f
              |   +------ 0
              +---------- 1
end

というように可視化する場合に、thenelseに該当するコードの範囲がわかると、可視化が簡単になります。

2つめは1行に複数の分岐を書くことができるためです。

(a == b) ? ((c == d) ? :A : :B) : :C

というコードは1行に2つの分岐をふくんでいます。1つめのthen (((c == d) ? :A : :B))と2つめのthen (:A)を区別するときに、該当するコードの範囲として表現できると、どの分岐の話をしているか明確になります。

どうやって位置情報を実装したのか

頑張って実装しました。という冗談はさておき、ここはそれなりに長い話になるので、ぜひ当日発表を聞きにきてください。 この話をする際には、どうしても"(悪魔城)parse.y"とか"bison"とか、ちょっとだけVMとかの話に触れる必要があります。

などを一読しておくと、いろいろ捗るかと思います。

他に応用できないのか

いくつか応用できる場所は考えており、その話も当日したいと思います。 一例をあげると、NoMethodErrorのメッセージをより詳しくするということが考えられます。

class A
  def foo
    nil
  end
end

A.new.foo.foo

こういうコードを実行した時に、以下のようなメッセージをだすことを考えています。

Traceback (most recent call last):
/tmp/test.rb:7:in `<main>': undefined method `foo' for nil:NilClass (NoMethodError)

A.new.foo.foo
         ^^^^

PoCの実装はGitHubにあげてあります。Buildして、上記のコードを適当な名前のファイル(例えば"test.rb")にコピーしてもらって、以下のように実行していただくことができるはずです。 手を抜いてno_method_error_ext.rbNoMethodErrorを拡張しているのがばればれですね。。。

$ ruby -r./no_method_error_ext.rb test.rb

余談

この辺を触り始めたのは、去年のRubyKaigiに向かう新幹線のなかでした。行きの新幹線でRNode構造体にnd_reservedという使われていなさそうなメンバーをみつけ、これをうまく使えないのかなぁと考えていました。その後広島で笹田さんや中田さんに、"これ空いてますよね? 使ってみていいですか?"という話をした記憶があります(たしか)。遠藤さんの発表で"だれかこのあたり実装してみてくれませんか"という呼びかけもあり、広島から戻ってから"parse.y"をいじるようになりました。 去年のRubyKaigiをきっかけに実装を始めた機能について、今年のRubyKaigiでお話ができるということで、個人的には感慨深くおもっています。

それでは仙台でお会いしましょう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment