heist 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module Heist
2
+ class Runtime
3
+
4
+ # The +Syntax+ class is used to model built-in special forms. +Syntax+
5
+ # functions are universally implemented in Ruby; user-defined special
6
+ # forms are represented using +Macro+. +Syntax+ is very simple: its
7
+ # body is a Ruby block that accepts a +Scope+ and a +Cons+ list of
8
+ # the expression following the special form, and the +Syntax+ class
9
+ # does not automatically evaluate any parameters. It is up to the
10
+ # Ruby code implementing the syntax to decide what to evaluate. For
11
+ # example, here's a couple of implementations for Scheme's <tt>(if)</tt>
12
+ # and <tt>(set!)</tt>.
13
+ #
14
+ # env = Scope.new
15
+ #
16
+ # # e.g. (set! x (+ 9 4))
17
+ # env['set!'] = Syntax.new(env) do |scope, cells|
18
+ # value = Heist.evaluate(cells.cdr.car, scope)
19
+ # scope.set!(cells.car, value)
20
+ # end
21
+ #
22
+ # # e.g. (if (> 6 3) 'yes 'no)
23
+ # env['if'] = Syntax.new(env) do |scope, cells|
24
+ # which = Heist.evaluate(cells.car, scope) ? cells.cdr : cells.cdr.cdr
25
+ # Heist.evaluate(which.car, scope)
26
+ # end
27
+ #
28
+ class Syntax < Function
29
+
30
+ # Calls the Ruby implementation of the +Syntax+ and returns the result.
31
+ def call(scope, cells)
32
+ @body.call(scope, cells)
33
+ end
34
+
35
+ # Returns a string placeholder for the +Syntax+, containing its
36
+ # name if it has one.
37
+ def to_s
38
+ "#<syntax:#{ @name }>"
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,234 @@
1
+ module Heist
2
+ class Runtime
3
+
4
+ # +Cons+ is the basic data structure element in Scheme. A +Cons+ is an
5
+ # object with two pointers to other objects, the pointers being called
6
+ # the +car+ and the +cdr+. You can typically think of this type of
7
+ # object as representing a pair of values, though not all +Cons+ objects
8
+ # are pairs according to the <tt>(pair?)</tt> procedure; specifically
9
+ # +NULL+ is not considered a pair.
10
+ #
11
+ # +Cons+ objects can be joined together to form lists; a list is either
12
+ # the empty list or a +Cons+ whose +cdr+ is a list. That is, a list is
13
+ # chain of +Cons+ pairs joined by their +cdr+ fields, whose +car+ fields
14
+ # hold the elements of the list. A list is considered 'proper' if its
15
+ # final +Cons+ is +NULL+, and improper otherwise. Some examples:
16
+ #
17
+ # Proper list: (1 2 3) Improper list: (1 2 3 . 4)
18
+ #
19
+ # # # # = cons
20
+ # / \\ / \\ / = car
21
+ # 1 # 1 # \\ = cdr
22
+ # / \\ / \\ () = NULL
23
+ # 2 # 2 #
24
+ # / \\ / \\
25
+ # 3 () 3 4
26
+ #
27
+ # +Cons+ objects are +Enumerable+, though always keep in mind that a
28
+ # +Cons+ does not 'contain' a whole list, it contains one value and a
29
+ # pointer to the rest of the list. Iterating on a +Cons+ involves
30
+ # walking this object graph.
31
+
32
+ class Cons
33
+ include Enumerable
34
+ include Expression
35
+
36
+ attr_accessor :car, :cdr
37
+
38
+ # +NULL+ is a special +Cons+ instance representing the empty list, which
39
+ # is used as a nil value in Scheme. The empty list is a singleton: there
40
+ # is only ever one empty list in memory, and the Scheme expressions
41
+ # <tt>'()</tt> and <tt>(list)</tt> always return the same object. +NULL+
42
+ # is frozen, and its +car+ and +cdr+ are pointers back to itself.
43
+ NULL = self.new
44
+ NULL.car = NULL.cdr = NULL
45
+
46
+ # +NULL+ is only equal to itself.
47
+ def NULL.==(other); equal?(other); end
48
+ NULL.freeze
49
+
50
+ cadr_combos = (1..4).map { |n|
51
+ (0...(2**n)).map do |x|
52
+ 'c' + ("%0#{n}d" % x.to_s(2)).gsub('0', 'a').gsub('1', 'd') + 'r'
53
+ end
54
+ }.flatten
55
+
56
+ # An array of all the c[ad]+r functions supported by Heist
57
+ ACCESSORS = cadr_combos
58
+
59
+ class << self
60
+ # Creates a new list from the elements of the enumerable object +enum+,
61
+ # and returns the +Cons+ that forms the head of the list. If +enum+ is
62
+ # empty, +NULL+ is returned. +construct+ optionally takes a block that
63
+ # can be used to transform each value as the list is constructed:
64
+ #
65
+ # Cons.construct([1,2,3,4]) { |x| x*x }
66
+ # #=> (1 4 9 16)
67
+ #
68
+ # The method also takes an optional second parameter +linking+, which,
69
+ # if set to true, makes each +car+ element aware of the +Cons+ that
70
+ # holds it. This is used when inlining macro expansions, where
71
+ # expressions need to replace themselves in their parent expression.
72
+ #
73
+ # Note that this method copies improper lists correctly, though
74
+ # iteration over a +Cons+ list does not include the tail value.
75
+ #
76
+ def construct(enum, linking = false, &block)
77
+ return NULL if enum.nil?
78
+ root, last = nil, nil
79
+ enum.each do |value|
80
+ value = block.call(value) if block_given?
81
+ pair = self.new(value)
82
+ pair.hosts(value) if linking
83
+ root ||= pair
84
+ last.cdr = pair if last
85
+ last = pair.tail
86
+ end
87
+ last.cdr = enum.tail.cdr if last and Cons === enum
88
+ root || NULL
89
+ end
90
+ alias :[] :construct
91
+ end
92
+
93
+ # A +Cons+ is initialized using a +car+ and a +cdr+ value.
94
+ def initialize(car = nil, cdr = nil)
95
+ self.car = car
96
+ self.cdr = cdr
97
+ end
98
+
99
+ ACCESSORS[2..-1].each do |accsr|
100
+ class_eval <<-EOS
101
+ def #{ accsr }
102
+ #{ accsr.scan(/[ad]/).reverse.map { |s| "c#{s}r" } * '.' }
103
+ end
104
+ EOS
105
+ end
106
+
107
+ # Sets the +car+ of the receiving +Cons+, unless it is frozen. If the
108
+ # given value is +nil+, +NULL+ is used instead.
109
+ def car=(car)
110
+ raise ImmutableError.new("Cannot modify constant value") if frozen?
111
+ @car = car.nil? ? NULL : car
112
+ end
113
+
114
+ # Sets the +cdr+ of the receiving +Cons+, unless it is frozen. If the
115
+ # given value is +nil+, +NULL+ is used instead.
116
+ def cdr=(cdr)
117
+ raise ImmutableError.new("Cannot modify constant value") if frozen?
118
+ @cdr = cdr.nil? ? NULL : cdr
119
+ end
120
+
121
+ # Recursively freezes the +Cons+ and any conses it points to. This is
122
+ # used to prevent quoted list constants from being modified since they
123
+ # are part of the parse tree: quoting a list does not allocate a new
124
+ # object so changing a quoted list would have undesirable side effects
125
+ # on any other references to the list.
126
+ def freeze!
127
+ return if null?
128
+ [@car, @cdr].each { |slot| slot.freeze! if slot.respond_to?(:freeze!) }
129
+ freeze
130
+ end
131
+
132
+ # Sets the parent pair of +value+ to the receiver. Some +car+ values
133
+ # need a reference back to their containing +Cons+ for concerns such
134
+ # as macro expansion inlining.
135
+ def hosts(value)
136
+ value.parent = self if Expression === value and value != NULL
137
+ end
138
+
139
+ # Creates and returns a copy of the +Cons+ list, taking an optional
140
+ # mapping block (see +construct+).
141
+ def clone(&block)
142
+ Cons.construct(self, &block)
143
+ end
144
+
145
+ # Iterates over each +Cons+ in a list, yielding the +car+ value for
146
+ # each +Cons+. Returns the final +Cons+ in the list, from where we
147
+ # can inspect the tail's +cdr+ to see if the list if proper or not.
148
+ # The block is optional and the method is aliased as +tail+, so to
149
+ # get the tail value of an improper list you'd call <tt>list.tail.cdr</tt>.
150
+ def each
151
+ pair, tail = self, NULL
152
+ while Cons === pair and not pair.null?
153
+ yield(pair.car) if block_given?
154
+ tail = pair
155
+ pair = pair.cdr
156
+ end
157
+ tail
158
+ end
159
+ alias :tail :each
160
+
161
+ # Returns the length of the list whose head is the receiving +Cons+.
162
+ # If the list is improper an exception is raised.
163
+ def length
164
+ size = 0
165
+ tail = each { |value| size += 1 }
166
+ raise TypeError.new("Cannot get the length of improper list #{self}") unless tail.cdr == NULL
167
+ size
168
+ end
169
+ alias :size :length
170
+
171
+ # Returns +true+ iff +other+ is equal to the receiver, that is to
172
+ # say that +other+ is a +Cons+ with the same +car+ and +cdr+ as the
173
+ # receiving +Cons+.
174
+ def ==(other)
175
+ return false unless Cons === other
176
+ return false if NULL == other
177
+ @car == other.car and @cdr == other.cdr
178
+ end
179
+
180
+ # If the receiving list contains +Binding+ objects, said objects are
181
+ # forced to evaluate themselves to populate the list.
182
+ def force!
183
+ return self unless Binding === @car
184
+ pair = self
185
+ while not pair.null?
186
+ pair.car = pair.car.force!
187
+ pair = pair.cdr
188
+ end
189
+ self
190
+ end
191
+
192
+ # Returns +true+ iff the receiving +Cons+ is the head of a proper list.
193
+ def list?
194
+ tail.cdr == NULL; end
195
+
196
+ # Returns +true+ iff the receiving +Cons+ is a valid pair.
197
+ def pair?
198
+ not null?; end
199
+
200
+ # Returns +true+ iff the receiving +Cons+ is the empty list.
201
+ def null?
202
+ self == NULL; end
203
+
204
+ # Returns an array representation of the list. If +deep+ is +true+,
205
+ # the array conversion is performed recursively.
206
+ def to_a(deep = false)
207
+ map { |cell| deep && Cons === cell ? cell.to_a : cell }
208
+ end
209
+
210
+ # Returns a pure Ruby representation of the list, with any Heist
211
+ # specific objects converted to basic Ruby equivalents.
212
+ def to_ruby
213
+ map { |cell| cell.respond_to?(:to_ruby) ? cell.to_ruby : cell }
214
+ end
215
+
216
+ # Returns a Scheme-style string representation of the list.
217
+ def to_s
218
+ strings = []
219
+ tail = each { |value|
220
+ strings << case value
221
+ when String then value.inspect
222
+ when Symbol then "'#{value}"
223
+ else value
224
+ end
225
+ }.cdr
226
+ '(' + (strings * ' ') +
227
+ (tail == NULL ? '' : ' . ' + tail.to_s) + ')'
228
+ end
229
+ alias :inspect :to_s
230
+ end
231
+
232
+ end
233
+ end
234
+
@@ -1,18 +1,27 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # Classes may mix in the +Expression+ module to signal that they represent
5
+ # sections of code that may be evaluated. This is mostly a flag module
6
+ # and does not provide much in the way of extensibility, since the evaluator
7
+ # needs to know how to execute all the kinds of expressions you want it
8
+ # to deal with (for reasons such as tail recursion, expressions are not
9
+ # responsible for evaluating themselves).
4
10
  module Expression
5
- attr_reader :parent, :index
6
-
7
- def exists_at!(parent, index)
8
- @parent, @index = parent, index
9
- end
11
+ attr_accessor :parent
10
12
 
13
+ # Replaces the receiver with +expression+ in the receiver's parent
14
+ # expression. This is used as part of the macro expansion process.
11
15
  def replace(expression)
12
16
  return unless @parent
13
- @parent[@index] = expression
17
+ @parent.car = expression
18
+ @parent.hosts(expression)
14
19
  end
15
20
 
21
+ # Returns the result of evaluating the receiving +Expression+ in the
22
+ # given +scope+. Works by pushing the receiver and scope onto the
23
+ # runtime stack (could be a +Stack+ or +Stackless+), which then
24
+ # evaluates the expression using a trampoline.
16
25
  def eval(scope)
17
26
  scope.runtime.stack << Frame.new(self, scope)
18
27
  end
@@ -1,17 +1,34 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # An +Identifier+ is used to represent any name that appears in Scheme
5
+ # code. We might throw it away and use symbols at some point. I had this
6
+ # idea for storing metadata on +Identifier+ objects to speed things up
7
+ # but it pretty much came to nothing. Its one saving grace is that it
8
+ # needs to be treated as an +Expression+ class and I don't want to
9
+ # modify core Ruby classes.
4
10
  class Identifier
5
11
  include Expression
6
12
 
7
13
  extend Forwardable
8
- def_delegators(:@metadata, :[], :[]=)
9
14
  def_delegators(:@name, :to_s)
10
15
  alias :inspect :to_s
11
16
 
17
+ # An +Identifier+ is initialized using a string that becomes the
18
+ # name of the identifier.
12
19
  def initialize(name)
13
20
  @name = name.to_s
14
- @metadata = {}
21
+ end
22
+
23
+ # Returns +true+ if the receiver has the same name as the argument.
24
+ def ==(other)
25
+ Identifier === other and @name == other.to_s
26
+ end
27
+
28
+ # Returns a raw Ruby representation of the identifier, for which
29
+ # we use symbols.
30
+ def to_ruby
31
+ @name.to_s.to_sym
15
32
  end
16
33
  end
17
34
 
data/lib/runtime/frame.rb CHANGED
@@ -1,115 +1,182 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # +Frame+ is used by the +Stack+ class to encapsulate a single +Expression+
5
+ # bound to a +Scope+. +Frame+ objects evaluate expressions one sub-expression
6
+ # at a time, providing fine-grained opportunity for inspecting intermediate
7
+ # results and supporting the Scheme notion of continuations.
8
+ #
9
+ # The state of a +Frame+ is held in five variables: <tt>@expression</tt>,
10
+ # <tt>@scope</tt>, <tt>@values</tt>, <tt>@current</tt> and <tt>@complete</tt>.
11
+ # <tt>@expression</tt> is the +Expression+ object the +Frame+ is evaluating,
12
+ # and <tt>@scope</tt> is a +Scope+ object in which to evaluate it. <tt>@values</tt>
13
+ # is a copy of <tt>@expression</tt> that is used to store the values returned
14
+ # by the subexpressions; it represents the partially evaluated state of the
15
+ # expression for use when saving a +Stack+ when creating a +Continuation+.
16
+ # <tt>@current</tt> is a pointer to the subexpression that is to be evaluated
17
+ # next, and <tt>@complete</tt> is a boolean that indicates whether all the
18
+ # required subexpressions have been evaluated.
19
+ #
20
+ # See +Stack+ for a fuller explanation of how expressions are evaluated.
21
+ #
4
22
  class Frame
5
23
  attr_reader :expression, :scope
6
24
 
25
+ # A +Frame+ is initialized using an +Expression+ and a +Scope+, which
26
+ # are simply stored as instance variables. No evaluation takes place
27
+ # upon initialization.
7
28
  def initialize(expression, scope)
8
29
  reset!(expression)
9
30
  @scope = scope
10
31
  end
11
32
 
33
+ # Returns +true+ iff the +Frame+ has completed evaluating its expression.
12
34
  def complete?
13
35
  @complete
14
36
  end
15
37
 
38
+ # Processes a single subexpression from the +Frame+ and returns the
39
+ # result. If all the required subexpressions have been evaluated, we
40
+ # call the resulting function with the arguments and return the result
41
+ # of that call.
16
42
  def process!
17
43
  case @expression
18
44
 
19
- when List then
20
- if Syntax === @values.first or @values.size == @expression.size
45
+ when Cons then
46
+ # If we have evaluated all the subexpressions, or we have a
47
+ # +Syntax+ value first in the list, we can make a function call
48
+ # and return its result.
49
+ function = @values.car
50
+ if Syntax === function or @current.null?
21
51
  @complete = true
22
- merge!
23
- raise SyntaxError.new("Invalid expression: #{@expression}") unless Function === @data.first
24
- result = @data.first.call(@scope, @data[1..-1])
52
+ raise SyntaxError.new("Invalid expression: #{@expression}") unless Function === function
53
+ result = function.call(@scope, @values.cdr)
54
+
55
+ # If the result of the call is a macro expansion, inline it
56
+ # and set its expression as the new expression for the frame
25
57
  return result unless Macro::Expansion === result
26
58
  return reset!(result.expression, true)
27
59
  end
28
60
 
61
+ # Otherwise, we still have work to do on the subexpressions, so
62
+ # we evaluate the next one in the list.
29
63
  stack = @scope.runtime.stack
30
- stack << Frame.new(@expression[@values.size], @scope)
64
+ stack << Frame.new(@current.car, @scope)
31
65
 
66
+ # If the expression is an +Identifier+, just look it up.
32
67
  when Identifier then
33
68
  @complete = true
34
69
  @scope[@expression]
35
70
 
71
+ # Otherwise, the expression is just data so return it.
36
72
  else
37
73
  @complete = true
38
74
  Heist.evaluate(@expression, @scope)
39
75
  end
40
76
  end
41
77
 
42
- def dup
43
- copy, values = super, @values.dup
78
+ # Returns a copy of the +Frame+. The <tt>@values</tt> variable is also
79
+ # copied by this method so that state changes do not transfer from the
80
+ # copy to the original.
81
+ def clone
82
+ copy, values = super, @values.clone
44
83
  copy.instance_eval { @values = values }
45
84
  copy
46
85
  end
47
86
 
87
+ # Fills the hole corresponding to +subexpr+ in the receiving +Frame+ with
88
+ # +value+. When a hole is filled, the <tt>@current</tt> pointer is moved
89
+ # to the next subexpression that needs evaluating.
48
90
  def fill!(subexpr, value)
49
- subexpr ||= @expression[@values.size]
50
- @values << value
51
- @subexprs << subexpr
91
+ epair, vpair = @expression, @values
92
+ while Cons === epair and not epair.null?
93
+ if epair.car.equal?(subexpr)
94
+ vpair.car = value
95
+ @current = epair.cdr
96
+ end
97
+ epair, vpair = epair.cdr, vpair.cdr
98
+ end
52
99
  end
53
100
 
101
+ # Sets the replacement target for the receiving +Frame+ to the given
102
+ # +expression+. When the receiving frame returns, it will fill the
103
+ # hole corresponding to the +expression+ in the previous +Frame+ on
104
+ # the +Stack+.
54
105
  def replaces(expression)
55
106
  @target = expression
56
107
  end
57
108
 
109
+ # Returns the +Expression+ that is the replacement target for the
110
+ # previous +Frame+ on the +Stack+. When the receiving +Frame+ completes,
111
+ # the returned value will replace the return value of this method in
112
+ # the previous +Frame+. The +target+ of a +Frame+ is its <tt>@expression</tt>
113
+ # by default, but tail calls require us to change the target as, when
114
+ # a tail call returns, the +Frame+ that originated it will no longer
115
+ # by on the stack so +replaces+ is used to change the replacement
116
+ # target for the tail call.
58
117
  def target
59
118
  @target || @expression
60
119
  end
61
120
 
62
121
  private
63
122
 
64
- def merge!
65
- return @data = @values unless Syntax === @values.first
66
- @data = @expression.dup
67
- @expression.each_with_index do |expr, i|
68
- index = @subexprs.index(expr)
69
- @data[i] = @values[index] if index
70
- end
71
- end
72
-
123
+ # Resets the +Expression+ stored in the +Frame+. If the second argument
124
+ # is given as +true+, the given +expression+ replaces the previously
125
+ # stored expression in the source tree; this is used to inline macro
126
+ # expansions. Also resets the <tt>@values</tt> list to be a copy of the
127
+ # new +Expression+.
73
128
  def reset!(expression, replace = false)
74
129
  @expression.replace(expression) if replace
75
130
  @expression = expression
76
- @values = []
77
- @subexprs = []
131
+ @current = expression
132
+ @values = (Cons === expression) ? expression.clone : nil
78
133
  @complete = false
79
134
  end
80
135
  end
81
136
 
137
+ # +Body+ is a subclass of +Frame+, used for evaluating +Function+ bodies.
138
+ # Instead of providing break points between subexpressions in a single
139
+ # expression, it provides break points between whole expressions inside
140
+ # a Scheme procedure. (A 'break point' is a point in code execution during
141
+ # which +Stack+ can inspect the last value returned and decide whether to
142
+ # continue the current +Frame+ or switch to some other action.)
82
143
  class Body < Frame
144
+
145
+ # A +Body+ is initialized using a +Cons+ list containing a list of
146
+ # <tt>Expression</tt>s that make up the body of a +Function+, and a
147
+ # +Scope+ in which these expressions are to be evaluated.
83
148
  def initialize(expressions, scope)
84
149
  @expression = expressions
85
150
  @scope = scope
86
151
  @values = []
87
- @index = 0
88
152
  end
89
153
 
154
+ # Returns +true+ iff the +Body+ has evaluated all the expressions.
90
155
  def complete?
91
- @index == @expression.size
156
+ @expression.null?
92
157
  end
93
158
 
159
+ # Processes the next remaining +Expression+ from the +Function+ body,
160
+ # returning the result for inspection by the +Stack+.
94
161
  def process!
95
- expression = @expression[@index]
96
- @index += 1 # increment before evaluating the expression
97
- # so that when a continuation is saved we
98
- # resume from the following statement
162
+ expression = @expression.car
163
+
164
+ # Increment before evaluating the expression so that when a
165
+ # continuation is saved we resume from the following statement
166
+ @expression = @expression.cdr
99
167
 
100
- return Frame.new(expression, @scope) if @index == @expression.size
168
+ # Return the final expression as a +Frame+ to enable tail calls
169
+ return Frame.new(expression, @scope) if complete?
101
170
 
171
+ # For all non-tail calls, evaluate the expression and return the value
102
172
  stack = @scope.runtime.stack
103
173
  stack << Frame.new(expression, @scope)
104
174
  stack.value
105
175
  end
106
176
 
107
- def fill!(value, subexpr = nil)
108
- @values << value
109
- end
110
-
111
- def to_s
112
- @expression.map { |e| e.to_s } * ' '
177
+ # Do-nothing override of <tt>Frame#fill!</tt>. +Function+ bodies do not
178
+ # need to remember the return value of each expression.
179
+ def fill!(*args)
113
180
  end
114
181
  end
115
182