heist 0.1.0 → 0.2.0

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,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