furnace 0.2.6 → 0.3.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.
- data/.gitignore +3 -2
- data/.yardopts +1 -0
- data/Gemfile +1 -0
- data/Rakefile +23 -1
- data/furnace.gemspec +5 -0
- data/lib/furnace/ast/node.rb +148 -61
- data/lib/furnace/ast/processor.rb +265 -0
- data/lib/furnace/ast/sexp.rb +30 -0
- data/lib/furnace/ast.rb +18 -8
- data/lib/furnace/cfg/algorithms.rb +193 -0
- data/lib/furnace/cfg/graph.rb +5 -185
- data/lib/furnace/cfg.rb +5 -4
- data/lib/furnace/version.rb +1 -1
- data/lib/furnace.rb +19 -9
- data/test/ast_test.rb +185 -0
- data/test/test_helper.rb +36 -0
- metadata +80 -16
- data/lib/furnace/ast/matcher/dsl.rb +0 -50
- data/lib/furnace/ast/matcher/special.rb +0 -20
- data/lib/furnace/ast/matcher.rb +0 -198
- data/lib/furnace/ast/strict_visitor.rb +0 -20
- data/lib/furnace/ast/visitor.rb +0 -50
- data/lib/furnace/code/newline_token.rb +0 -9
- data/lib/furnace/code/nonterminal_token.rb +0 -16
- data/lib/furnace/code/separated_token.rb +0 -17
- data/lib/furnace/code/surrounded_token.rb +0 -21
- data/lib/furnace/code/terminal_token.rb +0 -9
- data/lib/furnace/code/token.rb +0 -73
- data/lib/furnace/code.rb +0 -10
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module Furnace::CFG
|
|
2
|
+
module Algorithms
|
|
3
|
+
def eliminate_unreachable!
|
|
4
|
+
worklist = Set[ entry, exit ]
|
|
5
|
+
reachable = Set[]
|
|
6
|
+
|
|
7
|
+
while worklist.any?
|
|
8
|
+
node = worklist.first
|
|
9
|
+
worklist.delete node
|
|
10
|
+
reachable.add node
|
|
11
|
+
|
|
12
|
+
node.targets.each do |target|
|
|
13
|
+
unless reachable.include? target
|
|
14
|
+
worklist.add target
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if node.exception
|
|
19
|
+
unless reachable.include? node.exception
|
|
20
|
+
worklist.add node.exception
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@nodes.each do |node|
|
|
26
|
+
unless reachable.include? node
|
|
27
|
+
@nodes.delete node
|
|
28
|
+
yield node if block_given?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
flush
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def merge_redundant!
|
|
36
|
+
worklist = @nodes.dup
|
|
37
|
+
while worklist.any?
|
|
38
|
+
node = worklist.first
|
|
39
|
+
worklist.delete node
|
|
40
|
+
|
|
41
|
+
target = node.targets[0]
|
|
42
|
+
next if target == @exit
|
|
43
|
+
|
|
44
|
+
# Skip explicitly non-redundant nodes
|
|
45
|
+
if node.metadata[:keep]
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if node.targets.uniq == [target] &&
|
|
50
|
+
target.sources.uniq == [node] &&
|
|
51
|
+
node.exception == target.exception
|
|
52
|
+
|
|
53
|
+
yield node, target if block_given?
|
|
54
|
+
|
|
55
|
+
node.insns.delete node.cti
|
|
56
|
+
@nodes.delete target
|
|
57
|
+
worklist.delete target
|
|
58
|
+
|
|
59
|
+
node.insns.concat target.insns
|
|
60
|
+
node.cti = target.cti
|
|
61
|
+
node.target_labels = target.target_labels
|
|
62
|
+
|
|
63
|
+
worklist.add node
|
|
64
|
+
|
|
65
|
+
flush
|
|
66
|
+
elsif node.targets.count == 1 &&
|
|
67
|
+
node.insns.empty?
|
|
68
|
+
|
|
69
|
+
target = node.targets.first
|
|
70
|
+
|
|
71
|
+
yield target, node if block_given?
|
|
72
|
+
|
|
73
|
+
node.sources.each do |source|
|
|
74
|
+
index = source.targets.index(node)
|
|
75
|
+
source.target_labels[index] = target.label
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@nodes.delete node
|
|
79
|
+
worklist.delete node
|
|
80
|
+
|
|
81
|
+
if @entry == node
|
|
82
|
+
@entry = target
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
flush
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Shamelessly stolen from
|
|
91
|
+
# http://www.cs.colostate.edu/~mstrout/CS553/slides/lecture04.pdf
|
|
92
|
+
def compute_generic_domination(start, forward)
|
|
93
|
+
# values of β will give rise to dom!
|
|
94
|
+
dom = { start => Set[start] }
|
|
95
|
+
|
|
96
|
+
@nodes.each do |node|
|
|
97
|
+
next if node == start
|
|
98
|
+
dom[node] = @nodes.dup
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
change = true
|
|
102
|
+
while change
|
|
103
|
+
change = false
|
|
104
|
+
@nodes.each do |node|
|
|
105
|
+
next if node == start
|
|
106
|
+
|
|
107
|
+
# Are we computing dominators or postdominators?
|
|
108
|
+
if forward
|
|
109
|
+
edges = node.sources + node.exception_sources
|
|
110
|
+
elsif node.exception.nil?
|
|
111
|
+
edges = node.targets
|
|
112
|
+
else
|
|
113
|
+
edges = node.targets + [ node.exception ]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Key Idea [for dominators]
|
|
117
|
+
# If a node dominates all
|
|
118
|
+
# predecessors of node n, then it
|
|
119
|
+
# also dominates node n.
|
|
120
|
+
pred = edges.map do |source|
|
|
121
|
+
dom[source]
|
|
122
|
+
end.reduce(:&)
|
|
123
|
+
|
|
124
|
+
# An exception handler header node has no regular sources.
|
|
125
|
+
pred = [] if pred.nil?
|
|
126
|
+
|
|
127
|
+
current = Set[node].merge(pred)
|
|
128
|
+
if current != dom[node]
|
|
129
|
+
dom[node] = current
|
|
130
|
+
change = true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
dom
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def dominators
|
|
139
|
+
@dominators ||= compute_generic_domination(@entry, true)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def postdominators
|
|
143
|
+
@postdominators ||= compute_generic_domination(@exit, false)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# See also {#dominators} for references.
|
|
147
|
+
def identify_loops
|
|
148
|
+
loops = Hash.new { |h,k| h[k] = Set.new }
|
|
149
|
+
|
|
150
|
+
dom = dominators
|
|
151
|
+
|
|
152
|
+
@nodes.each do |node|
|
|
153
|
+
node.targets.each do |target|
|
|
154
|
+
# Back edges
|
|
155
|
+
# A back edge of a natural loop is one whose
|
|
156
|
+
# target dominates its source.
|
|
157
|
+
if dom[node].include? target
|
|
158
|
+
loops[target].add node
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# At this point, +loops+ contains a list of all nodes
|
|
164
|
+
# which have a back edge to the loop header. Expand
|
|
165
|
+
# it to the list of all nodes in the loop.
|
|
166
|
+
loops.each do |header, nodes|
|
|
167
|
+
# Natural loop
|
|
168
|
+
# The natural loop of a back edge (m→n), where
|
|
169
|
+
# n dominates m, is the set of nodes x such that n
|
|
170
|
+
# dominates x and there is a path from x to m not
|
|
171
|
+
# containing n.
|
|
172
|
+
pre_header = dom[header]
|
|
173
|
+
all_nodes = Set[header]
|
|
174
|
+
|
|
175
|
+
nodes.each do |node|
|
|
176
|
+
all_nodes.merge(dom[node] - pre_header)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
nodes.replace all_nodes
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
loops.default = nil
|
|
183
|
+
loops
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def flush
|
|
187
|
+
@dominators = nil
|
|
188
|
+
@postdominators = nil
|
|
189
|
+
|
|
190
|
+
super if defined?(super)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/furnace/cfg/graph.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Furnace::CFG
|
|
2
2
|
class Graph
|
|
3
|
+
include Algorithms
|
|
4
|
+
|
|
3
5
|
attr_reader :nodes
|
|
4
6
|
attr_accessor :entry, :exit
|
|
5
7
|
|
|
@@ -21,188 +23,7 @@ module Furnace::CFG
|
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
worklist = Set[ entry, exit ]
|
|
26
|
-
reachable = Set[]
|
|
27
|
-
|
|
28
|
-
while worklist.any?
|
|
29
|
-
node = worklist.first
|
|
30
|
-
worklist.delete node
|
|
31
|
-
reachable.add node
|
|
32
|
-
|
|
33
|
-
node.targets.each do |target|
|
|
34
|
-
unless reachable.include? target
|
|
35
|
-
worklist.add target
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
if node.exception
|
|
40
|
-
unless reachable.include? node.exception
|
|
41
|
-
worklist.add node.exception
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
@nodes.each do |node|
|
|
47
|
-
unless reachable.include? node
|
|
48
|
-
@nodes.delete node
|
|
49
|
-
yield node if block_given?
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
flush
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def merge_redundant!
|
|
57
|
-
worklist = @nodes.dup
|
|
58
|
-
while worklist.any?
|
|
59
|
-
node = worklist.first
|
|
60
|
-
worklist.delete node
|
|
61
|
-
|
|
62
|
-
target = node.targets[0]
|
|
63
|
-
next if target == @exit
|
|
64
|
-
|
|
65
|
-
# Skip explicitly non-redundant nodes
|
|
66
|
-
if node.metadata[:keep]
|
|
67
|
-
next
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if node.targets.uniq == [target] &&
|
|
71
|
-
target.sources.uniq == [node] &&
|
|
72
|
-
node.exception == target.exception
|
|
73
|
-
|
|
74
|
-
yield node, target if block_given?
|
|
75
|
-
|
|
76
|
-
node.insns.delete node.cti
|
|
77
|
-
@nodes.delete target
|
|
78
|
-
worklist.delete target
|
|
79
|
-
|
|
80
|
-
node.insns.concat target.insns
|
|
81
|
-
node.cti = target.cti
|
|
82
|
-
node.target_labels = target.target_labels
|
|
83
|
-
|
|
84
|
-
worklist.add node
|
|
85
|
-
|
|
86
|
-
flush
|
|
87
|
-
elsif node.targets.count == 1 &&
|
|
88
|
-
node.insns.empty?
|
|
89
|
-
|
|
90
|
-
target = node.targets.first
|
|
91
|
-
|
|
92
|
-
yield target, node if block_given?
|
|
93
|
-
|
|
94
|
-
node.sources.each do |source|
|
|
95
|
-
index = source.targets.index(node)
|
|
96
|
-
source.target_labels[index] = target.label
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
@nodes.delete node
|
|
100
|
-
worklist.delete node
|
|
101
|
-
|
|
102
|
-
if @entry == node
|
|
103
|
-
@entry = target
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
flush
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Shamelessly stolen from
|
|
112
|
-
# http://www.cs.colostate.edu/~mstrout/CS553/slides/lecture04.pdf
|
|
113
|
-
def compute_generic_domination(start, forward)
|
|
114
|
-
# values of β will give rise to dom!
|
|
115
|
-
dom = { start => Set[start] }
|
|
116
|
-
|
|
117
|
-
@nodes.each do |node|
|
|
118
|
-
next if node == start
|
|
119
|
-
dom[node] = @nodes.dup
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
change = true
|
|
123
|
-
while change
|
|
124
|
-
change = false
|
|
125
|
-
@nodes.each do |node|
|
|
126
|
-
next if node == start
|
|
127
|
-
|
|
128
|
-
# Are we computing dominators or postdominators?
|
|
129
|
-
if forward
|
|
130
|
-
edges = node.sources + node.exception_sources
|
|
131
|
-
else
|
|
132
|
-
edges = node.targets + [ node.exception ]
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Key Idea [for dominators]
|
|
136
|
-
# If a node dominates all
|
|
137
|
-
# predecessors of node n, then it
|
|
138
|
-
# also dominates node n.
|
|
139
|
-
pred = edges.map do |source|
|
|
140
|
-
dom[source]
|
|
141
|
-
end.reduce(:&)
|
|
142
|
-
|
|
143
|
-
# An exception handler header node has no regular sources.
|
|
144
|
-
pred = [] if pred.nil?
|
|
145
|
-
|
|
146
|
-
current = Set[node].merge(pred)
|
|
147
|
-
if current != dom[node]
|
|
148
|
-
dom[node] = current
|
|
149
|
-
change = true
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
dom
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def dominators
|
|
158
|
-
@dominators ||= compute_generic_domination(@entry, true)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def postdominators
|
|
162
|
-
@postdominators ||= compute_generic_domination(@exit, false)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# See also {#dominators} for references.
|
|
166
|
-
def identify_loops
|
|
167
|
-
loops = Hash.new { |h,k| h[k] = Set.new }
|
|
168
|
-
|
|
169
|
-
dom = dominators
|
|
170
|
-
|
|
171
|
-
@nodes.each do |node|
|
|
172
|
-
node.targets.each do |target|
|
|
173
|
-
# Back edges
|
|
174
|
-
# A back edge of a natural loop is one whose
|
|
175
|
-
# target dominates its source.
|
|
176
|
-
if dom[node].include? target
|
|
177
|
-
loops[target].add node
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# At this point, +loops+ contains a list of all nodes
|
|
183
|
-
# which have a back edge to the loop header. Expand
|
|
184
|
-
# it to the list of all nodes in the loop.
|
|
185
|
-
loops.each do |header, nodes|
|
|
186
|
-
# Natural loop
|
|
187
|
-
# The natural loop of a back edge (m→n), where
|
|
188
|
-
# n dominates m, is the set of nodes x such that n
|
|
189
|
-
# dominates x and there is a path from x to m not
|
|
190
|
-
# containing n.
|
|
191
|
-
pre_header = dom[header]
|
|
192
|
-
all_nodes = Set[header]
|
|
193
|
-
|
|
194
|
-
nodes.each do |node|
|
|
195
|
-
all_nodes.merge(dom[node] - pre_header)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
nodes.replace all_nodes
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
loops.default = nil
|
|
202
|
-
loops
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def sources_for(node, exceptions=false)
|
|
26
|
+
def sources_for(node, find_exceptions=false)
|
|
206
27
|
unless @source_map
|
|
207
28
|
@source_map = Hash.new { |h, k| h[k] = [] }
|
|
208
29
|
@exception_source_map = Hash.new { |h, k| h[k] = [] }
|
|
@@ -224,7 +45,7 @@ module Furnace::CFG
|
|
|
224
45
|
end
|
|
225
46
|
end
|
|
226
47
|
|
|
227
|
-
if
|
|
48
|
+
if find_exceptions
|
|
228
49
|
@exception_source_map[node]
|
|
229
50
|
else
|
|
230
51
|
@source_map[node]
|
|
@@ -235,8 +56,7 @@ module Furnace::CFG
|
|
|
235
56
|
@source_map = nil
|
|
236
57
|
@label_map.clear
|
|
237
58
|
|
|
238
|
-
|
|
239
|
-
@postdominators = nil
|
|
59
|
+
super if defined?(super)
|
|
240
60
|
end
|
|
241
61
|
|
|
242
62
|
def to_graphviz
|
data/lib/furnace/cfg.rb
CHANGED
data/lib/furnace/version.rb
CHANGED
data/lib/furnace.rb
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
# Furnace is a set of tools for writing compilers, translators or
|
|
2
|
+
# static analyzers--any programs which read, manipulate or transform
|
|
3
|
+
# other programs.
|
|
4
|
+
#
|
|
5
|
+
# Currently it provides three independent modules, grouped by the main
|
|
6
|
+
# data structure being used:
|
|
7
|
+
#
|
|
8
|
+
# * Abstract syntax trees: {AST}
|
|
9
|
+
# * Control flow graphs: {CFG}
|
|
10
|
+
# * Transformations: {Transform}
|
|
11
|
+
#
|
|
12
|
+
# Additionally, a simple {Graphviz} adapter is provided.
|
|
1
13
|
module Furnace
|
|
2
|
-
|
|
14
|
+
require "furnace/version"
|
|
3
15
|
|
|
4
|
-
require "furnace/
|
|
16
|
+
require "furnace/ast"
|
|
17
|
+
require "furnace/cfg"
|
|
5
18
|
|
|
6
|
-
require "furnace/
|
|
7
|
-
require "furnace/
|
|
8
|
-
require "furnace/code"
|
|
19
|
+
require "furnace/transform/pipeline"
|
|
20
|
+
require "furnace/transform/iterative_process"
|
|
9
21
|
|
|
10
|
-
require "furnace/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
require "furnace/graphviz"
|
|
22
|
+
require "furnace/graphviz"
|
|
23
|
+
end
|
data/test/ast_test.rb
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
describe AST::Node do
|
|
4
|
+
class MetaNode < AST::Node
|
|
5
|
+
attr_reader :meta
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
@node = AST::Node.new(:node, [ 0, 1 ])
|
|
10
|
+
@metanode = MetaNode.new(:node, [ 0, 1 ], meta: 'value')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'should have accessors for type and children' do
|
|
14
|
+
@node.type.should.equal :node
|
|
15
|
+
@node.children.should.equal [0, 1]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'should set metadata' do
|
|
19
|
+
@metanode.meta.should.equal 'value'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'should be frozen' do
|
|
23
|
+
@node.frozen?.should.be.true
|
|
24
|
+
@node.children.frozen?.should.be.true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'should not allow duping' do
|
|
28
|
+
-> { @node.dup }.should.raise NoMethodError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'should return an updated node, but only if needed' do
|
|
32
|
+
@node.updated().should.be.identical_to @node
|
|
33
|
+
@node.updated(:node).should.be.identical_to @node
|
|
34
|
+
@node.updated(nil, [0, 1]).should.be.identical_to @node
|
|
35
|
+
|
|
36
|
+
updated = @node.updated(:other_node)
|
|
37
|
+
updated.should.not.be.identical_to @node
|
|
38
|
+
updated.type.should.equal :other_node
|
|
39
|
+
updated.children.should.equal @node.children
|
|
40
|
+
|
|
41
|
+
updated.frozen?.should.be.true
|
|
42
|
+
|
|
43
|
+
updated = @node.updated(nil, [1, 1])
|
|
44
|
+
updated.should.not.be.identical_to @node
|
|
45
|
+
updated.type.should.equal @node.type
|
|
46
|
+
updated.children.should.equal [1, 1]
|
|
47
|
+
|
|
48
|
+
updated = @metanode.updated(nil, nil, meta: 'other_value')
|
|
49
|
+
updated.meta.should.equal 'other_value'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'should use fancy type in to_s' do
|
|
53
|
+
node = AST::Node.new(:ast_node)
|
|
54
|
+
node.to_s.should.equal '(ast-node ...)'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should format to_sexp correctly' do
|
|
58
|
+
AST::Node.new(:a, [ :sym, [ 1, 2 ] ]).to_sexp.should.equal '(a :sym [1, 2])'
|
|
59
|
+
AST::Node.new(:a, [ :sym, @node ]).to_sexp.should.equal "(a :sym\n (node 0 1))"
|
|
60
|
+
AST::Node.new(:a, [ :sym,
|
|
61
|
+
AST::Node.new(:b, [ @node, @node ])
|
|
62
|
+
]).to_sexp.should.equal "(a :sym\n (b\n (node 0 1)\n (node 0 1)))"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'should return self in to_ast' do
|
|
66
|
+
@node.to_ast.should.be.identical_to @node
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'should only use type and children in comparisons' do
|
|
70
|
+
@node.should.equal @node
|
|
71
|
+
@node.should.equal @metanode
|
|
72
|
+
@node.should.not.equal :foo
|
|
73
|
+
|
|
74
|
+
mock_node = Object.new.tap do |obj|
|
|
75
|
+
def obj.to_ast
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def obj.type
|
|
80
|
+
:node
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def obj.children
|
|
84
|
+
[ 0, 1 ]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
@node.should.equal mock_node
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe AST::Processor do
|
|
92
|
+
def have_sexp(text)
|
|
93
|
+
text = text.lines.map { |line| line.sub /^ +\|(.+)/, '\1' }.join.rstrip
|
|
94
|
+
lambda { |ast| ast.to_sexp == text }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class MockProcessor < AST::Processor
|
|
98
|
+
attr_reader :counts
|
|
99
|
+
|
|
100
|
+
def initialize
|
|
101
|
+
@counts = Hash.new(0)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def on_root(node)
|
|
105
|
+
count_node(node)
|
|
106
|
+
node.updated(nil, process_all(node.children))
|
|
107
|
+
end
|
|
108
|
+
alias on_body on_root
|
|
109
|
+
|
|
110
|
+
def on_def(node)
|
|
111
|
+
count_node(node)
|
|
112
|
+
name, arglist, body = node.children
|
|
113
|
+
node.updated(:def, [ name, process(arglist), process(body) ])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handler_missing(node)
|
|
117
|
+
count_node(node)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def count_node(node)
|
|
121
|
+
@counts[node.type] += 1; nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
before do
|
|
126
|
+
@ast = AST::Node.new(:root, [
|
|
127
|
+
AST::Node.new(:def, [ :func,
|
|
128
|
+
AST::Node.new(:arglist, [ :foo, :bar ]),
|
|
129
|
+
AST::Node.new(:body, [
|
|
130
|
+
AST::Node.new(:invoke, [ :puts, "Hello world" ])
|
|
131
|
+
])
|
|
132
|
+
]),
|
|
133
|
+
AST::Node.new(:invoke, [ :func ])
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
@processor = MockProcessor.new
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'should visit every node' do
|
|
140
|
+
@processor.process(@ast).should.equal @ast
|
|
141
|
+
@processor.counts.should.equal({
|
|
142
|
+
root: 1,
|
|
143
|
+
def: 1,
|
|
144
|
+
arglist: 1,
|
|
145
|
+
body: 1,
|
|
146
|
+
invoke: 2
|
|
147
|
+
})
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'should be able to replace inner nodes' do
|
|
151
|
+
def @processor.on_arglist(node)
|
|
152
|
+
node.updated(:new_fancy_arglist)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@processor.process(@ast).should have_sexp(<<-SEXP)
|
|
156
|
+
|(root
|
|
157
|
+
| (def :func
|
|
158
|
+
| (new-fancy-arglist :foo :bar)
|
|
159
|
+
| (body
|
|
160
|
+
| (invoke :puts "Hello world")))
|
|
161
|
+
| (invoke :func))
|
|
162
|
+
SEXP
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
extend Furnace::AST::Sexp
|
|
166
|
+
|
|
167
|
+
it 'should build sexps' do
|
|
168
|
+
s(:add,
|
|
169
|
+
s(:integer, 1),
|
|
170
|
+
s(:multiply,
|
|
171
|
+
s(:integer, 2),
|
|
172
|
+
s(:integer, 3))).should have_sexp(<<-SEXP)
|
|
173
|
+
|(add
|
|
174
|
+
| (integer 1)
|
|
175
|
+
| (multiply
|
|
176
|
+
| (integer 2)
|
|
177
|
+
| (integer 3)))
|
|
178
|
+
SEXP
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'should refuse to process non-nodes' do
|
|
182
|
+
-> { @processor.process(nil) }.should.raise NoMethodError, %r|to_ast|
|
|
183
|
+
-> { @processor.process([]) }.should.raise NoMethodError, %r|to_ast|
|
|
184
|
+
end
|
|
185
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
|
2
|
+
|
|
3
|
+
require 'furnace'
|
|
4
|
+
include Furnace
|
|
5
|
+
|
|
6
|
+
module Bacon
|
|
7
|
+
module ColoredOutput
|
|
8
|
+
def handle_specification(name)
|
|
9
|
+
puts spaces + name
|
|
10
|
+
yield
|
|
11
|
+
puts if Counter[:context_depth] == 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def handle_requirement(description)
|
|
15
|
+
print spaces
|
|
16
|
+
|
|
17
|
+
error = yield
|
|
18
|
+
|
|
19
|
+
print error.empty? ? "\e[32m" : "\e[1;31m"
|
|
20
|
+
print " - #{description}"
|
|
21
|
+
puts error.empty? ? "\e[0m" : " [#{error}]\e[0m"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle_summary
|
|
25
|
+
print ErrorLog if Backtraces
|
|
26
|
+
puts "%d specifications (%d requirements), %d failures, %d errors" %
|
|
27
|
+
Counter.values_at(:specifications, :requirements, :failed, :errors)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def spaces
|
|
31
|
+
" " * (Counter[:context_depth] - 1)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
extend ColoredOutput
|
|
36
|
+
end
|