heist 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 }