ast 2.0.0 → 2.4.1
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 +5 -5
- data/README.YARD.md +2 -2
- data/lib/ast.rb +2 -2
- data/lib/ast/node.rb +59 -31
- data/lib/ast/processor.rb +7 -261
- data/lib/ast/processor/mixin.rb +288 -0
- metadata +9 -48
- data/.gitignore +0 -8
- data/.travis.yml +0 -9
- data/.yardopts +0 -1
- data/CHANGELOG.md +0 -9
- data/Gemfile +0 -4
- data/README.md +0 -23
- data/Rakefile +0 -19
- data/ast.gemspec +0 -28
- data/test/helper.rb +0 -17
- data/test/test_ast.rb +0 -243
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e1ea61250ad8d285c9857cb70e2529f827b561b673b01dc02e92cc7bcea81094
|
4
|
+
data.tar.gz: 88c407ffbff0293306bda599d2fe5146bb3504a389f2a69c2c43a8b5c2244446
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dad932faea02614394a61c4075efb11a2932c6231f9cd11d59d8e7345d8f74d4e7602881fce0e77094017c137159466b92f13ebecaee17e4ee611a0091a3f17a
|
7
|
+
data.tar.gz: 59ea82e96f376f3dc4805a66baa37e577dec80bcad6485ba3a4bc440b27d0ba31ca5d789ca74e7594758f7cfbd279bc75195da8073af17b817e91f714192e874
|
data/README.YARD.md
CHANGED
@@ -8,5 +8,5 @@ This is a design choice. It does create some pressure on
|
|
8
8
|
garbage collector, but completely eliminates all concurrency
|
9
9
|
and aliasing problems.
|
10
10
|
|
11
|
-
See also {AST::Node}, {AST::Processor} and {AST::Sexp} for
|
12
|
-
recommendations and design patterns.
|
11
|
+
See also {AST::Node}, {AST::Processor::Mixin} and {AST::Sexp} for
|
12
|
+
additional recommendations and design patterns.
|
data/lib/ast.rb
CHANGED
@@ -7,8 +7,8 @@
|
|
7
7
|
# garbage collector, but completely eliminates all concurrency
|
8
8
|
# and aliasing problems.
|
9
9
|
#
|
10
|
-
# See also {AST::Node}, {AST::Processor} and {AST::Sexp} for
|
11
|
-
# recommendations and design patterns.
|
10
|
+
# See also {AST::Node}, {AST::Processor::Mixin} and {AST::Sexp} for
|
11
|
+
# additional recommendations and design patterns.
|
12
12
|
#
|
13
13
|
module AST
|
14
14
|
require 'ast/node'
|
data/lib/ast/node.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AST
|
2
4
|
# Node is an immutable class, instances of which represent abstract
|
3
5
|
# syntax tree nodes. It combines semantic information (i.e. anything
|
@@ -42,8 +44,17 @@ module AST
|
|
42
44
|
|
43
45
|
# Returns the children of this node.
|
44
46
|
# The returned value is frozen.
|
47
|
+
# The to_a alias is useful for decomposing nodes concisely.
|
48
|
+
# For example:
|
49
|
+
#
|
50
|
+
# node = s(:gasgn, :$foo, s(:integer, 1))
|
51
|
+
# var_name, value = *node
|
52
|
+
# p var_name # => :$foo
|
53
|
+
# p value # => (integer 1)
|
54
|
+
#
|
45
55
|
# @return [Array]
|
46
56
|
attr_reader :children
|
57
|
+
alias to_a children
|
47
58
|
|
48
59
|
# Returns the precomputed hash value for this node
|
49
60
|
# @return [Fixnum]
|
@@ -80,7 +91,7 @@ module AST
|
|
80
91
|
# By default, each entry in the `properties` hash is assigned to
|
81
92
|
# an instance variable in this instance of Node. A subclass should define
|
82
93
|
# attribute readers for such variables. The values passed in the hash
|
83
|
-
# are not frozen or whitelisted; such behavior can also be implemented
|
94
|
+
# are not frozen or whitelisted; such behavior can also be implemented
|
84
95
|
# by subclassing Node and overriding this method.
|
85
96
|
#
|
86
97
|
# @return [nil]
|
@@ -104,6 +115,7 @@ module AST
|
|
104
115
|
def dup
|
105
116
|
self
|
106
117
|
end
|
118
|
+
alias :clone :dup
|
107
119
|
|
108
120
|
# Returns a new instance of Node where non-nil arguments replace the
|
109
121
|
# corresponding fields of `self`.
|
@@ -128,7 +140,9 @@ module AST
|
|
128
140
|
properties.nil?
|
129
141
|
self
|
130
142
|
else
|
131
|
-
|
143
|
+
copy = original_dup
|
144
|
+
copy.send :initialize, new_type, new_children, new_properties
|
145
|
+
copy
|
132
146
|
end
|
133
147
|
end
|
134
148
|
|
@@ -166,57 +180,71 @@ module AST
|
|
166
180
|
|
167
181
|
alias << append
|
168
182
|
|
169
|
-
# Converts `self` to a
|
183
|
+
# Converts `self` to a pretty-printed s-expression.
|
170
184
|
#
|
185
|
+
# @param [Integer] indent Base indentation level.
|
171
186
|
# @return [String]
|
172
|
-
def
|
173
|
-
"
|
174
|
-
|
187
|
+
def to_sexp(indent=0)
|
188
|
+
indented = " " * indent
|
189
|
+
sexp = "#{indented}(#{fancy_type}"
|
175
190
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
children
|
191
|
+
children.each do |child|
|
192
|
+
if child.is_a?(Node)
|
193
|
+
sexp += "\n#{child.to_sexp(indent + 1)}"
|
194
|
+
else
|
195
|
+
sexp += " #{child.inspect}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
sexp += ")"
|
200
|
+
|
201
|
+
sexp
|
188
202
|
end
|
189
203
|
|
190
|
-
|
204
|
+
alias to_s to_sexp
|
205
|
+
|
206
|
+
# Converts `self` to a s-expression ruby string.
|
207
|
+
# The code return will recreate the node, using the sexp module s()
|
191
208
|
#
|
192
209
|
# @param [Integer] indent Base indentation level.
|
193
210
|
# @return [String]
|
194
|
-
def
|
211
|
+
def inspect(indent=0)
|
195
212
|
indented = " " * indent
|
196
|
-
sexp = "#{indented}(
|
197
|
-
|
198
|
-
first_node_child = children.index do |child|
|
199
|
-
child.is_a?(Node) || child.is_a?(Array)
|
200
|
-
end || children.count
|
213
|
+
sexp = "#{indented}s(:#{@type}"
|
201
214
|
|
202
|
-
children.
|
203
|
-
if child.is_a?(Node)
|
204
|
-
sexp
|
215
|
+
children.each do |child|
|
216
|
+
if child.is_a?(Node)
|
217
|
+
sexp += ",\n#{child.inspect(indent + 1)}"
|
205
218
|
else
|
206
|
-
sexp
|
219
|
+
sexp += ", #{child.inspect}"
|
207
220
|
end
|
208
221
|
end
|
209
222
|
|
210
|
-
sexp
|
223
|
+
sexp += ")"
|
211
224
|
|
212
225
|
sexp
|
213
226
|
end
|
214
|
-
alias :inspect :to_sexp
|
215
227
|
|
216
228
|
# @return [AST::Node] self
|
217
229
|
def to_ast
|
218
230
|
self
|
219
231
|
end
|
232
|
+
|
233
|
+
# Converts `self` to an Array where the first element is the type as a Symbol,
|
234
|
+
# and subsequent elements are the same representation of its children.
|
235
|
+
#
|
236
|
+
# @return [Array<Symbol, [...Array]>]
|
237
|
+
def to_sexp_array
|
238
|
+
children_sexp_arrs = children.map do |child|
|
239
|
+
if child.is_a?(Node)
|
240
|
+
child.to_sexp_array
|
241
|
+
else
|
242
|
+
child
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
[type, *children_sexp_arrs]
|
247
|
+
end
|
220
248
|
|
221
249
|
protected
|
222
250
|
|
data/lib/ast/processor.rb
CHANGED
@@ -1,266 +1,12 @@
|
|
1
1
|
module AST
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
2
|
+
# This class includes {AST::Processor::Mixin}; however, it is
|
3
|
+
# deprecated, since the module defines all of the behaviors that
|
4
|
+
# the processor includes. Any new libraries should use
|
5
|
+
# {AST::Processor::Mixin} instead of subclassing this.
|
6
6
|
#
|
7
|
-
#
|
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.
|
7
|
+
# @deprecated Use {AST::Processor::Mixin} instead.
|
221
8
|
class Processor
|
222
|
-
|
223
|
-
|
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, nil]
|
231
|
-
def process(node)
|
232
|
-
return if node.nil?
|
233
|
-
|
234
|
-
node = node.to_ast
|
235
|
-
|
236
|
-
# Invoke a specific handler
|
237
|
-
on_handler = :"on_#{node.type}"
|
238
|
-
if respond_to? on_handler
|
239
|
-
new_node = send on_handler, node
|
240
|
-
else
|
241
|
-
new_node = handler_missing(node)
|
242
|
-
end
|
243
|
-
|
244
|
-
node = new_node if new_node
|
245
|
-
|
246
|
-
node
|
247
|
-
end
|
248
|
-
|
249
|
-
# {#process}es each node from `nodes` and returns an array of results.
|
250
|
-
#
|
251
|
-
# @param [Array<AST::Node>] nodes
|
252
|
-
# @return [Array<AST::Node>]
|
253
|
-
def process_all(nodes)
|
254
|
-
nodes.to_a.map do |node|
|
255
|
-
process node
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
# Default handler. Does nothing.
|
260
|
-
#
|
261
|
-
# @param [AST::Node] node
|
262
|
-
# @return [AST::Node, nil]
|
263
|
-
def handler_missing(node)
|
264
|
-
end
|
9
|
+
require 'ast/processor/mixin'
|
10
|
+
include Mixin
|
265
11
|
end
|
266
12
|
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
module AST
|
2
|
+
class Processor
|
3
|
+
# The processor module is a module which helps transforming one
|
4
|
+
# AST into another. In a nutshell, the {#process} method accepts
|
5
|
+
# a {Node} and dispatches it to a handler corresponding to its
|
6
|
+
# type, and returns a (possibly) updated variant of the node.
|
7
|
+
#
|
8
|
+
# The processor module has a set of associated design patterns.
|
9
|
+
# They are best explained with a concrete example. Let's define a
|
10
|
+
# simple arithmetic language and an AST format for it:
|
11
|
+
#
|
12
|
+
# Terminals (AST nodes which do not have other AST nodes inside):
|
13
|
+
#
|
14
|
+
# * `(integer <int-literal>)`,
|
15
|
+
#
|
16
|
+
# Nonterminals (AST nodes with other nodes as children):
|
17
|
+
#
|
18
|
+
# * `(add <node> <node>)`,
|
19
|
+
# * `(multiply <node> <node>)`,
|
20
|
+
# * `(divide <node> <node>)`,
|
21
|
+
# * `(negate <node>)`,
|
22
|
+
# * `(store <node> <string-literal>)`: stores value of `<node>`
|
23
|
+
# into a variable named `<string-literal>`,
|
24
|
+
# * `(load <string-literal>)`: loads value of a variable named
|
25
|
+
# `<string-literal>`,
|
26
|
+
# * `(each <node> ...)`: computes each of the `<node>`s and
|
27
|
+
# prints the result.
|
28
|
+
#
|
29
|
+
# All AST nodes have the same Ruby class, and therefore they don't
|
30
|
+
# know how to traverse themselves. (A solution which dynamically
|
31
|
+
# checks the type of children is possible, but is slow and
|
32
|
+
# error-prone.) So, a class including the module which knows how
|
33
|
+
# to traverse the entire tree should be defined. Such classes
|
34
|
+
# have a handler for each nonterminal node which recursively
|
35
|
+
# processes children nodes:
|
36
|
+
#
|
37
|
+
# require 'ast'
|
38
|
+
#
|
39
|
+
# class ArithmeticsProcessor
|
40
|
+
# include AST::Processor::Mixin
|
41
|
+
# # This method traverses any binary operators such as (add)
|
42
|
+
# # or (multiply).
|
43
|
+
# def process_binary_op(node)
|
44
|
+
# # Children aren't decomposed automatically; it is
|
45
|
+
# # suggested to use Ruby multiple assignment expansion,
|
46
|
+
# # as it is very convenient here.
|
47
|
+
# left_expr, right_expr = *node
|
48
|
+
#
|
49
|
+
# # AST::Node#updated won't change node type if nil is
|
50
|
+
# # passed as a first argument, which allows to reuse the
|
51
|
+
# # same handler for multiple node types using `alias'
|
52
|
+
# # (below).
|
53
|
+
# node.updated(nil, [
|
54
|
+
# process(left_expr),
|
55
|
+
# process(right_expr)
|
56
|
+
# ])
|
57
|
+
# end
|
58
|
+
# alias_method :on_add, :process_binary_op
|
59
|
+
# alias_method :on_multiply, :process_binary_op
|
60
|
+
# alias_method :on_divide, :process_binary_op
|
61
|
+
#
|
62
|
+
# def on_negate(node)
|
63
|
+
# # It is also possible to use #process_all for more
|
64
|
+
# # compact code if every child is a Node.
|
65
|
+
# node.updated(nil, process_all(node))
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# def on_store(node)
|
69
|
+
# expr, variable_name = *node
|
70
|
+
#
|
71
|
+
# # Note that variable_name is not a Node and thus isn't
|
72
|
+
# # passed to #process.
|
73
|
+
# node.updated(nil, [
|
74
|
+
# process(expr),
|
75
|
+
# variable_name
|
76
|
+
# ])
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# # (load) is effectively a terminal node, and so it does
|
80
|
+
# # not need an explicit handler, as the following is the
|
81
|
+
# # default behavior. Essentially, for any nodes that don't
|
82
|
+
# # have a defined handler, the node remains unchanged.
|
83
|
+
# def on_load(node)
|
84
|
+
# nil
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# def on_each(node)
|
88
|
+
# node.updated(nil, process_all(node))
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# Let's test our ArithmeticsProcessor:
|
93
|
+
#
|
94
|
+
# include AST::Sexp
|
95
|
+
# expr = s(:add, s(:integer, 2), s(:integer, 2))
|
96
|
+
#
|
97
|
+
# p ArithmeticsProcessor.new.process(expr) == expr # => true
|
98
|
+
#
|
99
|
+
# As expected, it does not change anything at all. This isn't
|
100
|
+
# actually very useful, so let's now define a Calculator, which
|
101
|
+
# will compute the expression values:
|
102
|
+
#
|
103
|
+
# # This Processor folds nonterminal nodes and returns an
|
104
|
+
# # (integer) terminal node.
|
105
|
+
# class ArithmeticsCalculator < ArithmeticsProcessor
|
106
|
+
# def compute_op(node)
|
107
|
+
# # First, node children are processed and then unpacked
|
108
|
+
# # to local variables.
|
109
|
+
# nodes = process_all(node)
|
110
|
+
#
|
111
|
+
# if nodes.all? { |node| node.type == :integer }
|
112
|
+
# # If each of those nodes represents a literal, we can
|
113
|
+
# # fold this node!
|
114
|
+
# values = nodes.map { |node| node.children.first }
|
115
|
+
# AST::Node.new(:integer, [
|
116
|
+
# yield(values)
|
117
|
+
# ])
|
118
|
+
# else
|
119
|
+
# # Otherwise, we can just leave the current node in the
|
120
|
+
# # tree and only update it with processed children
|
121
|
+
# # nodes, which can be partially folded.
|
122
|
+
# node.updated(nil, nodes)
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# def on_add(node)
|
127
|
+
# compute_op(node) { |left, right| left + right }
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# def on_multiply(node)
|
131
|
+
# compute_op(node) { |left, right| left * right }
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# Let's check:
|
136
|
+
#
|
137
|
+
# p ArithmeticsCalculator.new.process(expr) # => (integer 4)
|
138
|
+
#
|
139
|
+
# Excellent, the calculator works! Now, a careful reader could
|
140
|
+
# notice that the ArithmeticsCalculator does not know how to
|
141
|
+
# divide numbers. What if we pass an expression with division to
|
142
|
+
# it?
|
143
|
+
#
|
144
|
+
# expr_with_division = \
|
145
|
+
# s(:add,
|
146
|
+
# s(:integer, 1),
|
147
|
+
# s(:divide,
|
148
|
+
# s(:add, s(:integer, 8), s(:integer, 4)),
|
149
|
+
# s(:integer, 3))) # 1 + (8 + 4) / 3
|
150
|
+
#
|
151
|
+
# folded_expr_with_division = ArithmeticsCalculator.new.process(expr_with_division)
|
152
|
+
# p folded_expr_with_division
|
153
|
+
# # => (add
|
154
|
+
# # (integer 1)
|
155
|
+
# # (divide
|
156
|
+
# # (integer 12)
|
157
|
+
# # (integer 3)))
|
158
|
+
#
|
159
|
+
# As you can see, the expression was folded _partially_: the inner
|
160
|
+
# `(add)` node which could be computed was folded to
|
161
|
+
# `(integer 12)`, the `(divide)` node is left as-is because there
|
162
|
+
# is no computing handler for it, and the root `(add)` node was
|
163
|
+
# also left as it is because some of its children were not
|
164
|
+
# literals.
|
165
|
+
#
|
166
|
+
# Note that this partial folding is only possible because the
|
167
|
+
# _data_ format, i.e. the format in which the computed values of
|
168
|
+
# the nodes are represented, is the same as the AST itself.
|
169
|
+
#
|
170
|
+
# Let's extend our ArithmeticsCalculator class further.
|
171
|
+
#
|
172
|
+
# class ArithmeticsCalculator
|
173
|
+
# def on_divide(node)
|
174
|
+
# compute_op(node) { |left, right| left / right }
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# def on_negate(node)
|
178
|
+
# # Note how #compute_op works regardless of the operator
|
179
|
+
# # arity.
|
180
|
+
# compute_op(node) { |value| -value }
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# Now, let's apply our renewed ArithmeticsCalculator to a partial
|
185
|
+
# result of previous evaluation:
|
186
|
+
#
|
187
|
+
# p ArithmeticsCalculator.new.process(expr_with_division) # => (integer 5)
|
188
|
+
#
|
189
|
+
# Five! Excellent. This is also pretty much how CRuby 1.8 executed
|
190
|
+
# its programs.
|
191
|
+
#
|
192
|
+
# Now, let's do some automated bug searching. Division by zero is
|
193
|
+
# an error, right? So if we could detect that someone has divided
|
194
|
+
# by zero before the program is even run, that could save some
|
195
|
+
# debugging time.
|
196
|
+
#
|
197
|
+
# class DivisionByZeroVerifier < ArithmeticsProcessor
|
198
|
+
# class VerificationFailure < Exception; end
|
199
|
+
#
|
200
|
+
# def on_divide(node)
|
201
|
+
# # You need to process the children to handle nested divisions
|
202
|
+
# # such as:
|
203
|
+
# # (divide
|
204
|
+
# # (integer 1)
|
205
|
+
# # (divide (integer 1) (integer 0))
|
206
|
+
# left, right = process_all(node)
|
207
|
+
#
|
208
|
+
# if right.type == :integer &&
|
209
|
+
# right.children.first == 0
|
210
|
+
# raise VerificationFailure, "Ouch! This code divides by zero."
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
# def divides_by_zero?(ast)
|
215
|
+
# process(ast)
|
216
|
+
# false
|
217
|
+
# rescue VerificationFailure
|
218
|
+
# true
|
219
|
+
# end
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# nice_expr = \
|
223
|
+
# s(:divide,
|
224
|
+
# s(:add, s(:integer, 10), s(:integer, 2)),
|
225
|
+
# s(:integer, 4))
|
226
|
+
#
|
227
|
+
# p DivisionByZeroVerifier.new.divides_by_zero?(nice_expr)
|
228
|
+
# # => false. Good.
|
229
|
+
#
|
230
|
+
# bad_expr = \
|
231
|
+
# s(:add, s(:integer, 10),
|
232
|
+
# s(:divide, s(:integer, 1), s(:integer, 0)))
|
233
|
+
#
|
234
|
+
# p DivisionByZeroVerifier.new.divides_by_zero?(bad_expr)
|
235
|
+
# # => true. WHOOPS. DO NOT RUN THIS.
|
236
|
+
#
|
237
|
+
# Of course, this won't detect more complex cases... unless you
|
238
|
+
# use some partial evaluation before! The possibilites are
|
239
|
+
# endless. Have fun.
|
240
|
+
module Mixin
|
241
|
+
# Dispatches `node`. If a node has type `:foo`, then a handler
|
242
|
+
# named `on_foo` is invoked with one argument, the `node`; if
|
243
|
+
# there isn't such a handler, {#handler_missing} is invoked
|
244
|
+
# with the same argument.
|
245
|
+
#
|
246
|
+
# If the handler returns `nil`, `node` is returned; otherwise,
|
247
|
+
# the return value of the handler is passed along.
|
248
|
+
#
|
249
|
+
# @param [AST::Node, nil] node
|
250
|
+
# @return [AST::Node, nil]
|
251
|
+
def process(node)
|
252
|
+
return if node.nil?
|
253
|
+
|
254
|
+
node = node.to_ast
|
255
|
+
|
256
|
+
# Invoke a specific handler
|
257
|
+
on_handler = :"on_#{node.type}"
|
258
|
+
if respond_to? on_handler
|
259
|
+
new_node = send on_handler, node
|
260
|
+
else
|
261
|
+
new_node = handler_missing(node)
|
262
|
+
end
|
263
|
+
|
264
|
+
node = new_node if new_node
|
265
|
+
|
266
|
+
node
|
267
|
+
end
|
268
|
+
|
269
|
+
# {#process}es each node from `nodes` and returns an array of
|
270
|
+
# results.
|
271
|
+
#
|
272
|
+
# @param [Array<AST::Node>] nodes
|
273
|
+
# @return [Array<AST::Node>]
|
274
|
+
def process_all(nodes)
|
275
|
+
nodes.to_a.map do |node|
|
276
|
+
process node
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Default handler. Does nothing.
|
281
|
+
#
|
282
|
+
# @param [AST::Node] node
|
283
|
+
# @return [AST::Node, nil]
|
284
|
+
def handler_missing(node)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ast
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- whitequark
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '12.3'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '12.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bacon
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,46 +68,18 @@ dependencies:
|
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: coveralls
|
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: json_pure
|
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
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: mime-types
|
99
71
|
requirement: !ruby/object:Gem::Requirement
|
100
72
|
requirements:
|
101
73
|
- - "~>"
|
102
74
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
75
|
+
version: 0.8.23
|
104
76
|
type: :development
|
105
77
|
prerelease: false
|
106
78
|
version_requirements: !ruby/object:Gem::Requirement
|
107
79
|
requirements:
|
108
80
|
- - "~>"
|
109
81
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
82
|
+
version: 0.8.23
|
111
83
|
- !ruby/object:Gem::Dependency
|
112
84
|
name: yard
|
113
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,22 +115,13 @@ executables: []
|
|
143
115
|
extensions: []
|
144
116
|
extra_rdoc_files: []
|
145
117
|
files:
|
146
|
-
- ".gitignore"
|
147
|
-
- ".travis.yml"
|
148
|
-
- ".yardopts"
|
149
|
-
- CHANGELOG.md
|
150
|
-
- Gemfile
|
151
118
|
- LICENSE.MIT
|
152
119
|
- README.YARD.md
|
153
|
-
- README.md
|
154
|
-
- Rakefile
|
155
|
-
- ast.gemspec
|
156
120
|
- lib/ast.rb
|
157
121
|
- lib/ast/node.rb
|
158
122
|
- lib/ast/processor.rb
|
123
|
+
- lib/ast/processor/mixin.rb
|
159
124
|
- lib/ast/sexp.rb
|
160
|
-
- test/helper.rb
|
161
|
-
- test/test_ast.rb
|
162
125
|
homepage: https://whitequark.github.io/ast/
|
163
126
|
licenses:
|
164
127
|
- MIT
|
@@ -178,10 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
141
|
- !ruby/object:Gem::Version
|
179
142
|
version: '0'
|
180
143
|
requirements: []
|
181
|
-
|
182
|
-
rubygems_version: 2.2.2
|
144
|
+
rubygems_version: 3.1.2
|
183
145
|
signing_key:
|
184
146
|
specification_version: 4
|
185
147
|
summary: A library for working with Abstract Syntax Trees.
|
186
148
|
test_files: []
|
187
|
-
has_rdoc:
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/.yardopts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
-r README.YARD.md -m markdown --protected
|
data/CHANGELOG.md
DELETED
data/Gemfile
DELETED
data/README.md
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# AST
|
2
|
-
|
3
|
-
[](https://travis-ci.org/whitequark/ast)
|
4
|
-
[](https://codeclimate.com/github/whitequark/ast)
|
5
|
-
[](https://coveralls.io/r/whitequark/ast)
|
6
|
-
|
7
|
-
AST is a small library for working with immutable abstract syntax trees.
|
8
|
-
|
9
|
-
## Installation
|
10
|
-
|
11
|
-
$ gem install ast
|
12
|
-
|
13
|
-
## Usage
|
14
|
-
|
15
|
-
See the documentation at [GitHub](http://whitequark.github.com/ast/frames.html) or [rdoc.info](http://rdoc.info/gems/ast).
|
16
|
-
|
17
|
-
## Contributing
|
18
|
-
|
19
|
-
1. Fork it
|
20
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
21
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
22
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
23
|
-
5. Create new Pull Request
|
data/Rakefile
DELETED
@@ -1,19 +0,0 @@
|
|
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 -Itest -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/;" 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
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
Gem::Specification.new do |s|
|
2
|
-
s.name = 'ast'
|
3
|
-
s.version = '2.0.0'
|
4
|
-
s.license = 'MIT'
|
5
|
-
s.authors = ["Peter Zotov"]
|
6
|
-
s.email = ["whitequark@whitequark.org"]
|
7
|
-
s.homepage = "https://whitequark.github.io/ast/"
|
8
|
-
s.summary = %q{A library for working with Abstract Syntax Trees.}
|
9
|
-
s.description = s.summary
|
10
|
-
|
11
|
-
s.files = `git ls-files`.split("\n")
|
12
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
13
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
14
|
-
s.require_paths = ["lib"]
|
15
|
-
|
16
|
-
s.add_development_dependency 'rake', '~> 10.0'
|
17
|
-
|
18
|
-
s.add_development_dependency 'bacon', '~> 1.2'
|
19
|
-
s.add_development_dependency 'bacon-colored_output'
|
20
|
-
s.add_development_dependency 'simplecov'
|
21
|
-
|
22
|
-
s.add_development_dependency 'coveralls'
|
23
|
-
s.add_development_dependency 'json_pure' # for coveralls on 1.9.2
|
24
|
-
s.add_development_dependency 'mime-types', '~> 1.25' # for coveralls on 1.8.7
|
25
|
-
|
26
|
-
s.add_development_dependency 'yard'
|
27
|
-
s.add_development_dependency 'kramdown'
|
28
|
-
end
|
data/test/helper.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'bacon'
|
2
|
-
require 'bacon/colored_output'
|
3
|
-
|
4
|
-
require 'simplecov'
|
5
|
-
require 'coveralls'
|
6
|
-
|
7
|
-
SimpleCov.start do
|
8
|
-
self.formatter = SimpleCov::Formatter::MultiFormatter[
|
9
|
-
SimpleCov::Formatter::HTMLFormatter,
|
10
|
-
Coveralls::SimpleCov::Formatter
|
11
|
-
]
|
12
|
-
|
13
|
-
# Exclude the testsuite itself.
|
14
|
-
add_filter "/test/"
|
15
|
-
end
|
16
|
-
|
17
|
-
require 'ast'
|
data/test/test_ast.rb
DELETED
@@ -1,243 +0,0 @@
|
|
1
|
-
require '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 to compute #hash' do
|
72
|
-
@node.hash.should.equal([@node.type, @node.children, @node.class].hash)
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'should only use type and children in #eql? comparisons' do
|
76
|
-
# Not identical but equivalent
|
77
|
-
@node.eql?(AST::Node.new(:node, [0, 1])).should.be.true
|
78
|
-
# Not identical and not equivalent
|
79
|
-
@node.eql?(AST::Node.new(:other, [0, 1])).should.be.false
|
80
|
-
# Not identical and not equivalent because of differend class
|
81
|
-
@node.eql?(@metanode).should.be.false
|
82
|
-
end
|
83
|
-
|
84
|
-
it 'should only use type and children in #== comparisons' do
|
85
|
-
@node.should.equal @node
|
86
|
-
@node.should.equal @metanode
|
87
|
-
@node.should.not.equal :foo
|
88
|
-
|
89
|
-
mock_node = Object.new.tap do |obj|
|
90
|
-
def obj.to_ast
|
91
|
-
self
|
92
|
-
end
|
93
|
-
|
94
|
-
def obj.type
|
95
|
-
:node
|
96
|
-
end
|
97
|
-
|
98
|
-
def obj.children
|
99
|
-
[ 0, 1 ]
|
100
|
-
end
|
101
|
-
end
|
102
|
-
@node.should.equal mock_node
|
103
|
-
end
|
104
|
-
|
105
|
-
it 'should allow to decompose nodes with a, b = *node' do
|
106
|
-
node = s(:gasgn, :$foo, s(:integer, 1))
|
107
|
-
|
108
|
-
var_name, value = *node
|
109
|
-
var_name.should.equal :$foo
|
110
|
-
value.should.equal s(:integer, 1)
|
111
|
-
end
|
112
|
-
|
113
|
-
it 'should concatenate with arrays' do
|
114
|
-
node = s(:gasgn, :$foo)
|
115
|
-
(node + [s(:integer, 1)]).
|
116
|
-
should.equal s(:gasgn, :$foo, s(:integer, 1))
|
117
|
-
end
|
118
|
-
|
119
|
-
it 'should append elements' do
|
120
|
-
node = s(:array)
|
121
|
-
(node << s(:integer, 1) << s(:string, "foo")).
|
122
|
-
should.equal s(:array, s(:integer, 1), s(:string, "foo"))
|
123
|
-
end
|
124
|
-
|
125
|
-
begin
|
126
|
-
eval <<-CODE
|
127
|
-
it 'should not trigger a rubinius bug' do
|
128
|
-
bar = [ s(:bar, 1) ]
|
129
|
-
baz = s(:baz, 2)
|
130
|
-
s(:foo, *bar, baz).should.equal s(:foo, s(:bar, 1), s(:baz, 2))
|
131
|
-
end
|
132
|
-
CODE
|
133
|
-
rescue SyntaxError
|
134
|
-
# Running on 1.8, ignore.
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
describe AST::Processor do
|
139
|
-
extend AST::Sexp
|
140
|
-
|
141
|
-
def have_sexp(text)
|
142
|
-
text = text.lines.map { |line| line.sub /^ +\|(.+)/, '\1' }.join.rstrip
|
143
|
-
lambda { |ast| ast.to_sexp == text }
|
144
|
-
end
|
145
|
-
|
146
|
-
class MockProcessor < AST::Processor
|
147
|
-
attr_reader :counts
|
148
|
-
|
149
|
-
def initialize
|
150
|
-
@counts = Hash.new(0)
|
151
|
-
end
|
152
|
-
|
153
|
-
def on_root(node)
|
154
|
-
count_node(node)
|
155
|
-
node.updated(nil, process_all(node.children))
|
156
|
-
end
|
157
|
-
alias on_body on_root
|
158
|
-
|
159
|
-
def on_def(node)
|
160
|
-
count_node(node)
|
161
|
-
name, arglist, body = node.children
|
162
|
-
node.updated(:def, [ name, process(arglist), process(body) ])
|
163
|
-
end
|
164
|
-
|
165
|
-
def handler_missing(node)
|
166
|
-
count_node(node)
|
167
|
-
end
|
168
|
-
|
169
|
-
def count_node(node)
|
170
|
-
@counts[node.type] += 1; nil
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
before do
|
175
|
-
@ast = AST::Node.new(:root, [
|
176
|
-
AST::Node.new(:def, [ :func,
|
177
|
-
AST::Node.new(:arglist, [ :foo, :bar ]),
|
178
|
-
AST::Node.new(:body, [
|
179
|
-
AST::Node.new(:invoke, [ :puts, "Hello world" ])
|
180
|
-
])
|
181
|
-
]),
|
182
|
-
AST::Node.new(:invoke, [ :func ])
|
183
|
-
])
|
184
|
-
|
185
|
-
@processor = MockProcessor.new
|
186
|
-
end
|
187
|
-
|
188
|
-
it 'should visit every node' do
|
189
|
-
@processor.process(@ast).should.equal @ast
|
190
|
-
@processor.counts.should.equal({
|
191
|
-
:root => 1,
|
192
|
-
:def => 1,
|
193
|
-
:arglist => 1,
|
194
|
-
:body => 1,
|
195
|
-
:invoke => 2,
|
196
|
-
})
|
197
|
-
end
|
198
|
-
|
199
|
-
it 'should be able to replace inner nodes' do
|
200
|
-
def @processor.on_arglist(node)
|
201
|
-
node.updated(:new_fancy_arglist)
|
202
|
-
end
|
203
|
-
|
204
|
-
@processor.process(@ast).should have_sexp(<<-SEXP)
|
205
|
-
|(root
|
206
|
-
| (def :func
|
207
|
-
| (new-fancy-arglist :foo :bar)
|
208
|
-
| (body
|
209
|
-
| (invoke :puts "Hello world")))
|
210
|
-
| (invoke :func))
|
211
|
-
SEXP
|
212
|
-
end
|
213
|
-
|
214
|
-
it 'should build sexps' do
|
215
|
-
s(:add,
|
216
|
-
s(:integer, 1),
|
217
|
-
s(:multiply,
|
218
|
-
s(:integer, 2),
|
219
|
-
s(:integer, 3))).should have_sexp(<<-SEXP)
|
220
|
-
|(add
|
221
|
-
| (integer 1)
|
222
|
-
| (multiply
|
223
|
-
| (integer 2)
|
224
|
-
| (integer 3)))
|
225
|
-
SEXP
|
226
|
-
end
|
227
|
-
|
228
|
-
it 'should return nil if passed nil' do
|
229
|
-
@processor.process(nil).should == nil
|
230
|
-
end
|
231
|
-
|
232
|
-
it 'should refuse to process non-nodes' do
|
233
|
-
lambda { @processor.process([]) }.should.raise NoMethodError, %r|to_ast|
|
234
|
-
end
|
235
|
-
|
236
|
-
it 'should allow to visit nodes with process_all(node)' do
|
237
|
-
@processor.process_all s(:foo, s(:bar), s(:integer, 1))
|
238
|
-
@processor.counts.should.equal({
|
239
|
-
:bar => 1,
|
240
|
-
:integer => 1,
|
241
|
-
})
|
242
|
-
end
|
243
|
-
end
|