はじめに
この記事は Ruby2.6 + Graphvizで抽象構文木をグラフにしてみる という記事を大きく参考にさせていただいています。
ありがとうございます。
rprとは
rprは私が作成したgemで、コマンドラインからRubyのコードを抽象構文木に変換して表示するためのツールです。
Rubyには様々なパーサが存在します。Ruby本体が提供しているRipperやRuby 2.6から提供されているRubyVM::AbstractSyntaxTreeの他にも、Parser gemやRubyParser 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を画像として見ることができます。
実装
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を扱う機会が多い方にインストールして使っていただけるとうれしいです。