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.
@@ -1,59 +1,84 @@
1
- require 'forwardable'
2
-
3
1
  module Heist
2
+
3
+ # +Runtime+ objects represent instances of the Heist runtime environment.
4
+ # Each +Runtime+ defines a top-level +Scope+, into which are injected
5
+ # the standard set of primitive functions and special forms as defined
6
+ # in <tt>lib/builtin</tt>.
7
+ #
8
+ # +Runtime+ exposes several methods from the top-level +Scope+ object,
9
+ # allowing runtime objects to be used as interfaces for defining
10
+ # functions, eval'ing code and running source files.
11
+ #
4
12
  class Runtime
5
13
 
6
- %w[ data/expression data/identifier data/list
7
- callable/function callable/macro callable/continuation
8
- scope binding frame
9
- stack stackless
14
+ %w[ data/expression data/identifier data/cons
15
+ callable/function callable/syntax callable/macro callable/continuation
16
+ frame stack stackless
17
+ scope binding
10
18
 
11
19
  ].each do |file|
12
20
  require RUNTIME_PATH + file
13
21
  end
14
22
 
15
23
  extend Forwardable
16
- def_delegators(:@top_level, :[], :eval, :define, :syntax, :call)
24
+ def_delegators(:@top_level, :[], :eval, :exec, :define, :syntax, :run)
17
25
 
18
- attr_reader :order
19
26
  attr_accessor :stack, :top_level
20
27
 
28
+ # A +Runtime+ is initialized using a set of options. The available
29
+ # options include the following, all of which are +false+ unless
30
+ # you override them yourself:
31
+ #
32
+ # * <tt>:continuations</tt>: set to +true+ to enable <tt>call/cc</tt>
33
+ # * <tt>:lazy</tt>: set to +true+ to enable lazy evaluation
34
+ # * <tt>:unhygienic</tt>: set to +true+ to disable macro hygiene
35
+ #
21
36
  def initialize(options = {})
22
37
  @lazy = !!options[:lazy]
23
38
  @continuations = !!options[:continuations]
24
39
  @hygienic = !options[:unhygienic]
25
40
 
26
41
  @top_level = Scope.new(self)
27
- @stack = create_stack
28
-
29
- syntax_type = (lazy? or not @hygienic) ? 'rb' : 'scm'
42
+ @stack = stackless? ? Stackless.new : Stack.new
30
43
 
31
44
  run("#{ BUILTIN_PATH }primitives.rb")
32
- run("#{ BUILTIN_PATH }syntax.#{syntax_type}")
45
+ run("#{ BUILTIN_PATH }syntax.scm")
33
46
  run("#{ BUILTIN_PATH }library.scm")
34
47
 
35
48
  @start_time = Time.now.to_f
36
49
  end
37
50
 
38
- def run(path)
39
- return instance_eval(File.read(path)) if File.extname(path) == '.rb'
40
- @top_level.run(path)
41
- end
42
-
51
+ # Returns the length of time the +Runtime+ has been alive for, as a
52
+ # number in microseconds.
43
53
  def elapsed_time
44
54
  (Time.now.to_f - @start_time) * 1000000
45
55
  end
46
56
 
57
+ # Returns +true+ iff the +Runtime+ is using lazy evaluation.
47
58
  def lazy?; @lazy; end
48
59
 
60
+ # Returns +true+ iff the +Runtime+ is using hygienic macros.
49
61
  def hygienic?; @hygienic; end
50
62
 
63
+ # Returns +true+ iff the +Runtime+ is using the faster +Stackless+
64
+ # evaluator, which does not support <tt>(call/cc)</tt>.
51
65
  def stackless?
52
66
  lazy? or not @continuations
53
67
  end
54
68
 
55
- def create_stack
56
- stackless? ? Stackless.new : Stack.new
69
+ def to_s
70
+ "#<runtime: #{ stackless? ? 'call/cc disabled' : 'call/cc enabled'
71
+ }, #{ hygienic? ? 'hygienic' : 'unhygienic'
72
+ }, #{ lazy? ? 'lazy' : 'eager' }>"
73
+ end
74
+ alias :inspect :to_s
75
+
76
+ def info
77
+ [ "Heist Scheme interpreter v. #{ VERSION }",
78
+ "Evaluation mode: #{ lazy? ? 'LAZY' : 'EAGER' }",
79
+ "Continuations enabled? #{ stackless? ? 'NO' : 'YES' }",
80
+ "Macros: #{ hygienic? ? 'HYGIENIC' : 'UNHYGIENIC' }\n\n"
81
+ ] * "\n"
57
82
  end
58
83
 
59
84
  end
data/lib/runtime/scope.rb CHANGED
@@ -1,9 +1,24 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # +Scope+ is primarily used to represent symbol tables, though it also
5
+ # has a few other scope-related responsibilities such as defining
6
+ # functions (functions need to remember the scope they appear in) and
7
+ # loading files. Scheme uses lexical scope, which we model using a simple
8
+ # delegation system.
9
+ #
10
+ # Every +Scope+ has a hash (<tt>@symbols</tt>) in which it stores names
11
+ # of variables and their associated values, and a parent scope
12
+ # (<tt>@parent</tt>). If a variable cannot be found in one scope, the
13
+ # lookup is delegated to the parent until we get to the top level, at
14
+ # which point an exception is raised.
15
+ #
4
16
  class Scope
5
17
  attr_reader :runtime
6
18
 
19
+ # A +Scope+ is initialized using another +Scope+ to use as the parent.
20
+ # The parent may also be a +Runtime+ instance, indicating that the
21
+ # new +Scope+ is being used as the top level of a runtime environment.
7
22
  def initialize(parent = {})
8
23
  @symbols = {}
9
24
  is_runtime = (Runtime === parent)
@@ -11,48 +26,127 @@ module Heist
11
26
  @runtime = is_runtime ? parent : parent.runtime
12
27
  end
13
28
 
29
+ # Returns the value corresponding to the given variable name. If the
30
+ # name does not exist in the receiver, the call is delegated to its
31
+ # parent scope. If the name cannot be found in any scope an exception
32
+ # is raised.
33
+ #
34
+ # In lazy mode, +Binding+ objects are stored in the symbol table when
35
+ # functions are called; we do not evaluate the arguments to a function
36
+ # before calling it, but instead we force an argument's value if the
37
+ # function's body attempts to access it by name.
38
+ #
14
39
  def [](name)
15
40
  name = to_name(name)
16
- bound = @symbols.has_key?(to_name(name))
41
+ bound = @symbols.has_key?(name)
17
42
 
18
43
  raise UndefinedVariable.new(
19
44
  "Variable '#{name}' is not defined") unless bound or Scope === @parent
20
45
 
21
46
  value = bound ? @symbols[name] : @parent[name]
22
- value = value.extract if Binding === value
47
+ value = value.force! if value.respond_to?(:force!)
23
48
  value
24
49
  end
25
50
 
51
+ # Binds the given +value+ to the given +name+ in the receiving +Scope+.
52
+ # Note this always sets the variable in the receiver; see <tt>set!</tt>
53
+ # for a method corresponding to Scheme's <tt>(set!)</tt> function.
26
54
  def []=(name, value)
27
55
  @symbols[to_name(name)] = value
28
56
  value.name = name if Function === value
29
57
  value
30
58
  end
31
59
 
60
+ # Returns +true+ iff the given name is bound as a variable in the
61
+ # receiving scope or in any of its ancestor scopes.
32
62
  def defined?(name)
33
63
  @symbols.has_key?(to_name(name)) or
34
64
  (Scope === @parent and @parent.defined?(name))
35
65
  end
36
66
 
37
- def set!(name, value)
67
+ # Returns a +Scope+ object representing the innermost scope in which
68
+ # the given name is bound. This is used to find out whether two or
69
+ # more identifiers have the same binding.
70
+ #
71
+ # outer = Scope.new
72
+ # outer['foo'] = "a value"
73
+ #
74
+ # inner = Scope.new(outer)
75
+ # inner['bar'] = "something"
76
+ #
77
+ # inner.innermost_binding('foo') #=> outer
78
+ # inner.innermost_binding('bar') #=> inner
79
+ #
80
+ def innermost_binding(name)
38
81
  name = to_name(name)
39
- bound = @symbols.has_key?(name)
40
-
41
- raise UndefinedVariable.new(
42
- "Cannot set undefined variable '#{name}'") unless bound or Scope === @parent
43
-
44
- return @parent.set!(name, value) unless bound
45
- self[name] = value
82
+ @symbols.has_key?(name) ?
83
+ self :
84
+ Scope === @parent ?
85
+ @parent.innermost_binding(name) :
86
+ nil
87
+ end
88
+
89
+ # Analogous to Scheme's <tt>(set!)</tt> procedure. Assigns the given
90
+ # +value+ to the given variable +name+ in the innermost region in
91
+ # which +name+ is bound. If the +name+ does not exist in the receiving
92
+ # scope, the assignment is delegated to the parent. If no visible
93
+ # binding exists for the given +name+ an exception is raised.
94
+ def set!(name, value)
95
+ scope = innermost_binding(name)
96
+ raise UndefinedVariable.new("Cannot set undefined variable '#{name}'") if scope.nil?
97
+ scope[name] = value
46
98
  end
47
-
99
+
100
+ # +define+ is used to define functions using either Scheme or Ruby
101
+ # code. Takes either a name and a Ruby block to represent the function,
102
+ # or a name, a list of formal arguments and a list of body expressions.
103
+ # The <tt>(define)</tt> primitive exposes this method to the Scheme
104
+ # environment. This method allows easy extension using Ruby, for
105
+ # example:
106
+ #
107
+ # scope.define('+') |*args|
108
+ # args.inject { |a,b| a + b }
109
+ # end
110
+ #
111
+ # See +Function+ for more information.
112
+ #
48
113
  def define(name, *args, &block)
49
114
  self[name] = Function.new(self, *args, &block)
50
115
  end
51
116
 
52
- def syntax(name, holes = [], &block)
53
- self[name] = Syntax.new(self, holes,&block)
117
+ # +syntax+ is similar to +define+, but is used for defining syntactic
118
+ # forms. Heist's parser has no predefined syntax apart from generic
119
+ # Lisp paren syntax and Scheme data literals. All special forms are
120
+ # defined as special functions and stored in the symbol table, making
121
+ # them first-class objects that can be easily aliased and overridden.
122
+ #
123
+ # This method takes a name and a Ruby block. The block will be called
124
+ # with the calling +Scope+ object and a +Cons+ containing the section
125
+ # of the parse tree representing the parameters the form has been called
126
+ # with.
127
+ #
128
+ # It is not recommended that you write your own syntax using Ruby
129
+ # since it requires too much knowledge of the plumbing for features
130
+ # like tail calls and continuations. If you define new syntax using
131
+ # Scheme macros you get correct behaviour of these features for free.
132
+ #
133
+ # See +Syntax+ for more information.
134
+ #
135
+ def syntax(name, &block)
136
+ self[name] = Syntax.new(self, &block)
137
+ end
138
+
139
+ # Parses and executes the given string of source code in the receiving
140
+ # +Scope+. Accepts strings of Scheme source and arrays of Ruby data to
141
+ # be interpreted as Scheme lists.
142
+ def eval(source)
143
+ source = Heist.parse(source)
144
+ source.eval(self)
54
145
  end
146
+ alias :exec :eval
55
147
 
148
+ # Returns all the variable names visible in the receiving +Scope+ that
149
+ # match the given regex +pattern+. Used by the REPL for tab completion.
56
150
  def grep(pattern)
57
151
  base = (Scope === @parent) ? @parent.grep(pattern) : []
58
152
  @symbols.each do |key, value|
@@ -61,25 +155,23 @@ module Heist
61
155
  base.uniq
62
156
  end
63
157
 
64
- # TODO: this isn't great, figure out a way for functions
65
- # to transparently handle inter-primitive calls so Ruby can
66
- # call Scheme code as well as other Ruby code
67
- def call(name, *params)
68
- self[name].body.call(*params)
69
- end
70
-
71
- def run(path)
72
- path = path + FILE_EXT unless File.file?(path)
73
- source = Heist.parse(File.read(path))
74
- scope = FileScope.new(self, path)
75
- source.eval(scope)
76
- end
77
-
78
- def eval(source)
79
- source = Heist.parse(source) if String === source
80
- source.eval(self)
158
+ # Runs the given Scheme or Ruby definition file in the receiving
159
+ # +Scope+. Note that local vars in this method can cause block vars
160
+ # to become delocalized when running Ruby files under 1.8, so make
161
+ # sure we use 'obscure' names here.
162
+ def run(_path)
163
+ return instance_eval(File.read(_path)) if File.extname(_path) == '.rb'
164
+ _path = _path + FILE_EXT unless File.file?(_path)
165
+ _source = Heist.parse(File.read(_path))
166
+ _scope = FileScope.new(self, _path)
167
+ _source.eval(_scope)
81
168
  end
82
169
 
170
+ # Loads the given Scheme file and executes it in the global scope.
171
+ # Paths are treated as relative to the current file. If no local file
172
+ # is found, the path is assumed to refer to a module from the Heist
173
+ # standard library. The <tt>(load)</tt> primitive is a wrapper
174
+ # around this method.
83
175
  def load(path)
84
176
  dir = load_path.find do |dir|
85
177
  File.file?("#{dir}/#{path}") or File.file?("#{dir}/#{path}#{FILE_EXT}")
@@ -89,16 +181,34 @@ module Heist
89
181
  true
90
182
  end
91
183
 
184
+ # Returns the path of the current file. The receiving scope must have
185
+ # a +FileScope+ as an ancestor, otherwise this method will return +nil+.
92
186
  def current_file
93
187
  @path || @parent.current_file rescue nil
94
188
  end
95
189
 
96
190
  private
97
191
 
192
+ # Calls the named primitive function with the given arguments, and
193
+ # returns the result of the call.
194
+ #
195
+ # TODO: this is currently hampered by the fact that Functions expect to
196
+ # be called with a +Scope+, but Ruby primitives are not given the
197
+ # current +scope+. Figure out something better.
198
+ def call(name, *params)
199
+ self[name].body.call(*params)
200
+ end
201
+
202
+ # Converts any Ruby object to a name string. All names are downcased
203
+ # as this Scheme is case-insensitive.
98
204
  def to_name(name)
99
205
  name.to_s.downcase
100
206
  end
101
207
 
208
+ # Returns the current set of directories in which to look for Scheme
209
+ # files to load. Includes the standard library path by default, and
210
+ # the directory of the current file if the receiving +Scope+ has a
211
+ # +FileScope+ as an ancestor.
102
212
  def load_path
103
213
  paths, file = [], current_file
104
214
  paths << File.dirname(file) if file
@@ -106,6 +216,11 @@ module Heist
106
216
  end
107
217
  end
108
218
 
219
+ # A +FileScope+ is a special kind of +Scope+ used to represent the region
220
+ # of a single file. It provides Scheme code with an awareness of its
221
+ # path so it can load local files. +FileScope+ instances delegate all
222
+ # variable assignments to their parent +Scope+ (this is typically the
223
+ # global scope) so that variables are visible across files.
109
224
  class FileScope < Scope
110
225
  extend Forwardable
111
226
  def_delegators(:@parent, :[]=)
data/lib/runtime/stack.rb CHANGED
@@ -1,29 +1,116 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # +Stack+ is responsible for executing code by successively evaluating
5
+ # expressions. It provides fine-grained intermediate result inspection
6
+ # to support the Scheme notion of continuations, working with the +Frame+
7
+ # and +Body+ classes to evaluate expressions and function bodies piece
8
+ # by piece. Using the +Stack+ engine allows the creation of +Continuation+
9
+ # functions, which save the current state of the stack (i.e. the state
10
+ # of any unfinished expressions and function bodies) and allow it to be
11
+ # resumed at some later time.
12
+ #
13
+ # +Stack+ inherits from +Array+, and is a last-in-first-out structure:
14
+ # the next expression evaluated is always the last expression on the
15
+ # stack.
16
+ #
17
+ # You should think of the +Stack+ as an array of +Frame+ objects that
18
+ # hold expressions and track their progress. For example, take the
19
+ # expression:
20
+ #
21
+ # (+ (- (* 8 9) (/ 21 7)) 4)
22
+ #
23
+ # Evaluating it involves evaluating each subexpression to fill in holes
24
+ # where we expect values; when all the holes in an expression have been
25
+ # filled, we can apply the resulting function to the arguments and get
26
+ # a value. Evaluating this expression causes the stack to evolve as
27
+ # follows, where STATE lists the expressions on the stack and <tt>[]</tt>
28
+ # represents a hole that is waiting for a value:
29
+ #
30
+ # PUSH: (+ (- (* 8 9) (/ 21 7)) 4)
31
+ # STATE: ([] [] 4)
32
+ #
33
+ # PUSH: +
34
+ # VALUE: #<procedure:+>
35
+ # STATE: (#<procedure:+> [] 4)
36
+ #
37
+ # PUSH: (- (* 8 9) (/ 21 7))
38
+ # STATE: (#<procedure:+> [] 4), ([] [] [])
39
+ #
40
+ # PUSH: -
41
+ # VALUE: #<procedure:->
42
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> [] [])
43
+ #
44
+ # PUSH: (* 8 9)
45
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> [] []), ([] 8 9)
46
+ #
47
+ # PUSH: *
48
+ # VALUE: #<procedure:*>
49
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> [] []), (#<procedure:*> 8 9)
50
+ #
51
+ # VALUE: 72
52
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> 72 [])
53
+ #
54
+ # PUSH: (/ 21 7)
55
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> 72 []), ([] 21 7)
56
+ #
57
+ # PUSH: /
58
+ # VALUE: #<procedure:/>
59
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> 72 []), (#<procedure:/> 21 7)
60
+ #
61
+ # VALUE: 3
62
+ # STATE: (#<procedure:+> [] 4), (#<procedure:-> 72 3)
63
+ #
64
+ # VALUE: 69
65
+ # STATE: (#<procedure:+> 69 4)
66
+ #
67
+ # VALUE: 73
68
+ #
69
+ # So we find that <tt>(+ (- (* 8 9) (/ 21 7)) 4)</tt> gives the value 73.
70
+ # Whenever a value is returned by a subexpression we must inspect it to
71
+ # see if a +Continuation+ has been called. All this inspection of
72
+ # intermediate values takes time; if you don't need full +Continuation+
73
+ # support, use the faster +Stackless+ engine instead.
74
+ #
4
75
  class Stack < Array
5
76
  attr_reader :value
6
77
 
78
+ # Pushes a new +Frame+ or +Body+ onto the +Stack+ and then executes
79
+ # the resulting code until the pushed frame returns a value, which
80
+ # is then returned.
7
81
  def <<(frame)
8
82
  super
9
83
  clear!(size - 1)
10
84
  end
11
85
 
86
+ # Creates and returns a copy of the stack, which represents the current
87
+ # computational state: any unfinished expressions and function bodies
88
+ # are stored in the stack. Pass +false+ to discard the final frame,
89
+ # which will typically be a call to <tt>(call/cc)</tt> when creating
90
+ # a +Continuation+.
12
91
  def copy(keep_last = true)
13
92
  copy = self.class.new
14
93
  range = keep_last ? 0..-1 : 0...-1
15
94
  self[range].each do |frame|
16
- copy[copy.size] = frame.dup
95
+ copy[copy.size] = frame.clone
17
96
  end
18
97
  copy
19
98
  end
20
99
 
100
+ # Fills a hole in the final +Frame+ on the +Stack+ by replacing the
101
+ # given epxression +subexpr+ with the given +value+. If the +value+
102
+ # is a +Frame+, this frame is pushed onto the stack rather than filling
103
+ # a hole in the previous frame.
21
104
  def fill!(subexpr, value)
22
105
  return self[size] = value if Frame === value
23
106
  return @value = value if empty?
24
107
  last.fill!(subexpr, value)
25
108
  end
26
109
 
110
+ # Causes the stack to evaluate expressions in order to pop them off the
111
+ # stack, until it gets down to the size given by +limit+. The resulting
112
+ # value if returned after all necessary computations have been done,
113
+ # and if an error takes place at any point we empty the stack.
27
114
  def clear!(limit = 0)
28
115
  process! while size > limit
29
116
  @value
@@ -32,6 +119,14 @@ module Heist
32
119
  raise ex
33
120
  end
34
121
 
122
+ # Sets the +value+ on the +Stack+, which is always the value returned by
123
+ # the last completed expression or function body. If the given +value+
124
+ # is another +Stack+, this new stack replaces the state of the receiver;
125
+ # this takes place when a +Continuation+ is called. If the +value+ is
126
+ # a +Frame+, it is pushed onto the stack and we set a flag to indicate
127
+ # that a tail call is in effect and the replacement target of the call
128
+ # needs to be repointed: the expression that generated the tail call will
129
+ # have been removed from the stack by the time the call returns.
35
130
  def value=(value)
36
131
  @value = value
37
132
  @unwind = (Stack === @value)
@@ -41,6 +136,11 @@ module Heist
41
136
 
42
137
  private
43
138
 
139
+ # Processes one piece of the final +Frame+ on the +Stack+ and inspects the
140
+ # return value. The value must be inspected to see if a +Continuation+ has
141
+ # been called (indicated by <tt>@unwind</tt>), or a tail call has taken
142
+ # place. Continuation calls replace the state of the stack, and tail calls
143
+ # need modifying so they fill the correct hole when they return.
44
144
  def process!
45
145
  self.value = last.process!
46
146
  return if empty? or @unwind or not last.complete?
@@ -48,6 +148,8 @@ module Heist
48
148
  fill!(pop.target, @value)
49
149
  end
50
150
 
151
+ # Replaces the state of the receiver with the state of the argument. We
152
+ # call this when calling a +Continuation+, or when recovering from errors.
51
153
  def restack!(stack = [])
52
154
  pop while not empty?
53
155
  stack.each_with_index { |frame, i| self[i] = frame }