ast 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: