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 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
+ -r {Furnace} -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,23 @@
1
- require "bundler/gem_tasks"
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
+ require 'bacon'
9
+ Bacon.summary_at_exit
10
+ Dir["test/**/*_test.rb"].each do |file|
11
+ load file
12
+ end
13
+ end
14
+
15
+ PAGES_REPO = 'git@github.com:whitequark/furnace'
16
+
17
+ desc "Build and deploy documentation to GitHub pages"
18
+ task :pages do
19
+ system "git clone #{PAGES_REPO} gh-temp/ -b gh-pages; rm gh-temp/* -rf; touch gh-temp/.nojekyll" or abort
20
+ system "yardoc -o gh-temp/; cp gh-temp/frames.html gh-temp/index.html; sed s,index.html,_index.html, -i gh-temp/index.html" or abort
21
+ system "cd gh-temp/; git add -A; git commit -m 'Updated pages.'; git push -f origin gh-pages" or abort
22
+ FileUtils.rm_rf 'gh-temp'
23
+ end
data/furnace.gemspec CHANGED
@@ -16,4 +16,9 @@ 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'
22
+ s.add_development_dependency 'yard'
23
+ s.add_development_dependency 'redcarpet'
19
24
  end
@@ -1,36 +1,121 @@
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_accessor :type, :children, :metadata
4
-
5
- def initialize(type, children=[], metadata={})
6
- @type, @children, @metadata = type.to_sym, children, metadata
39
+ # Returns the type of this node.
40
+ # @return [Symbol]
41
+ attr_reader :type
42
+
43
+ # Returns the children of this node.
44
+ # The returned value is frozen.
45
+ # @return [Array]
46
+ attr_reader :children
47
+
48
+ # Constructs a new instance of Node.
49
+ #
50
+ # The arguments `type` and `children` are converted with `to_sym` and
51
+ # `to_a` respectively. Additionally, the result of converting `children`
52
+ # is frozen. While mutating the arguments is generally considered harmful,
53
+ # the most common case is to pass an array literal to the constructor. If
54
+ # your code does not expect the argument to be frozen, use `#dup`.
55
+ #
56
+ # The `properties` hash is passed to {#assign_properties}.
57
+ def initialize(type, children=[], properties={})
58
+ @type, @children = type.to_sym, children.to_a.freeze
59
+
60
+ assign_properties(properties)
61
+
62
+ freeze
7
63
  end
8
64
 
9
- def update(type, children=nil, metadata={})
10
- @type = type
11
- @children = children || @children
12
-
13
- # If something non-nil is passed, including default value, then merge.
14
- # Else, clear metadata store.
15
- if metadata
16
- @metadata.merge!(metadata)
17
- else
18
- @metadata = {}
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
+ #
71
+ # @return [nil]
72
+ def assign_properties(properties)
73
+ properties.each do |name, value|
74
+ instance_variable_set :"@#{name}", value
19
75
  end
20
76
 
21
- self
77
+ nil
22
78
  end
23
-
24
- def dup
25
- node = super
26
- node.children = @children.dup
27
- node.metadata = @metadata.dup
28
- node
79
+ protected :assign_properties
80
+
81
+ protected :dup
82
+
83
+ # Returns a new instance of Node where non-nil arguments replace the
84
+ # corresponding fields of `self`.
85
+ #
86
+ # For example, `Node.new(:foo, [ 1, 2 ]).updated(:bar)` would yield
87
+ # `(bar 1 2)`, and `Node.new(:foo, [ 1, 2 ]).updated(nil, [])` would
88
+ # yield `(foo)`.
89
+ #
90
+ # If the resulting node would be identical to `self`, does nothing.
91
+ #
92
+ # @param [Symbol, nil] type
93
+ # @param [Array, nil] children
94
+ # @param [Hash, nil] properties
95
+ # @return [AST::Node]
96
+ def updated(type=nil, children=nil, properties=nil)
97
+ new_type = type || @type
98
+ new_children = children || @children
99
+ new_properties = properties || {}
100
+
101
+ if @type == new_type &&
102
+ @children == new_children &&
103
+ properties.nil?
104
+ self
105
+ else
106
+ dup.send :initialize, new_type, new_children, new_properties
107
+ end
29
108
  end
30
109
 
110
+ # Compares `self` to `other`, possibly converting with `to_ast`. Only
111
+ # `type` and `children` are compared; metadata is deliberately ignored.
112
+ #
113
+ # @return [Boolean]
31
114
  def ==(other)
32
- if other.respond_to? :to_astlet
33
- other = other.to_astlet
115
+ if equal?(other)
116
+ true
117
+ elsif other.respond_to? :to_ast
118
+ other = other.to_ast
34
119
  other.type == self.type &&
35
120
  other.children == self.children
36
121
  else
@@ -38,63 +123,65 @@ module Furnace::AST
38
123
  end
39
124
  end
40
125
 
126
+ # Converts `self` to a concise s-expression, omitting any children.
127
+ #
128
+ # @return [String]
41
129
  def to_s
42
130
  "(#{fancy_type} ...)"
43
131
  end
44
132
 
133
+ # Converts `self` to a pretty-printed s-expression.
134
+ #
135
+ # @param [Integer] indent Base indentation level.
136
+ # @return [String]
45
137
  def to_sexp(indent=0)
46
- str = "#{" " * indent}(#{fancy_type}"
47
-
48
- if @metadata[:ellipsis]
49
- str << " <omitted>)"
50
-
51
- return str
52
- end
53
-
54
- children.each do |child|
55
- if (!children[0].is_a?(Node) && child.is_a?(Node)) ||
56
- (children[0].is_a?(Node) && child.is_a?(Node) &&
57
- child.children.any? { |c| c.is_a?(Node) || c.is_a?(Array) }) ||
58
- (child.is_a?(Node) && child.metadata[:label])
59
- str << "\n#{child.to_sexp(indent + 1)}"
138
+ indented = " " * indent
139
+ sexp = "#{indented}(#{fancy_type}"
140
+
141
+ first_node_child = children.index do |child|
142
+ child.is_a?(Node) || child.is_a?(Array)
143
+ end || children.count
144
+
145
+ children.each_with_index do |child, idx|
146
+ if child.is_a?(Node) && idx >= first_node_child
147
+ sexp << "\n#{child.to_sexp(indent + 1)}"
148
+ elsif child.is_a?(Hash)
149
+ sexp << " {\n"
150
+ child.each do |key, value|
151
+ if value.is_a?(Node)
152
+ pretty_value = value.to_sexp(indent + 2).lstrip
153
+ else
154
+ pretty_value = value.inspect
155
+ end
156
+
157
+ sexp << "#{indented} #{key.inspect} => #{pretty_value}\n"
158
+ end
159
+ sexp << "#{indented} }"
60
160
  else
61
- str << " #{child.inspect}"
161
+ sexp << " #{child.inspect}"
62
162
  end
63
163
  end
64
164
 
65
- str << ")"
165
+ sexp << ")"
66
166
 
67
- str
167
+ sexp
68
168
  end
69
169
  alias :inspect :to_sexp
70
170
 
71
- def to_astlet
171
+ # @return [AST::Node] self
172
+ def to_ast
72
173
  self
73
174
  end
74
175
 
75
176
  protected
76
177
 
178
+ # Returns `@type` with all underscores replaced by dashes. This allows
179
+ # to write symbol literals without quotes in Ruby sources and yet have
180
+ # nicely looking s-expressions.
181
+ #
182
+ # @return [String]
77
183
  def fancy_type
78
- dasherized = @type.to_s.gsub('_', '-')
79
-
80
- if @metadata.any?
81
- metainfo = @metadata.dup
82
- metainfo.delete :label
83
- metainfo.delete :origin
84
- metainfo.delete :ellipsis
85
-
86
- if metainfo.any?
87
- metainfo = "#{metainfo.inspect}:"
88
- else
89
- metainfo = nil
90
- end
91
- end
92
-
93
- if @metadata[:label]
94
- "#{@metadata[:label]}:#{metainfo}#{dasherized}"
95
- else
96
- "#{metainfo}#{dasherized}"
97
- end
184
+ @type.to_s.gsub('_', '-')
98
185
  end
99
186
  end
100
187
  end
@@ -0,0 +1,265 @@
1
+ module Furnace::AST
2
+ # Processor is a class which helps transforming one AST into another.
3
+ # In a nutshell, the {#process} method accepts a {Node} and dispatches
4
+ # it to a handler corresponding to its type, and returns a (possibly)
5
+ # updated variant of the node.
6
+ #
7
+ # Processor has a set of associated design patterns. They are best
8
+ # explained with a concrete example. Let's define a simple arithmetic
9
+ # language and an AST format for it:
10
+ #
11
+ # Terminals (AST nodes which do not have other AST nodes inside):
12
+ #
13
+ # * `(integer <int-literal>)`,
14
+ #
15
+ # Nonterminals (AST nodes with other nodes as children):
16
+ #
17
+ # * `(add <node> <node>)`,
18
+ # * `(multiply <node> <node>)`,
19
+ # * `(divide <node> <node>)`,
20
+ # * `(negate <node>)`,
21
+ # * `(store <node> <string-literal>)`: stores value of `<node>` into a variable named `<string-literal>`,
22
+ # * `(load <string-literal>)`: loads value of a variable named `<string-literal>`,
23
+ # * `(each <node> ...): computes each of the `<node>`s and prints the result.
24
+ #
25
+ # Furnace AST nodes all 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 'furnace'
33
+ # include Furnace
34
+ #
35
+ # class ArithmeticsProcessor < AST::Processor
36
+ # # This method traverses any binary operators such as (add) or (multiply).
37
+ # def process_binary_op(node)
38
+ # # Children aren't decomposed automatically; it is suggested to use Ruby
39
+ # # multiple assignment expansion, as it is very convenient here.
40
+ # left_expr, right_expr = node.children
41
+ #
42
+ # # AST::Node#updated won't change node type if nil is passed as a first
43
+ # # argument, which allows to reuse the same handler for multiple node types
44
+ # # using `alias' (below).
45
+ # node.updated(nil, [
46
+ # process(left_expr),
47
+ # process(right_expr)
48
+ # ])
49
+ # end
50
+ # alias on_add process_binary_op
51
+ # alias on_multiply process_binary_op
52
+ # alias on_divide process_binary_op
53
+ #
54
+ # def on_negate(node)
55
+ # # It is also possible to use #process_all for more compact code
56
+ # # if every child is a Node.
57
+ # node.updated(nil, process_all(node.children))
58
+ # end
59
+ #
60
+ # def on_store(node)
61
+ # expr, variable_name = node.children
62
+ #
63
+ # # Note that variable_name is not a Node and thus isn't passed to #process.
64
+ # node.updated(nil, [
65
+ # process(expr),
66
+ # variable_name
67
+ # ])
68
+ # end
69
+ #
70
+ # # (load) is effectively a terminal node, and so it does not need
71
+ # # an explicit handler, as the following is the default behavior.
72
+ # def on_load(node)
73
+ # nil
74
+ # end
75
+ #
76
+ # def on_each(node)
77
+ # node.updated(nil, process_all(node.children))
78
+ # end
79
+ # end
80
+ #
81
+ # Let's test our ArithmeticsProcessor:
82
+ #
83
+ # include AST::Sexp
84
+ # expr = s(:add, s(:integer, 2), s(:integer, 2))
85
+ #
86
+ # p ArithmeticsProcessor.new.process(expr) == expr # => true
87
+ #
88
+ # As expected, it does not change anything at all. This isn't actually
89
+ # very useful, so let's now define a Calculator, which will compute the
90
+ # expression values:
91
+ #
92
+ # # This Processor folds nonterminal nodes and returns an (integer)
93
+ # # terminal node.
94
+ # class ArithmeticsCalculator < ArithmeticsProcessor
95
+ # def compute_op(node)
96
+ # # First, node children are processed and then unpacked to local
97
+ # # variables.
98
+ # nodes = process_all(node.children)
99
+ #
100
+ # if nodes.all? { |node| node.type == :integer }
101
+ # # If each of those nodes represents a literal, we can fold this
102
+ # # node!
103
+ # values = nodes.map { |node| node.children.first }
104
+ # AST::Node.new(:integer, [
105
+ # yield(values)
106
+ # ])
107
+ # else
108
+ # # Otherwise, we can just leave the current node in the tree and
109
+ # # only update it with processed children nodes, which can be
110
+ # # partially folded.
111
+ # node.updated(nil, nodes)
112
+ # end
113
+ # end
114
+ #
115
+ # def on_add(node)
116
+ # compute_op(node) { |left, right| left + right }
117
+ # end
118
+ #
119
+ # def on_multiply(node)
120
+ # compute_op(node) { |left, right| left * right }
121
+ # end
122
+ # end
123
+ #
124
+ # Let's check:
125
+ #
126
+ # p ArithmeticsCalculator.new.process(expr) # => (integer 4)
127
+ #
128
+ # Excellent, the calculator works! Now, a careful reader could notice that
129
+ # the ArithmeticsCalculator does not know how to divide numbers. What if we
130
+ # pass an expression with division to it?
131
+ #
132
+ # expr_with_division = \
133
+ # s(:add,
134
+ # s(:integer, 1),
135
+ # s(:divide,
136
+ # s(:add, s(:integer, 8), s(:integer, 4)),
137
+ # s(:integer, 3))) # 1 + (8 + 4) / 3
138
+ #
139
+ # folded_expr_with_division = ArithmeticsCalculator.new.process(expr_with_division)
140
+ # p folded_expr_with_division
141
+ # # => (add
142
+ # # (integer 1)
143
+ # # (divide
144
+ # # (integer 12)
145
+ # # (integer 3)))
146
+ #
147
+ # As you can see, the expression was folded _partially_: the inner `(add)` node which
148
+ # could be computed was folded to `(integer 12)`, the `(divide)` node is left as-is
149
+ # because there is no computing handler for it, and the root `(add)` node was also left
150
+ # as it is because some of its children were not literals.
151
+ #
152
+ # Note that this partial folding is only possible because the _data_ format, i.e.
153
+ # the format in which the computed values of the nodes are represented, is the same as
154
+ # the AST itself.
155
+ #
156
+ # Let's extend our ArithmeticsCalculator class further.
157
+ #
158
+ # class ArithmeticsCalculator
159
+ # def on_divide(node)
160
+ # compute_op(node) { |left, right| left / right }
161
+ # end
162
+ #
163
+ # def on_negate(node)
164
+ # # Note how #compute_op works regardless of the operator arity.
165
+ # compute_op(node) { |value| -value }
166
+ # end
167
+ # end
168
+ #
169
+ # Now, let's apply our renewed ArithmeticsCalculator to a partial result of previous
170
+ # evaluation:
171
+ #
172
+ # p ArithmeticsCalculator.new.process(expr_with_division) # => (integer 5)
173
+ #
174
+ # Five! Excellent. This is also pretty much how CRuby 1.8 executed its programs.
175
+ #
176
+ # Now, let's do some automated bug searching. Division by zero is an error, right?
177
+ # So if we could detect that someone has divided by zero before the program is even
178
+ # run, that could save some debugging time.
179
+ #
180
+ # class DivisionByZeroVerifier < ArithmeticsProcessor
181
+ # class VerificationFailure < Exception; end
182
+ #
183
+ # def on_divide(node)
184
+ # # You need to process the children to handle nested divisions
185
+ # # such as:
186
+ # # (divide
187
+ # # (integer 1)
188
+ # # (divide (integer 1) (integer 0))
189
+ # left, right = process_all(node.children)
190
+ #
191
+ # if right.type == :integer &&
192
+ # right.children.first == 0
193
+ # raise VerificationFailure, "Ouch! This code divides by zero."
194
+ # end
195
+ # end
196
+ #
197
+ # def divides_by_zero?(ast)
198
+ # process(ast)
199
+ # false
200
+ # rescue VerificationFailure
201
+ # true
202
+ # end
203
+ # end
204
+ #
205
+ # nice_expr = \
206
+ # s(:divide,
207
+ # s(:add, s(:integer, 10), s(:integer, 2)),
208
+ # s(:integer, 4))
209
+ #
210
+ # p DivisionByZeroVerifier.new.divides_by_zero?(nice_expr)
211
+ # # => false. Good.
212
+ #
213
+ # bad_expr = \
214
+ # s(:add, s(:integer, 10),
215
+ # s(:divide, s(:integer, 1), s(:integer, 0)))
216
+ #
217
+ # p DivisionByZeroVerifier.new.divides_by_zero?(bad_expr)
218
+ # # => true. WHOOPS. DO NOT RUN THIS.
219
+ #
220
+ # Of course, this won't detect more complex cases... unless you use some partial
221
+ # evaluation before! The possibilites are endless. Have fun.
222
+ class Processor
223
+ # Dispatches `node`. If a node has type `:foo`, then a handler named
224
+ # `on_foo` is invoked with one argument, the `node`; if there isn't
225
+ # such a handler, {#handler_missing} is invoked with the same argument.
226
+ #
227
+ # If the handler returns `nil`, `node` is returned; otherwise, the return
228
+ # value of the handler is passed along.
229
+ #
230
+ # @param [AST::Node, nil] node
231
+ # @return [AST::Node]
232
+ def process(node)
233
+ node = node.to_ast
234
+
235
+ # Invoke a specific handler
236
+ on_handler = :"on_#{node.type}"
237
+ if respond_to? on_handler
238
+ new_node = send on_handler, node
239
+ else
240
+ new_node = handler_missing(node)
241
+ end
242
+
243
+ node = new_node if new_node
244
+
245
+ node
246
+ end
247
+
248
+ # {#process}es each node from `nodes` and returns an array of results.
249
+ #
250
+ # @param [Array<AST::Node>] nodes
251
+ # @return [Array<AST::Node>]
252
+ def process_all(nodes)
253
+ nodes.map do |node|
254
+ process node
255
+ end
256
+ end
257
+
258
+ # Default handler. Does nothing.
259
+ #
260
+ # @param [AST::Node] node
261
+ # @return [AST::Node, nil]
262
+ def handler_missing(node)
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,30 @@
1
+ module Furnace::AST
2
+ # This simple module is very useful in the cases where one needs
3
+ # to define deeply nested ASTs from Ruby code, for example, in
4
+ # tests. It should be used like this:
5
+ #
6
+ # describe YourLanguage::AST do
7
+ # include Sexp
8
+ #
9
+ # it "should correctly parse expressions" do
10
+ # YourLanguage.parse("1 + 2 * 3").should ==
11
+ # s(:add,
12
+ # s(:integer, 1),
13
+ # s(:multiply,
14
+ # s(:integer, 2),
15
+ # s(:integer, 3)))
16
+ # end
17
+ # end
18
+ #
19
+ # This way the amount of boilerplate code is greatly reduced.
20
+ module Sexp
21
+ # Creates a {Node} with type `type` and children `children`.
22
+ # Note that the resulting node is of the type AST::Node and not a
23
+ # subclass.
24
+ # This would not pose a problem with comparisons, as {Node#==}
25
+ # ignores metadata.
26
+ def s(type, *children)
27
+ Node.new(type, children)
28
+ end
29
+ end
30
+ end
data/lib/furnace/ast.rb CHANGED
@@ -1,9 +1,19 @@
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}, {Processor} and {Sexp} 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
+ require_relative "ast/sexp"
19
+ end