Initial commit.

This commit is contained in:
Peter Zotov 2013-04-02 01:15:28 +04:00
commit b68375e123
13 changed files with 829 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.bundle
Gemfile.lock
pkg/*
.rbx/
*.sublime-*
doc/
.yardoc/
coverage/

6
.travis.yml Normal file
View File

@ -0,0 +1,6 @@
language: ruby
rvm:
- 1.9.3
- 2.0
- jruby-19mode
- rbx-19mode

1
.yardopts Normal file
View File

@ -0,0 +1 @@
-r {AST} -m markdown --protected

4
Gemfile Normal file
View File

@ -0,0 +1,4 @@
source "http://rubygems.org"
# Specify your gem's dependencies in furnace.gemspec
gemspec

20
LICENSE.MIT Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2011-2013 Peter Zotov <whitequark@whitequark.org>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
Rakefile Normal file
View File

@ -0,0 +1,19 @@
require 'bundler/gem_tasks'
require 'bundler/setup'
task :default => :test
desc "Run test suite"
task :test do
sh "bacon -a"
end
PAGES_REPO = 'git@github.com:whitequark/ast'
desc "Build and deploy documentation to GitHub pages"
task :pages do
system "git clone #{PAGES_REPO} gh-temp/ -b gh-pages; rm gh-temp/* -rf; touch gh-temp/.nojekyll" or abort
system "yardoc -o gh-temp/; cp gh-temp/frames.html gh-temp/index.html; sed s,index.html,_index.html, -i gh-temp/index.html" or abort
system "cd gh-temp/; git add -A; git commit -m 'Updated pages.'; git push -f origin gh-pages" or abort
FileUtils.rm_rf 'gh-temp'
end

21
ast.gemspec Normal file
View File

@ -0,0 +1,21 @@
Gem::Specification.new do |s|
s.name = 'ast'
s.version = '1.0.0'
s.authors = ["Peter Zotov"]
s.email = ["whitequark@whitequark.org"]
s.homepage = "http://github.com/whitequark/ast"
s.summary = %q{A library for working with Abstract Syntax Trees.}
s.description = s.summary
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.add_development_dependency 'rake', '~> 10.0'
s.add_development_dependency 'bacon', '~> 1.2'
s.add_development_dependency 'bacon-colored_output'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'yard'
s.add_development_dependency 'kramdown'
end

17
lib/ast.rb Normal file
View File

@ -0,0 +1,17 @@
module AST
# AST is a library for manipulating abstract syntax trees.
#
# It embraces immutability; each AST node is inherently frozen at
# creation, and updating a child node requires recreating that node
# and its every parent, recursively.
# This is a design choice. It does create some pressure on
# garbage collector, but completely eliminates all concurrency
# and aliasing problems.
#
# See also {Node}, {Processor} and {Sexp} for additional
# recommendations and design patterns.
require_relative "ast/node"
require_relative "ast/processor"
require_relative "ast/sexp"
end

217
lib/ast/node.rb Normal file
View File

@ -0,0 +1,217 @@
module AST
# Node is an immutable class, instances of which represent abstract
# syntax tree nodes. It combines semantic information (i.e. anything
# that affects the algorithmic properties of a program) with
# meta-information (line numbers or compiler intermediates).
#
# Notes on inheritance
# ====================
#
# The distinction between semantics and metadata is important. Complete
# semantic information should be contained within just the {#type} and
# {#children} of a Node instance; in other words, if an AST was to be
# stripped of all meta-information, it should remain a valid AST which
# could be successfully processed to yield a result with the same
# algorithmic properties.
#
# Thus, Node should never be inherited in order to define methods which
# affect or return semantic information, such as getters for `class_name`,
# `superclass` and `body` in the case of a hypothetical `ClassNode`. The
# correct solution is to use a generic Node with a {#type} of `:class`
# and three children. See also {Processor} for tips on working with such
# ASTs.
#
# On the other hand, Node can and should be inherited to define
# application-specific metadata (see also {#initialize}) or customize the
# printing format. It is expected that an application would have one or two
# such classes and use them across the entire codebase.
#
# The rationale for this pattern is extensibility and maintainability.
# Unlike static ones, dynamic languages do not require the presence of a
# predefined, rigid structure, nor does it improve dispatch efficiency,
# and while such a structure can certainly be defined, it does not add
# any value but incurs a maintaining cost.
# For example, extending the AST even with a transformation-local
# temporary node type requires making globally visible changes to
# the codebase.
#
class Node
# Returns the type of this node.
# @return [Symbol]
attr_reader :type
# Returns the children of this node.
# The returned value is frozen.
# @return [Array]
attr_reader :children
# Constructs a new instance of Node.
#
# The arguments `type` and `children` are converted with `to_sym` and
# `to_a` respectively. Additionally, the result of converting `children`
# is frozen. While mutating the arguments is generally considered harmful,
# the most common case is to pass an array literal to the constructor. If
# your code does not expect the argument to be frozen, use `#dup`.
#
# The `properties` hash is passed to {#assign_properties}.
def initialize(type, children=[], properties={})
@type, @children = type.to_sym, children.to_a.freeze
assign_properties(properties)
freeze
end
# By default, each entry in the `properties` hash is assigned to
# an instance variable in this instance of Node. A subclass should define
# attribute readers for such variables. The values passed in the hash
# are not frozen or whitelisted; such behavior can also be implemented\
# by subclassing Node and overriding this method.
#
# @return [nil]
def assign_properties(properties)
properties.each do |name, value|
instance_variable_set :"@#{name}", value
end
nil
end
protected :assign_properties
alias :original_dup :dup
private :original_dup
# Nodes are already frozen, so there is no harm in returning the
# current node as opposed to initializing from scratch and freezing
# another one.
#
# @return self
def dup
self
end
# Returns a new instance of Node where non-nil arguments replace the
# corresponding fields of `self`.
#
# For example, `Node.new(:foo, [ 1, 2 ]).updated(:bar)` would yield
# `(bar 1 2)`, and `Node.new(:foo, [ 1, 2 ]).updated(nil, [])` would
# yield `(foo)`.
#
# If the resulting node would be identical to `self`, does nothing.
#
# @param [Symbol, nil] type
# @param [Array, nil] children
# @param [Hash, nil] properties
# @return [AST::Node]
def updated(type=nil, children=nil, properties=nil)
new_type = type || @type
new_children = children || @children
new_properties = properties || {}
if @type == new_type &&
@children == new_children &&
properties.nil?
self
else
original_dup.send :initialize, new_type, new_children, new_properties
end
end
# Compares `self` to `other`, possibly converting with `to_ast`. Only
# `type` and `children` are compared; metadata is deliberately ignored.
#
# @return [Boolean]
def ==(other)
if equal?(other)
true
elsif other.respond_to? :to_ast
other = other.to_ast
other.type == self.type &&
other.children == self.children
else
false
end
end
# Concatenates `array` with `children` and returns the resulting node.
#
# @return [AST::Node]
def concat(array)
updated(nil, @children + array.to_a)
end
alias + concat
# Appends `element` to `children` and returns the resulting node.
#
# @return [AST::Node]
def append(element)
updated(nil, @children + [element])
end
alias << append
# Converts `self` to a concise s-expression, omitting any children.
#
# @return [String]
def to_s
"(#{fancy_type} ...)"
end
# Returns {#children}. This is very useful in order to decompose nodes
# concisely. For example:
#
# node = s(:gasgn, :$foo, s(:integer, 1))
# s
# var_name, value = *node
# p var_name # => :$foo
# p value # => (integer 1)
#
# @return [Array]
def to_a
children
end
# Converts `self` to a pretty-printed s-expression.
#
# @param [Integer] indent Base indentation level.
# @return [String]
def to_sexp(indent=0)
indented = " " * indent
sexp = "#{indented}(#{fancy_type}"
first_node_child = children.index do |child|
child.is_a?(Node) || child.is_a?(Array)
end || children.count
children.each_with_index do |child, idx|
if child.is_a?(Node) && idx >= first_node_child
sexp << "\n#{child.to_sexp(indent + 1)}"
else
sexp << " #{child.inspect}"
end
end
sexp << ")"
sexp
end
alias :inspect :to_sexp
# @return [AST::Node] self
def to_ast
self
end
protected
# Returns `@type` with all underscores replaced by dashes. This allows
# to write symbol literals without quotes in Ruby sources and yet have
# nicely looking s-expressions.
#
# @return [String]
def fancy_type
@type.to_s.gsub('_', '-')
end
end
end

264
lib/ast/processor.rb Normal file
View File

@ -0,0 +1,264 @@
module AST
# Processor is a class which helps transforming one AST into another.
# In a nutshell, the {#process} method accepts a {Node} and dispatches
# it to a handler corresponding to its type, and returns a (possibly)
# updated variant of the node.
#
# Processor has a set of associated design patterns. They are best
# explained with a concrete example. Let's define a simple arithmetic
# language and an AST format for it:
#
# Terminals (AST nodes which do not have other AST nodes inside):
#
# * `(integer <int-literal>)`,
#
# Nonterminals (AST nodes with other nodes as children):
#
# * `(add <node> <node>)`,
# * `(multiply <node> <node>)`,
# * `(divide <node> <node>)`,
# * `(negate <node>)`,
# * `(store <node> <string-literal>)`: stores value of `<node>` into a variable named `<string-literal>`,
# * `(load <string-literal>)`: loads value of a variable named `<string-literal>`,
# * `(each <node> ...): computes each of the `<node>`s and prints the result.
#
# All AST nodes have the same Ruby class, and therefore they don't
# know how to traverse themselves. (A solution which dynamically checks the
# type of children is possible, but is slow and error-prone.) So, a subclass
# of Processor which knows how to traverse the entire tree should be defined.
# Such subclass has a handler for each nonterminal node which recursively
# processes children nodes:
#
# require 'ast'
#
# class ArithmeticsProcessor < AST::Processor
# # This method traverses any binary operators such as (add) or (multiply).
# def process_binary_op(node)
# # Children aren't decomposed automatically; it is suggested to use Ruby
# # multiple assignment expansion, as it is very convenient here.
# left_expr, right_expr = *node
#
# # AST::Node#updated won't change node type if nil is passed as a first
# # argument, which allows to reuse the same handler for multiple node types
# # using `alias' (below).
# node.updated(nil, [
# process(left_expr),
# process(right_expr)
# ])
# end
# alias on_add process_binary_op
# alias on_multiply process_binary_op
# alias on_divide process_binary_op
#
# def on_negate(node)
# # It is also possible to use #process_all for more compact code
# # if every child is a Node.
# node.updated(nil, process_all(node))
# end
#
# def on_store(node)
# expr, variable_name = *node
#
# # Note that variable_name is not a Node and thus isn't passed to #process.
# node.updated(nil, [
# process(expr),
# variable_name
# ])
# end
#
# # (load) is effectively a terminal node, and so it does not need
# # an explicit handler, as the following is the default behavior.
# def on_load(node)
# nil
# end
#
# def on_each(node)
# node.updated(nil, process_all(node))
# end
# end
#
# Let's test our ArithmeticsProcessor:
#
# include AST::Sexp
# expr = s(:add, s(:integer, 2), s(:integer, 2))
#
# p ArithmeticsProcessor.new.process(expr) == expr # => true
#
# As expected, it does not change anything at all. This isn't actually
# very useful, so let's now define a Calculator, which will compute the
# expression values:
#
# # This Processor folds nonterminal nodes and returns an (integer)
# # terminal node.
# class ArithmeticsCalculator < ArithmeticsProcessor
# def compute_op(node)
# # First, node children are processed and then unpacked to local
# # variables.
# nodes = process_all(node)
#
# if nodes.all? { |node| node.type == :integer }
# # If each of those nodes represents a literal, we can fold this
# # node!
# values = nodes.map { |node| node.children.first }
# AST::Node.new(:integer, [
# yield(values)
# ])
# else
# # Otherwise, we can just leave the current node in the tree and
# # only update it with processed children nodes, which can be
# # partially folded.
# node.updated(nil, nodes)
# end
# end
#
# def on_add(node)
# compute_op(node) { |left, right| left + right }
# end
#
# def on_multiply(node)
# compute_op(node) { |left, right| left * right }
# end
# end
#
# Let's check:
#
# p ArithmeticsCalculator.new.process(expr) # => (integer 4)
#
# Excellent, the calculator works! Now, a careful reader could notice that
# the ArithmeticsCalculator does not know how to divide numbers. What if we
# pass an expression with division to it?
#
# expr_with_division = \
# s(:add,
# s(:integer, 1),
# s(:divide,
# s(:add, s(:integer, 8), s(:integer, 4)),
# s(:integer, 3))) # 1 + (8 + 4) / 3
#
# folded_expr_with_division = ArithmeticsCalculator.new.process(expr_with_division)
# p folded_expr_with_division
# # => (add
# # (integer 1)
# # (divide
# # (integer 12)
# # (integer 3)))
#
# As you can see, the expression was folded _partially_: the inner `(add)` node which
# could be computed was folded to `(integer 12)`, the `(divide)` node is left as-is
# because there is no computing handler for it, and the root `(add)` node was also left
# as it is because some of its children were not literals.
#
# Note that this partial folding is only possible because the _data_ format, i.e.
# the format in which the computed values of the nodes are represented, is the same as
# the AST itself.
#
# Let's extend our ArithmeticsCalculator class further.
#
# class ArithmeticsCalculator
# def on_divide(node)
# compute_op(node) { |left, right| left / right }
# end
#
# def on_negate(node)
# # Note how #compute_op works regardless of the operator arity.
# compute_op(node) { |value| -value }
# end
# end
#
# Now, let's apply our renewed ArithmeticsCalculator to a partial result of previous
# evaluation:
#
# p ArithmeticsCalculator.new.process(expr_with_division) # => (integer 5)
#
# Five! Excellent. This is also pretty much how CRuby 1.8 executed its programs.
#
# Now, let's do some automated bug searching. Division by zero is an error, right?
# So if we could detect that someone has divided by zero before the program is even
# run, that could save some debugging time.
#
# class DivisionByZeroVerifier < ArithmeticsProcessor
# class VerificationFailure < Exception; end
#
# def on_divide(node)
# # You need to process the children to handle nested divisions
# # such as:
# # (divide
# # (integer 1)
# # (divide (integer 1) (integer 0))
# left, right = process_all(node)
#
# if right.type == :integer &&
# right.children.first == 0
# raise VerificationFailure, "Ouch! This code divides by zero."
# end
# end
#
# def divides_by_zero?(ast)
# process(ast)
# false
# rescue VerificationFailure
# true
# end
# end
#
# nice_expr = \
# s(:divide,
# s(:add, s(:integer, 10), s(:integer, 2)),
# s(:integer, 4))
#
# p DivisionByZeroVerifier.new.divides_by_zero?(nice_expr)
# # => false. Good.
#
# bad_expr = \
# s(:add, s(:integer, 10),
# s(:divide, s(:integer, 1), s(:integer, 0)))
#
# p DivisionByZeroVerifier.new.divides_by_zero?(bad_expr)
# # => true. WHOOPS. DO NOT RUN THIS.
#
# Of course, this won't detect more complex cases... unless you use some partial
# evaluation before! The possibilites are endless. Have fun.
class Processor
# Dispatches `node`. If a node has type `:foo`, then a handler named
# `on_foo` is invoked with one argument, the `node`; if there isn't
# such a handler, {#handler_missing} is invoked with the same argument.
#
# If the handler returns `nil`, `node` is returned; otherwise, the return
# value of the handler is passed along.
#
# @param [AST::Node, nil] node
# @return [AST::Node]
def process(node)
node = node.to_ast
# Invoke a specific handler
on_handler = :"on_#{node.type}"
if respond_to? on_handler
new_node = send on_handler, node
else
new_node = handler_missing(node)
end
node = new_node if new_node
node
end
# {#process}es each node from `nodes` and returns an array of results.
#
# @param [Array<AST::Node>] nodes
# @return [Array<AST::Node>]
def process_all(nodes)
nodes.to_a.map do |node|
process node
end
end
# Default handler. Does nothing.
#
# @param [AST::Node] node
# @return [AST::Node, nil]
def handler_missing(node)
end
end
end

30
lib/ast/sexp.rb Normal file
View File

@ -0,0 +1,30 @@
module AST
# This simple module is very useful in the cases where one needs
# to define deeply nested ASTs from Ruby code, for example, in
# tests. It should be used like this:
#
# describe YourLanguage::AST do
# include Sexp
#
# it "should correctly parse expressions" do
# YourLanguage.parse("1 + 2 * 3").should ==
# s(:add,
# s(:integer, 1),
# s(:multiply,
# s(:integer, 2),
# s(:integer, 3)))
# end
# end
#
# This way the amount of boilerplate code is greatly reduced.
module Sexp
# Creates a {Node} with type `type` and children `children`.
# Note that the resulting node is of the type AST::Node and not a
# subclass.
# This would not pose a problem with comparisons, as {Node#==}
# ignores metadata.
def s(type, *children)
Node.new(type, children)
end
end
end

7
test/helper.rb Normal file
View File

@ -0,0 +1,7 @@
require 'bacon'
require 'bacon/colored_output'
require 'simplecov'
SimpleCov.start
require 'ast'

215
test/test_ast.rb Normal file
View File

@ -0,0 +1,215 @@
require_relative 'helper'
describe AST::Node do
extend AST::Sexp
class MetaNode < AST::Node
attr_reader :meta
end
before do
@node = AST::Node.new(:node, [ 0, 1 ])
@metanode = MetaNode.new(:node, [ 0, 1 ], meta: 'value')
end
it 'should have accessors for type and children' do
@node.type.should.equal :node
@node.children.should.equal [0, 1]
end
it 'should set metadata' do
@metanode.meta.should.equal 'value'
end
it 'should be frozen' do
@node.frozen?.should.be.true
@node.children.frozen?.should.be.true
end
it 'should return self when duping' do
@node.dup.should.equal? @node
end
it 'should return an updated node, but only if needed' do
@node.updated().should.be.identical_to @node
@node.updated(:node).should.be.identical_to @node
@node.updated(nil, [0, 1]).should.be.identical_to @node
updated = @node.updated(:other_node)
updated.should.not.be.identical_to @node
updated.type.should.equal :other_node
updated.children.should.equal @node.children
updated.frozen?.should.be.true
updated = @node.updated(nil, [1, 1])
updated.should.not.be.identical_to @node
updated.type.should.equal @node.type
updated.children.should.equal [1, 1]
updated = @metanode.updated(nil, nil, meta: 'other_value')
updated.meta.should.equal 'other_value'
end
it 'should use fancy type in to_s' do
node = AST::Node.new(:ast_node)
node.to_s.should.equal '(ast-node ...)'
end
it 'should format to_sexp correctly' do
AST::Node.new(:a, [ :sym, [ 1, 2 ] ]).to_sexp.should.equal '(a :sym [1, 2])'
AST::Node.new(:a, [ :sym, @node ]).to_sexp.should.equal "(a :sym\n (node 0 1))"
AST::Node.new(:a, [ :sym,
AST::Node.new(:b, [ @node, @node ])
]).to_sexp.should.equal "(a :sym\n (b\n (node 0 1)\n (node 0 1)))"
end
it 'should return self in to_ast' do
@node.to_ast.should.be.identical_to @node
end
it 'should only use type and children in comparisons' do
@node.should.equal @node
@node.should.equal @metanode
@node.should.not.equal :foo
mock_node = Object.new.tap do |obj|
def obj.to_ast
self
end
def obj.type
:node
end
def obj.children
[ 0, 1 ]
end
end
@node.should.equal mock_node
end
it 'should allow to decompose nodes with a, b = *node' do
node = s(:gasgn, :$foo, s(:integer, 1))
var_name, value = *node
var_name.should.equal :$foo
value.should.equal s(:integer, 1)
end
it 'should concatenate with arrays' do
node = s(:gasgn, :$foo)
(node + [s(:integer, 1)]).
should.equal s(:gasgn, :$foo, s(:integer, 1))
end
it 'should append elements' do
node = s(:array)
(node << s(:integer, 1) << s(:string, "foo")).
should.equal s(:array, s(:integer, 1), s(:string, "foo"))
end
end
describe AST::Processor do
extend AST::Sexp
def have_sexp(text)
text = text.lines.map { |line| line.sub /^ +\|(.+)/, '\1' }.join.rstrip
lambda { |ast| ast.to_sexp == text }
end
class MockProcessor < AST::Processor
attr_reader :counts
def initialize
@counts = Hash.new(0)
end
def on_root(node)
count_node(node)
node.updated(nil, process_all(node.children))
end
alias on_body on_root
def on_def(node)
count_node(node)
name, arglist, body = node.children
node.updated(:def, [ name, process(arglist), process(body) ])
end
def handler_missing(node)
count_node(node)
end
def count_node(node)
@counts[node.type] += 1; nil
end
end
before do
@ast = AST::Node.new(:root, [
AST::Node.new(:def, [ :func,
AST::Node.new(:arglist, [ :foo, :bar ]),
AST::Node.new(:body, [
AST::Node.new(:invoke, [ :puts, "Hello world" ])
])
]),
AST::Node.new(:invoke, [ :func ])
])
@processor = MockProcessor.new
end
it 'should visit every node' do
@processor.process(@ast).should.equal @ast
@processor.counts.should.equal({
root: 1,
def: 1,
arglist: 1,
body: 1,
invoke: 2
})
end
it 'should be able to replace inner nodes' do
def @processor.on_arglist(node)
node.updated(:new_fancy_arglist)
end
@processor.process(@ast).should have_sexp(<<-SEXP)
|(root
| (def :func
| (new-fancy-arglist :foo :bar)
| (body
| (invoke :puts "Hello world")))
| (invoke :func))
SEXP
end
it 'should build sexps' do
s(:add,
s(:integer, 1),
s(:multiply,
s(:integer, 2),
s(:integer, 3))).should have_sexp(<<-SEXP)
|(add
| (integer 1)
| (multiply
| (integer 2)
| (integer 3)))
SEXP
end
it 'should refuse to process non-nodes' do
-> { @processor.process(nil) }.should.raise NoMethodError, %r|to_ast|
-> { @processor.process([]) }.should.raise NoMethodError, %r|to_ast|
end
it 'should allow to visit nodes with process_all(node)' do
@processor.process_all s(:foo, s(:bar), s(:integer, 1))
@processor.counts.should.equal({
bar: 1,
integer: 1,
})
end
end