furnace 0.3.0.beta1 → 0.3.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
- *.gem
2
1
  .bundle
3
2
  Gemfile.lock
4
3
  pkg/*
5
4
  .rbx/
6
- *.sublime-*
5
+ *.sublime-*
6
+ doc/
7
+ .yardoc/
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown --protected
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in furnace.gemspec
4
4
  gemspec
5
+ gem 'bacon', github: 'chneukirchen/bacon'
data/Rakefile CHANGED
@@ -1 +1,12 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+
4
+ task :default => :test
5
+
6
+ task :test do
7
+ require 'bacon'
8
+ Bacon.summary_at_exit
9
+ Dir["test/**/*_test.rb"].each do |file|
10
+ load file
11
+ end
12
+ end
data/furnace.gemspec CHANGED
@@ -16,4 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
+
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'bacon', '~> 1.1'
19
22
  end
@@ -1,28 +1,105 @@
1
1
  module Furnace::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
+ #
2
38
  class Node
3
- attr_reader :type, :children
39
+ # Returns the type of this node.
40
+ # @return [Symbol]
41
+ attr_reader :type
4
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}.
5
57
  def initialize(type, children=[], properties={})
6
- @type, @children = type.to_sym, children.to_a
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
+ # a local 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
+ def assign_properties(properties)
7
71
  properties.each do |name, value|
8
72
  instance_variable_set :"@#{name}", value
9
73
  end
10
- freeze
11
74
  end
75
+ protected :assign_properties
12
76
 
13
- def update(type=nil, children=nil, properties={})
14
- new_type = type || @type
15
- new_children = children || @children
77
+ protected :dup
78
+
79
+ # Returns a new instance of Node where non-nil arguments replace the
80
+ # corresponding fields of `self`.
81
+ #
82
+ # For example, `Node.new(:foo, [ 1, 2 ]).updated(:bar)` would yield
83
+ # `(bar 1 2)`, and `Node.new(:foo, [ 1, 2 ]).updated(nil, [])` would
84
+ # yield `(foo)`.
85
+ #
86
+ # If the resulting node would be identical to `self`, does nothing.
87
+ def updated(type=nil, children=nil, properties=nil)
88
+ new_type = type || @type
89
+ new_children = children || @children
90
+ new_properties = properties || {}
16
91
 
17
92
  if @type == new_type &&
18
93
  @children == new_children &&
19
- properties.empty?
94
+ properties.nil?
20
95
  self
21
96
  else
22
- dup.send :initialize, new_type, new_children, properties
97
+ dup.send :initialize, new_type, new_children, new_properties
23
98
  end
24
99
  end
25
100
 
101
+ # Compares `self` to `other`, possibly converting with `to_ast`. Only
102
+ # `type` and `children` are compared; metadata is deliberately ignored.
26
103
  def ==(other)
27
104
  if equal?(other)
28
105
  true
@@ -35,35 +112,43 @@ module Furnace::AST
35
112
  end
36
113
  end
37
114
 
115
+ # Converts `self` to a concise s-expression, omitting any children.
38
116
  def to_s
39
117
  "(#{fancy_type} ...)"
40
118
  end
41
119
 
120
+ # Converts `self` to a pretty-printed s-expression.
42
121
  def to_sexp(indent=0)
43
- str = "#{" " * indent}(#{fancy_type}"
122
+ sexp = "#{" " * indent}(#{fancy_type}"
123
+
124
+ first_node_child = children.index do |child|
125
+ child.is_a?(Node) || child.is_a?(Array)
126
+ end || children.count
44
127
 
45
- children.each do |child|
46
- if (!children[0].is_a?(Node) && child.is_a?(Node)) ||
47
- (children[0].is_a?(Node) && child.is_a?(Node) &&
48
- child.children.any? { |c| c.is_a?(Node) || c.is_a?(Array) })
49
- str << "\n#{child.to_sexp(indent + 1)}"
128
+ children.each_with_index do |child, idx|
129
+ if child.is_a?(Node) && idx >= first_node_child
130
+ sexp << "\n#{child.to_sexp(indent + 1)}"
50
131
  else
51
- str << " #{child.inspect}"
132
+ sexp << " #{child.inspect}"
52
133
  end
53
134
  end
54
135
 
55
- str << ")"
136
+ sexp << ")"
56
137
 
57
- str
138
+ sexp
58
139
  end
59
140
  alias :inspect :to_sexp
60
141
 
142
+ # Returns `self`.
61
143
  def to_ast
62
144
  self
63
145
  end
64
146
 
65
147
  protected
66
148
 
149
+ # Returns `@type` with all underscores replaced by dashes. This allows
150
+ # to write symbol literals without quotes in Ruby sources and yet have
151
+ # nicely looking s-expressions.
67
152
  def fancy_type
68
153
  @type.to_s.gsub('_', '-')
69
154
  end
@@ -1,6 +1,6 @@
1
1
  module Furnace::AST
2
- module StrictVisitor
3
- def visit(node)
2
+ module Processor
3
+ def process(node)
4
4
  if node
5
5
  # Invoke a specific handler
6
6
  on_handler = :"on_#{node.type}"
@@ -16,9 +16,9 @@ module Furnace::AST
16
16
  node
17
17
  end
18
18
 
19
- def visit_all(nodes)
19
+ def process_all(nodes)
20
20
  nodes.map do |node|
21
- visit node
21
+ process node
22
22
  end
23
23
  end
24
24
 
data/lib/furnace/ast.rb CHANGED
@@ -1,9 +1,18 @@
1
- require "furnace"
1
+ module Furnace
2
+ # Furnace::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 significant pressure on
8
+ # garbage collector, but completely eliminates all concurrency
9
+ # and aliasing problems.
10
+ #
11
+ # See also {Node} and {Processor} for additional
12
+ # recommendations and design patterns.
13
+ module AST
14
+ end
2
15
 
3
- require "furnace/ast/node"
4
- require "furnace/ast/visitor"
5
- require "furnace/ast/strict_visitor"
6
-
7
- require "furnace/ast/matcher/special"
8
- require "furnace/ast/matcher/dsl"
9
- require "furnace/ast/matcher"
16
+ require_relative "ast/node"
17
+ require_relative "ast/processor"
18
+ end
@@ -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,190 +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
- elsif node.exception.nil?
132
- edges = node.targets
133
- else
134
- edges = node.targets + [ node.exception ]
135
- end
136
-
137
- # Key Idea [for dominators]
138
- # If a node dominates all
139
- # predecessors of node n, then it
140
- # also dominates node n.
141
- pred = edges.map do |source|
142
- dom[source]
143
- end.reduce(:&)
144
-
145
- # An exception handler header node has no regular sources.
146
- pred = [] if pred.nil?
147
-
148
- current = Set[node].merge(pred)
149
- if current != dom[node]
150
- dom[node] = current
151
- change = true
152
- end
153
- end
154
- end
155
-
156
- dom
157
- end
158
-
159
- def dominators
160
- @dominators ||= compute_generic_domination(@entry, true)
161
- end
162
-
163
- def postdominators
164
- @postdominators ||= compute_generic_domination(@exit, false)
165
- end
166
-
167
- # See also {#dominators} for references.
168
- def identify_loops
169
- loops = Hash.new { |h,k| h[k] = Set.new }
170
-
171
- dom = dominators
172
-
173
- @nodes.each do |node|
174
- node.targets.each do |target|
175
- # Back edges
176
- # A back edge of a natural loop is one whose
177
- # target dominates its source.
178
- if dom[node].include? target
179
- loops[target].add node
180
- end
181
- end
182
- end
183
-
184
- # At this point, +loops+ contains a list of all nodes
185
- # which have a back edge to the loop header. Expand
186
- # it to the list of all nodes in the loop.
187
- loops.each do |header, nodes|
188
- # Natural loop
189
- # The natural loop of a back edge (m→n), where
190
- # n dominates m, is the set of nodes x such that n
191
- # dominates x and there is a path from x to m not
192
- # containing n.
193
- pre_header = dom[header]
194
- all_nodes = Set[header]
195
-
196
- nodes.each do |node|
197
- all_nodes.merge(dom[node] - pre_header)
198
- end
199
-
200
- nodes.replace all_nodes
201
- end
202
-
203
- loops.default = nil
204
- loops
205
- end
206
-
207
- def sources_for(node, exceptions=false)
26
+ def sources_for(node, find_exceptions=false)
208
27
  unless @source_map
209
28
  @source_map = Hash.new { |h, k| h[k] = [] }
210
29
  @exception_source_map = Hash.new { |h, k| h[k] = [] }
@@ -226,7 +45,7 @@ module Furnace::CFG
226
45
  end
227
46
  end
228
47
 
229
- if exceptions
48
+ if find_exceptions
230
49
  @exception_source_map[node]
231
50
  else
232
51
  @source_map[node]
@@ -237,8 +56,7 @@ module Furnace::CFG
237
56
  @source_map = nil
238
57
  @label_map.clear
239
58
 
240
- @dominators = nil
241
- @postdominators = nil
59
+ super if defined?(super)
242
60
  end
243
61
 
244
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.3.0.beta1"
2
+ VERSION = "0.3.0.beta2"
3
3
  end