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.
@@ -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
@@ -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 eliminate_unreachable!
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 exceptions
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
- @dominators = nil
239
- @postdominators = nil
59
+ super if defined?(super)
240
60
  end
241
61
 
242
62
  def to_graphviz
data/lib/furnace/cfg.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "set"
2
2
 
3
- require "furnace"
4
-
5
- require "furnace/cfg/graph"
6
- require "furnace/cfg/node"
3
+ module Furnace
4
+ require_relative "cfg/node"
5
+ require_relative "cfg/algorithms"
6
+ require_relative "cfg/graph"
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Furnace
2
- VERSION = "0.2.6"
2
+ VERSION = "0.3.1"
3
3
  end
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
- end
14
+ require "furnace/version"
3
15
 
4
- require "furnace/version"
16
+ require "furnace/ast"
17
+ require "furnace/cfg"
5
18
 
6
- require "furnace/ast"
7
- require "furnace/cfg"
8
- require "furnace/code"
19
+ require "furnace/transform/pipeline"
20
+ require "furnace/transform/iterative_process"
9
21
 
10
- require "furnace/transform/pipeline"
11
- require "furnace/transform/iterative_process"
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
@@ -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