ast 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5324cd0fc7799039b3badf88291b21e0857933c9
4
+ data.tar.gz: 75f81158a19dd47fb7c714805494a9c74f227d7b
5
+ SHA512:
6
+ metadata.gz: 156b89d209dee02bfa4e416c9ea21c82dbe71ad8a23d1780d5131f7e1218accc46c142af91291637b92fa429560a81278e907e8238093fab753f73f5be32fd02
7
+ data.tar.gz: 7e6176035054a53088cf34fcab3ad6f92c51cbf9f6abc53934fb622c1b12d8432b5a559fe9085e631f23961e130d86fe07921adf47d2c240905b580eb3afbfa7
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
4
+ .rbx/
5
+ *.sublime-*
6
+ doc/
7
+ .yardoc/
8
+ coverage/
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - jruby-19mode
6
+ - rbx-19mode
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -r {AST} -m markdown --protected
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in furnace.gemspec
4
+ gemspec
data/LICENSE.MIT ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2013 Peter Zotov <whitequark@whitequark.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included
12
+ in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+
4
+ task :default => :test
5
+
6
+ desc "Run test suite"
7
+ task :test do
8
+ sh "bacon -a"
9
+ end
10
+
11
+ PAGES_REPO = 'git@github.com:whitequark/ast'
12
+
13
+ desc "Build and deploy documentation to GitHub pages"
14
+ task :pages do
15
+ system "git clone #{PAGES_REPO} gh-temp/ -b gh-pages; rm gh-temp/* -rf; touch gh-temp/.nojekyll" or abort
16
+ 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
17
+ system "cd gh-temp/; git add -A; git commit -m 'Updated pages.'; git push -f origin gh-pages" or abort
18
+ FileUtils.rm_rf 'gh-temp'
19
+ end
data/ast.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'ast'
3
+ s.version = '1.0.0'
4
+ s.authors = ["Peter Zotov"]
5
+ s.email = ["whitequark@whitequark.org"]
6
+ s.homepage = "http://github.com/whitequark/ast"
7
+ s.summary = %q{A library for working with Abstract Syntax Trees.}
8
+ s.description = s.summary
9
+
10
+ s.files = `git ls-files`.split("\n")
11
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ s.require_paths = ["lib"]
14
+
15
+ s.add_development_dependency 'rake', '~> 10.0'
16
+ s.add_development_dependency 'bacon', '~> 1.2'
17
+ s.add_development_dependency 'bacon-colored_output'
18
+ s.add_development_dependency 'simplecov'
19
+ s.add_development_dependency 'yard'
20
+ s.add_development_dependency 'kramdown'
21
+ end
data/lib/ast.rb ADDED
@@ -0,0 +1,17 @@
1
+ module AST
2
+ # AST is a library for manipulating abstract syntax trees.
3
+ #
4
+ # It embraces immutability; each AST node is inherently frozen at
5
+ # creation, and updating a child node requires recreating that node
6
+ # and its every parent, recursively.
7
+ # This is a design choice. It does create some pressure on
8
+ # garbage collector, but completely eliminates all concurrency
9
+ # and aliasing problems.
10
+ #
11
+ # See also {Node}, {Processor} and {Sexp} for additional
12
+ # recommendations and design patterns.
13
+
14
+ require_relative "ast/node"
15
+ require_relative "ast/processor"
16
+ require_relative "ast/sexp"
17
+ end
data/lib/ast/node.rb ADDED
@@ -0,0 +1,217 @@
1
+ module AST
2
+ # Node is an immutable class, instances of which represent abstract
3
+ # syntax tree nodes. It combines semantic information (i.e. anything
4
+ # that affects the algorithmic properties of a program) with
5
+ # meta-information (line numbers or compiler intermediates).
6
+ #
7
+ # Notes on inheritance
8
+ # ====================
9
+ #
10
+ # The distinction between semantics and metadata is important. Complete
11
+ # semantic information should be contained within just the {#type} and
12
+ # {#children} of a Node instance; in other words, if an AST was to be
13
+ # stripped of all meta-information, it should remain a valid AST which
14
+ # could be successfully processed to yield a result with the same
15
+ # algorithmic properties.
16
+ #
17
+ # Thus, Node should never be inherited in order to define methods which
18
+ # affect or return semantic information, such as getters for `class_name`,
19
+ # `superclass` and `body` in the case of a hypothetical `ClassNode`. The
20
+ # correct solution is to use a generic Node with a {#type} of `:class`
21
+ # and three children. See also {Processor} for tips on working with such
22
+ # ASTs.
23
+ #
24
+ # On the other hand, Node can and should be inherited to define
25
+ # application-specific metadata (see also {#initialize}) or customize the
26
+ # printing format. It is expected that an application would have one or two
27
+ # such classes and use them across the entire codebase.
28
+ #
29
+ # The rationale for this pattern is extensibility and maintainability.
30
+ # Unlike static ones, dynamic languages do not require the presence of a
31
+ # predefined, rigid structure, nor does it improve dispatch efficiency,
32
+ # and while such a structure can certainly be defined, it does not add
33
+ # any value but incurs a maintaining cost.
34
+ # For example, extending the AST even with a transformation-local
35
+ # temporary node type requires making globally visible changes to
36
+ # the codebase.
37
+ #
38
+ class Node
39
+ # Returns the type of this node.
40
+ # @return [Symbol]
41
+ attr_reader :type
42
+
43
+ # Returns the children of this node.
44
+ # The returned value is frozen.
45
+ # @return [Array]
46
+ attr_reader :children
47
+
48
+ # Constructs a new instance of Node.
49
+ #
50
+ # The arguments `type` and `children` are converted with `to_sym` and
51
+ # `to_a` respectively. Additionally, the result of converting `children`
52
+ # is frozen. While mutating the arguments is generally considered harmful,
53
+ # the most common case is to pass an array literal to the constructor. If
54
+ # your code does not expect the argument to be frozen, use `#dup`.
55
+ #
56
+ # The `properties` hash is passed to {#assign_properties}.
57
+ def initialize(type, children=[], properties={})
58
+ @type, @children = type.to_sym, children.to_a.freeze
59
+
60
+ assign_properties(properties)
61
+
62
+ freeze
63
+ end
64
+
65
+ # By default, each entry in the `properties` hash is assigned to
66
+ # an instance variable in this instance of Node. A subclass should define
67
+ # attribute readers for such variables. The values passed in the hash
68
+ # are not frozen or whitelisted; such behavior can also be implemented\
69
+ # by subclassing Node and overriding this method.
70
+ #
71
+ # @return [nil]
72
+ def assign_properties(properties)
73
+ properties.each do |name, value|
74
+ instance_variable_set :"@#{name}", value
75
+ end
76
+
77
+ nil
78
+ end
79
+ protected :assign_properties
80
+
81
+ alias :original_dup :dup
82
+ private :original_dup
83
+
84
+ # Nodes are already frozen, so there is no harm in returning the
85
+ # current node as opposed to initializing from scratch and freezing
86
+ # another one.
87
+ #
88
+ # @return self
89
+ def dup
90
+ self
91
+ end
92
+
93
+ # Returns a new instance of Node where non-nil arguments replace the
94
+ # corresponding fields of `self`.
95
+ #
96
+ # For example, `Node.new(:foo, [ 1, 2 ]).updated(:bar)` would yield
97
+ # `(bar 1 2)`, and `Node.new(:foo, [ 1, 2 ]).updated(nil, [])` would
98
+ # yield `(foo)`.
99
+ #
100
+ # If the resulting node would be identical to `self`, does nothing.
101
+ #
102
+ # @param [Symbol, nil] type
103
+ # @param [Array, nil] children
104
+ # @param [Hash, nil] properties
105
+ # @return [AST::Node]
106
+ def updated(type=nil, children=nil, properties=nil)
107
+ new_type = type || @type
108
+ new_children = children || @children
109
+ new_properties = properties || {}
110
+
111
+ if @type == new_type &&
112
+ @children == new_children &&
113
+ properties.nil?
114
+ self
115
+ else
116
+ original_dup.send :initialize, new_type, new_children, new_properties
117
+ end
118
+ end
119
+
120
+ # Compares `self` to `other`, possibly converting with `to_ast`. Only
121
+ # `type` and `children` are compared; metadata is deliberately ignored.
122
+ #
123
+ # @return [Boolean]
124
+ def ==(other)
125
+ if equal?(other)
126
+ true
127
+ elsif other.respond_to? :to_ast
128
+ other = other.to_ast
129
+ other.type == self.type &&
130
+ other.children == self.children
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+ # Concatenates `array` with `children` and returns the resulting node.
137
+ #
138
+ # @return [AST::Node]
139
+ def concat(array)
140
+ updated(nil, @children + array.to_a)
141
+ end
142
+
143
+ alias + concat
144
+
145
+ # Appends `element` to `children` and returns the resulting node.
146
+ #
147
+ # @return [AST::Node]
148
+ def append(element)
149
+ updated(nil, @children + [element])
150
+ end
151
+
152
+ alias << append
153
+
154
+ # Converts `self` to a concise s-expression, omitting any children.
155
+ #
156
+ # @return [String]
157
+ def to_s
158
+ "(#{fancy_type} ...)"
159
+ end
160
+
161
+ # Returns {#children}. This is very useful in order to decompose nodes
162
+ # concisely. For example:
163
+ #
164
+ # node = s(:gasgn, :$foo, s(:integer, 1))
165
+ # s
166
+ # var_name, value = *node
167
+ # p var_name # => :$foo
168
+ # p value # => (integer 1)
169
+ #
170
+ # @return [Array]
171
+ def to_a
172
+ children
173
+ end
174
+
175
+ # Converts `self` to a pretty-printed s-expression.
176
+ #
177
+ # @param [Integer] indent Base indentation level.
178
+ # @return [String]
179
+ def to_sexp(indent=0)
180
+ indented = " " * indent
181
+ sexp = "#{indented}(#{fancy_type}"
182
+
183
+ first_node_child = children.index do |child|
184
+ child.is_a?(Node) || child.is_a?(Array)
185
+ end || children.count
186
+
187
+ children.each_with_index do |child, idx|
188
+ if child.is_a?(Node) && idx >= first_node_child
189
+ sexp << "\n#{child.to_sexp(indent + 1)}"
190
+ else
191
+ sexp << " #{child.inspect}"
192
+ end
193
+ end
194
+
195
+ sexp << ")"
196
+
197
+ sexp
198
+ end
199
+ alias :inspect :to_sexp
200
+
201
+ # @return [AST::Node] self
202
+ def to_ast
203
+ self
204
+ end
205
+
206
+ protected
207
+
208
+ # Returns `@type` with all underscores replaced by dashes. This allows
209
+ # to write symbol literals without quotes in Ruby sources and yet have
210
+ # nicely looking s-expressions.
211
+ #
212
+ # @return [String]
213
+ def fancy_type
214
+ @type.to_s.gsub('_', '-')
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,264 @@
1
+ module AST
2
+ # Processor is a class which helps transforming one AST into another.
3
+ # In a nutshell, the {#process} method accepts a {Node} and dispatches
4
+ # it to a handler corresponding to its type, and returns a (possibly)
5
+ # updated variant of the node.
6
+ #
7
+ # Processor has a set of associated design patterns. They are best
8
+ # explained with a concrete example. Let's define a simple arithmetic
9
+ # language and an AST format for it:
10
+ #
11
+ # Terminals (AST nodes which do not have other AST nodes inside):
12
+ #
13
+ # * `(integer <int-literal>)`,
14
+ #
15
+ # Nonterminals (AST nodes with other nodes as children):
16
+ #
17
+ # * `(add <node> <node>)`,
18
+ # * `(multiply <node> <node>)`,
19
+ # * `(divide <node> <node>)`,
20
+ # * `(negate <node>)`,
21
+ # * `(store <node> <string-literal>)`: stores value of `<node>` into a variable named `<string-literal>`,
22
+ # * `(load <string-literal>)`: loads value of a variable named `<string-literal>`,
23
+ # * `(each <node> ...): computes each of the `<node>`s and prints the result.
24
+ #
25
+ # All AST nodes have the same Ruby class, and therefore they don't
26
+ # know how to traverse themselves. (A solution which dynamically checks the
27
+ # type of children is possible, but is slow and error-prone.) So, a subclass
28
+ # of Processor which knows how to traverse the entire tree should be defined.
29
+ # Such subclass has a handler for each nonterminal node which recursively
30
+ # processes children nodes:
31
+ #
32
+ # require 'ast'
33
+ #
34
+ # class ArithmeticsProcessor < AST::Processor
35
+ # # This method traverses any binary operators such as (add) or (multiply).
36
+ # def process_binary_op(node)
37
+ # # Children aren't decomposed automatically; it is suggested to use Ruby
38
+ # # multiple assignment expansion, as it is very convenient here.
39
+ # left_expr, right_expr = *node
40
+ #
41
+ # # AST::Node#updated won't change node type if nil is passed as a first
42
+ # # argument, which allows to reuse the same handler for multiple node types
43
+ # # using `alias' (below).
44
+ # node.updated(nil, [
45
+ # process(left_expr),
46
+ # process(right_expr)
47
+ # ])
48
+ # end
49
+ # alias on_add process_binary_op
50
+ # alias on_multiply process_binary_op
51
+ # alias on_divide process_binary_op
52
+ #
53
+ # def on_negate(node)
54
+ # # It is also possible to use #process_all for more compact code
55
+ # # if every child is a Node.
56
+ # node.updated(nil, process_all(node))
57
+ # end
58
+ #
59
+ # def on_store(node)
60
+ # expr, variable_name = *node
61
+ #
62
+ # # Note that variable_name is not a Node and thus isn't passed to #process.
63
+ # node.updated(nil, [
64
+ # process(expr),
65
+ # variable_name
66
+ # ])
67
+ # end
68
+ #
69
+ # # (load) is effectively a terminal node, and so it does not need
70
+ # # an explicit handler, as the following is the default behavior.
71
+ # def on_load(node)
72
+ # nil
73
+ # end
74
+ #
75
+ # def on_each(node)
76
+ # node.updated(nil, process_all(node))
77
+ # end
78
+ # end
79
+ #
80
+ # Let's test our ArithmeticsProcessor:
81
+ #
82
+ # include AST::Sexp
83
+ # expr = s(:add, s(:integer, 2), s(:integer, 2))
84
+ #
85
+ # p ArithmeticsProcessor.new.process(expr) == expr # => true
86
+ #
87
+ # As expected, it does not change anything at all. This isn't actually
88
+ # very useful, so let's now define a Calculator, which will compute the
89
+ # expression values:
90
+ #
91
+ # # This Processor folds nonterminal nodes and returns an (integer)
92
+ # # terminal node.
93
+ # class ArithmeticsCalculator < ArithmeticsProcessor
94
+ # def compute_op(node)
95
+ # # First, node children are processed and then unpacked to local
96
+ # # variables.
97
+ # nodes = process_all(node)
98
+ #
99
+ # if nodes.all? { |node| node.type == :integer }
100
+ # # If each of those nodes represents a literal, we can fold this
101
+ # # node!
102
+ # values = nodes.map { |node| node.children.first }
103
+ # AST::Node.new(:integer, [
104
+ # yield(values)
105
+ # ])
106
+ # else
107
+ # # Otherwise, we can just leave the current node in the tree and
108
+ # # only update it with processed children nodes, which can be
109
+ # # partially folded.
110
+ # node.updated(nil, nodes)
111
+ # end
112
+ # end
113
+ #
114
+ # def on_add(node)
115
+ # compute_op(node) { |left, right| left + right }
116
+ # end
117
+ #
118
+ # def on_multiply(node)
119
+ # compute_op(node) { |left, right| left * right }
120
+ # end
121
+ # end
122
+ #
123
+ # Let's check:
124
+ #
125
+ # p ArithmeticsCalculator.new.process(expr) # => (integer 4)
126
+ #
127
+ # Excellent, the calculator works! Now, a careful reader could notice that
128
+ # the ArithmeticsCalculator does not know how to divide numbers. What if we
129
+ # pass an expression with division to it?
130
+ #
131
+ # expr_with_division = \
132
+ # s(:add,
133
+ # s(:integer, 1),
134
+ # s(:divide,
135
+ # s(:add, s(:integer, 8), s(:integer, 4)),
136
+ # s(:integer, 3))) # 1 + (8 + 4) / 3
137
+ #
138
+ # folded_expr_with_division = ArithmeticsCalculator.new.process(expr_with_division)
139
+ # p folded_expr_with_division
140
+ # # => (add
141
+ # # (integer 1)
142
+ # # (divide
143
+ # # (integer 12)
144
+ # # (integer 3)))
145
+ #
146
+ # As you can see, the expression was folded _partially_: the inner `(add)` node which
147
+ # could be computed was folded to `(integer 12)`, the `(divide)` node is left as-is
148
+ # because there is no computing handler for it, and the root `(add)` node was also left
149
+ # as it is because some of its children were not literals.
150
+ #
151
+ # Note that this partial folding is only possible because the _data_ format, i.e.
152
+ # the format in which the computed values of the nodes are represented, is the same as
153
+ # the AST itself.
154
+ #
155
+ # Let's extend our ArithmeticsCalculator class further.
156
+ #
157
+ # class ArithmeticsCalculator
158
+ # def on_divide(node)
159
+ # compute_op(node) { |left, right| left / right }
160
+ # end
161
+ #
162
+ # def on_negate(node)
163
+ # # Note how #compute_op works regardless of the operator arity.
164
+ # compute_op(node) { |value| -value }
165
+ # end
166
+ # end
167
+ #
168
+ # Now, let's apply our renewed ArithmeticsCalculator to a partial result of previous
169
+ # evaluation:
170
+ #
171
+ # p ArithmeticsCalculator.new.process(expr_with_division) # => (integer 5)
172
+ #
173
+ # Five! Excellent. This is also pretty much how CRuby 1.8 executed its programs.
174
+ #
175
+ # Now, let's do some automated bug searching. Division by zero is an error, right?
176
+ # So if we could detect that someone has divided by zero before the program is even
177
+ # run, that could save some debugging time.
178
+ #
179
+ # class DivisionByZeroVerifier < ArithmeticsProcessor
180
+ # class VerificationFailure < Exception; end
181
+ #
182
+ # def on_divide(node)
183
+ # # You need to process the children to handle nested divisions
184
+ # # such as:
185
+ # # (divide
186
+ # # (integer 1)
187
+ # # (divide (integer 1) (integer 0))
188
+ # left, right = process_all(node)
189
+ #
190
+ # if right.type == :integer &&
191
+ # right.children.first == 0
192
+ # raise VerificationFailure, "Ouch! This code divides by zero."
193
+ # end
194
+ # end
195
+ #
196
+ # def divides_by_zero?(ast)
197
+ # process(ast)
198
+ # false
199
+ # rescue VerificationFailure
200
+ # true
201
+ # end
202
+ # end
203
+ #
204
+ # nice_expr = \
205
+ # s(:divide,
206
+ # s(:add, s(:integer, 10), s(:integer, 2)),
207
+ # s(:integer, 4))
208
+ #
209
+ # p DivisionByZeroVerifier.new.divides_by_zero?(nice_expr)
210
+ # # => false. Good.
211
+ #
212
+ # bad_expr = \
213
+ # s(:add, s(:integer, 10),
214
+ # s(:divide, s(:integer, 1), s(:integer, 0)))
215
+ #
216
+ # p DivisionByZeroVerifier.new.divides_by_zero?(bad_expr)
217
+ # # => true. WHOOPS. DO NOT RUN THIS.
218
+ #
219
+ # Of course, this won't detect more complex cases... unless you use some partial
220
+ # evaluation before! The possibilites are endless. Have fun.
221
+ class Processor
222
+ # Dispatches `node`. If a node has type `:foo`, then a handler named
223
+ # `on_foo` is invoked with one argument, the `node`; if there isn't
224
+ # such a handler, {#handler_missing} is invoked with the same argument.
225
+ #
226
+ # If the handler returns `nil`, `node` is returned; otherwise, the return
227
+ # value of the handler is passed along.
228
+ #
229
+ # @param [AST::Node, nil] node
230
+ # @return [AST::Node]
231
+ def process(node)
232
+ node = node.to_ast
233
+
234
+ # Invoke a specific handler
235
+ on_handler = :"on_#{node.type}"
236
+ if respond_to? on_handler
237
+ new_node = send on_handler, node
238
+ else
239
+ new_node = handler_missing(node)
240
+ end
241
+
242
+ node = new_node if new_node
243
+
244
+ node
245
+ end
246
+
247
+ # {#process}es each node from `nodes` and returns an array of results.
248
+ #
249
+ # @param [Array<AST::Node>] nodes
250
+ # @return [Array<AST::Node>]
251
+ def process_all(nodes)
252
+ nodes.to_a.map do |node|
253
+ process node
254
+ end
255
+ end
256
+
257
+ # Default handler. Does nothing.
258
+ #
259
+ # @param [AST::Node] node
260
+ # @return [AST::Node, nil]
261
+ def handler_missing(node)
262
+ end
263
+ end
264
+ end
data/lib/ast/sexp.rb ADDED
@@ -0,0 +1,30 @@
1
+ module AST
2
+ # This simple module is very useful in the cases where one needs
3
+ # to define deeply nested ASTs from Ruby code, for example, in
4
+ # tests. It should be used like this:
5
+ #
6
+ # describe YourLanguage::AST do
7
+ # include Sexp
8
+ #
9
+ # it "should correctly parse expressions" do
10
+ # YourLanguage.parse("1 + 2 * 3").should ==
11
+ # s(:add,
12
+ # s(:integer, 1),
13
+ # s(:multiply,
14
+ # s(:integer, 2),
15
+ # s(:integer, 3)))
16
+ # end
17
+ # end
18
+ #
19
+ # This way the amount of boilerplate code is greatly reduced.
20
+ module Sexp
21
+ # Creates a {Node} with type `type` and children `children`.
22
+ # Note that the resulting node is of the type AST::Node and not a
23
+ # subclass.
24
+ # This would not pose a problem with comparisons, as {Node#==}
25
+ # ignores metadata.
26
+ def s(type, *children)
27
+ Node.new(type, children)
28
+ end
29
+ end
30
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'bacon'
2
+ require 'bacon/colored_output'
3
+
4
+ require 'simplecov'
5
+ SimpleCov.start
6
+
7
+ require 'ast'
data/test/test_ast.rb ADDED
@@ -0,0 +1,215 @@
1
+ require_relative 'helper'
2
+
3
+ describe AST::Node do
4
+ extend AST::Sexp
5
+
6
+ class MetaNode < AST::Node
7
+ attr_reader :meta
8
+ end
9
+
10
+ before do
11
+ @node = AST::Node.new(:node, [ 0, 1 ])
12
+ @metanode = MetaNode.new(:node, [ 0, 1 ], meta: 'value')
13
+ end
14
+
15
+ it 'should have accessors for type and children' do
16
+ @node.type.should.equal :node
17
+ @node.children.should.equal [0, 1]
18
+ end
19
+
20
+ it 'should set metadata' do
21
+ @metanode.meta.should.equal 'value'
22
+ end
23
+
24
+ it 'should be frozen' do
25
+ @node.frozen?.should.be.true
26
+ @node.children.frozen?.should.be.true
27
+ end
28
+
29
+ it 'should return self when duping' do
30
+ @node.dup.should.equal? @node
31
+ end
32
+
33
+ it 'should return an updated node, but only if needed' do
34
+ @node.updated().should.be.identical_to @node
35
+ @node.updated(:node).should.be.identical_to @node
36
+ @node.updated(nil, [0, 1]).should.be.identical_to @node
37
+
38
+ updated = @node.updated(:other_node)
39
+ updated.should.not.be.identical_to @node
40
+ updated.type.should.equal :other_node
41
+ updated.children.should.equal @node.children
42
+
43
+ updated.frozen?.should.be.true
44
+
45
+ updated = @node.updated(nil, [1, 1])
46
+ updated.should.not.be.identical_to @node
47
+ updated.type.should.equal @node.type
48
+ updated.children.should.equal [1, 1]
49
+
50
+ updated = @metanode.updated(nil, nil, meta: 'other_value')
51
+ updated.meta.should.equal 'other_value'
52
+ end
53
+
54
+ it 'should use fancy type in to_s' do
55
+ node = AST::Node.new(:ast_node)
56
+ node.to_s.should.equal '(ast-node ...)'
57
+ end
58
+
59
+ it 'should format to_sexp correctly' do
60
+ AST::Node.new(:a, [ :sym, [ 1, 2 ] ]).to_sexp.should.equal '(a :sym [1, 2])'
61
+ AST::Node.new(:a, [ :sym, @node ]).to_sexp.should.equal "(a :sym\n (node 0 1))"
62
+ AST::Node.new(:a, [ :sym,
63
+ AST::Node.new(:b, [ @node, @node ])
64
+ ]).to_sexp.should.equal "(a :sym\n (b\n (node 0 1)\n (node 0 1)))"
65
+ end
66
+
67
+ it 'should return self in to_ast' do
68
+ @node.to_ast.should.be.identical_to @node
69
+ end
70
+
71
+ it 'should only use type and children in comparisons' do
72
+ @node.should.equal @node
73
+ @node.should.equal @metanode
74
+ @node.should.not.equal :foo
75
+
76
+ mock_node = Object.new.tap do |obj|
77
+ def obj.to_ast
78
+ self
79
+ end
80
+
81
+ def obj.type
82
+ :node
83
+ end
84
+
85
+ def obj.children
86
+ [ 0, 1 ]
87
+ end
88
+ end
89
+ @node.should.equal mock_node
90
+ end
91
+
92
+ it 'should allow to decompose nodes with a, b = *node' do
93
+ node = s(:gasgn, :$foo, s(:integer, 1))
94
+
95
+ var_name, value = *node
96
+ var_name.should.equal :$foo
97
+ value.should.equal s(:integer, 1)
98
+ end
99
+
100
+ it 'should concatenate with arrays' do
101
+ node = s(:gasgn, :$foo)
102
+ (node + [s(:integer, 1)]).
103
+ should.equal s(:gasgn, :$foo, s(:integer, 1))
104
+ end
105
+
106
+ it 'should append elements' do
107
+ node = s(:array)
108
+ (node << s(:integer, 1) << s(:string, "foo")).
109
+ should.equal s(:array, s(:integer, 1), s(:string, "foo"))
110
+ end
111
+ end
112
+
113
+ describe AST::Processor do
114
+ extend AST::Sexp
115
+
116
+ def have_sexp(text)
117
+ text = text.lines.map { |line| line.sub /^ +\|(.+)/, '\1' }.join.rstrip
118
+ lambda { |ast| ast.to_sexp == text }
119
+ end
120
+
121
+ class MockProcessor < AST::Processor
122
+ attr_reader :counts
123
+
124
+ def initialize
125
+ @counts = Hash.new(0)
126
+ end
127
+
128
+ def on_root(node)
129
+ count_node(node)
130
+ node.updated(nil, process_all(node.children))
131
+ end
132
+ alias on_body on_root
133
+
134
+ def on_def(node)
135
+ count_node(node)
136
+ name, arglist, body = node.children
137
+ node.updated(:def, [ name, process(arglist), process(body) ])
138
+ end
139
+
140
+ def handler_missing(node)
141
+ count_node(node)
142
+ end
143
+
144
+ def count_node(node)
145
+ @counts[node.type] += 1; nil
146
+ end
147
+ end
148
+
149
+ before do
150
+ @ast = AST::Node.new(:root, [
151
+ AST::Node.new(:def, [ :func,
152
+ AST::Node.new(:arglist, [ :foo, :bar ]),
153
+ AST::Node.new(:body, [
154
+ AST::Node.new(:invoke, [ :puts, "Hello world" ])
155
+ ])
156
+ ]),
157
+ AST::Node.new(:invoke, [ :func ])
158
+ ])
159
+
160
+ @processor = MockProcessor.new
161
+ end
162
+
163
+ it 'should visit every node' do
164
+ @processor.process(@ast).should.equal @ast
165
+ @processor.counts.should.equal({
166
+ root: 1,
167
+ def: 1,
168
+ arglist: 1,
169
+ body: 1,
170
+ invoke: 2
171
+ })
172
+ end
173
+
174
+ it 'should be able to replace inner nodes' do
175
+ def @processor.on_arglist(node)
176
+ node.updated(:new_fancy_arglist)
177
+ end
178
+
179
+ @processor.process(@ast).should have_sexp(<<-SEXP)
180
+ |(root
181
+ | (def :func
182
+ | (new-fancy-arglist :foo :bar)
183
+ | (body
184
+ | (invoke :puts "Hello world")))
185
+ | (invoke :func))
186
+ SEXP
187
+ end
188
+
189
+ it 'should build sexps' do
190
+ s(:add,
191
+ s(:integer, 1),
192
+ s(:multiply,
193
+ s(:integer, 2),
194
+ s(:integer, 3))).should have_sexp(<<-SEXP)
195
+ |(add
196
+ | (integer 1)
197
+ | (multiply
198
+ | (integer 2)
199
+ | (integer 3)))
200
+ SEXP
201
+ end
202
+
203
+ it 'should refuse to process non-nodes' do
204
+ -> { @processor.process(nil) }.should.raise NoMethodError, %r|to_ast|
205
+ -> { @processor.process([]) }.should.raise NoMethodError, %r|to_ast|
206
+ end
207
+
208
+ it 'should allow to visit nodes with process_all(node)' do
209
+ @processor.process_all s(:foo, s(:bar), s(:integer, 1))
210
+ @processor.counts.should.equal({
211
+ bar: 1,
212
+ integer: 1,
213
+ })
214
+ end
215
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ast
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Zotov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '10.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '10.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bacon
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bacon-colored_output
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: kramdown
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: A library for working with Abstract Syntax Trees.
98
+ email:
99
+ - whitequark@whitequark.org
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - .travis.yml
106
+ - .yardopts
107
+ - Gemfile
108
+ - LICENSE.MIT
109
+ - Rakefile
110
+ - ast.gemspec
111
+ - lib/ast.rb
112
+ - lib/ast/node.rb
113
+ - lib/ast/processor.rb
114
+ - lib/ast/sexp.rb
115
+ - test/helper.rb
116
+ - test/test_ast.rb
117
+ homepage: http://github.com/whitequark/ast
118
+ licenses: []
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.0.0
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A library for working with Abstract Syntax Trees.
140
+ test_files: []
141
+ has_rdoc: