LoginSignup
7
5

More than 5 years have passed since last update.

rprでRubyの抽象構文木を画像として出力する

Posted at

はじめに

この記事は Ruby2.6 + Graphvizで抽象構文木をグラフにしてみる という記事を大きく参考にさせていただいています。
ありがとうございます。

rprとは

rprは私が作成したgemで、コマンドラインからRubyのコードを抽象構文木に変換して表示するためのツールです。

Rubyには様々なパーサが存在します。Ruby本体が提供しているRipperやRuby 2.6から提供されているRubyVM::AbstractSyntaxTreeの他にも、Parser gemRubyParser gemなどがあります。
rprを使うと、これらのパーサを同一のインターフェイスで扱えます。
またコマンドラインから扱うことに特化しているため、手軽に構文解析結果を表示できます。
例を見てみましょう。

# Ripper.sexp メソッドを使って構文解析をし、ppで表示する
# -pと-fの値はこれがデフォルトなので、 rpr -e 'puts 1 + 2' のように省略しても同様の結果になる。
$ rpr -e 'puts 1 + 2' -p sexp -f pp
[:program,
 [[:command,
   [:@ident, "puts", [1, 0]],
   [:args_add_block,
    [[:binary, [:@int, "1", [1, 5]], :+, [:@int, "2", [1, 9]]]],
    false]]]]

# Parser gemを使って構文解析する
$ rpr -e 'puts 1 + 2' -p parser -f pp
s(:send, nil, :puts,
  s(:send,
    s(:int, 1), :+,
    s(:int, 2)))

# 構文木をpryで扱う
# self に構文木が入っている。
$ rpr -e 'puts 1 + 2' -p parser -f pry
[1] pry(#<Parser::AST::Node>)> self
=> s(:send, nil, :puts,
  s(:send,
    s(:int, 1), :+,
    s(:int, 2)))
[2] pry(#<Parser::AST::Node>)> self.type
=> :send
[3] pry(#<Parser::AST::Node>)> self.children
=> [nil, :puts, s(:send,
  s(:int, 1), :+,
  s(:int, 2))]

-fオプションはフォーマッタを指定するオプションです。バージョン1.91からフォーマッタにdotを指定できるようにしたので、この記事ではそのオプションについて解説します。

Dotフォーマッタの使用例

Dotフォーマッタは構文木をDOT Languageとして出力するためのフォーマッタです。DOT LanguageはGraphvizによって画像に変換できます。
実際に使用例を見てみましょう。次のコマンドを実行すると、ast.pngが生成されます。

$ rpr -e 'puts 1 + 2' -p parser -f dot | dot -Tpng -oast.png
$ open ast.png

ast.pngは次のようになります。

ast.png

このようにASTを画像として見ることができます。

実装

rprでは複数の構文木の形式に対応する必要があるため、各構文木を共通のインターフェイスで扱えるよう、Refinementsを用いてパッチをあてています。
全てのオブジェクトにnode_value, children, traversable?という3つのメソッドを定義しています。2
node_valueはそのノードのグラフ上での表示内容をStringで返します。
traversable?はそのノードが子要素を持っているかどうかを返します。
そしてchildrenはそのノードの子要素をArrayで返します。

これらのメソッドをフォーマッタから扱うことで、1つのコードで複数のASTの形式をサポートしています。

module UnifiedInterface
  refine Object do
    def node_value() inspect end
    def traversable?() false end
  end

  if defined?(Rpr::Parser::Sexp)
    refine Array do
      def node_value() traversable? ? self[0].to_s : super end
      def children
        res = []
        self[1..-1].each do |child|
          if child.is_a?(Array) &&
             !child.traversable? &&
             !(child.size == 2 && child[0].is_a?(Integer) && child[1].is_a?(Integer))
            res.concat(child)
          else
            res << child
          end
        end
        res
      end
      def traversable?() self.first.is_a?(Symbol) end
    end
  end

  if defined?(Rpr::Parser::Parser) || defined?(Rpr::Parser::Rubocop)
    refine ::Parser::AST::Node do
      def node_value() self.type.to_s end
      def traversable?() true end
    end
  end

  if defined?(Rpr::Parser::Rubyparser)
    refine Sexp do
      def node_value() self.sexp_type.to_s end
      def children() self.each.to_a end
      def traversable?() true end
    end
  end

  if defined?(Rpr::Parser::Rubyvm_ast)
    refine RubyVM::AbstractSyntaxTree::Node do
      def node_value() self.type.to_s end
      def traversable?() true end
    end
  end
end

DOT Languageを出力する部分の実装は冒頭にあげた記事のものとほとんど変わりがないので、解説を省略します。

最後に

RubyのASTをコマンドラインから扱うrprと、新機能であるdotフォーマットの紹介でした。
RubyのASTを扱う機会が多い方にインストールして使っていただけるとうれしいです。


  1. v1.9.0から対応していますが、バグがあったのでv1.9.1をリリースしています。そちらをお使いください。 

  2. traversable?がfalseを返すならchildrenは呼ばれないので、正確にはObjectにはchildrenは実装されていません。 

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5