Class: Heist::Runtime::Scope

Inherits:
Object
  • Object
show all
Defined in:
lib/heist/runtime/scope.rb

Overview

Scope is primarily used to represent symbol tables, though it also has a few other scope-related responsibilities such as defining functions (functions need to remember the scope they appear in) and loading files. Scheme uses lexical scope, which we model using a simple delegation system.

Every Scope has a hash (@symbols) in which it stores names of variables and their associated values, and a parent scope (@parent). If a variable cannot be found in one scope, the lookup is delegated to the parent until we get to the top level, at which point an exception is raised.

Direct Known Subclasses

FileScope

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent = {}) ⇒ Scope

A Scope is initialized using another Scope to use as the parent. The parent may also be a Runtime instance, indicating that the new Scope is being used as the top level of a runtime environment.



22
23
24
25
26
27
# File 'lib/heist/runtime/scope.rb', line 22

def initialize(parent = {})
  @symbols = Trie.new
  is_runtime = (Runtime === parent)
  @parent = is_runtime ? {} : parent
  @runtime = is_runtime ? parent : parent.runtime
end

Instance Attribute Details

#runtimeObject (readonly)

Returns the value of attribute runtime.



17
18
19
# File 'lib/heist/runtime/scope.rb', line 17

def runtime
  @runtime
end

Instance Method Details

#[](name) ⇒ Object

Returns the value corresponding to the given variable name. If the name does not exist in the receiver, the call is delegated to its parent scope. If the name cannot be found in any scope an exception is raised.

In lazy mode, Binding objects are stored in the symbol table when functions are called; we do not evaluate the arguments to a function before calling it, but instead we force an argument’s value if the function’s body attempts to access it by name.

Raises:



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/heist/runtime/scope.rb', line 39

def [](name)
  name = to_name(name)
  bound = @symbols.has_key?(name)
  
  raise UndefinedVariable.new(
    "Variable '#{name}' is not defined") unless bound or Scope === @parent
      
  value = bound ? @symbols[name] : @parent[name]
  value = value.force! if value.respond_to?(:force!)
  value
end

#[]=(name, value) ⇒ Object

Binds the given value to the given name in the receiving Scope. Note this always sets the variable in the receiver; see set! for a method corresponding to Scheme’s (set!) function.



54
55
56
57
58
# File 'lib/heist/runtime/scope.rb', line 54

def []=(name, value)
  @symbols[to_name(name)] = value
  value.name = name if Function === value
  value
end

#current_fileObject

Returns the path of the current file. The receiving scope must have a FileScope as an ancestor, otherwise this method will return nil.



191
192
193
# File 'lib/heist/runtime/scope.rb', line 191

def current_file
  @path || @parent.current_file rescue nil
end

#define(name, *args, &block) ⇒ Object

define is used to define functions using either Scheme or Ruby code. Takes either a name and a Ruby block to represent the function, or a name, a list of formal arguments and a list of body expressions. The (define) primitive exposes this method to the Scheme environment. This method allows easy extension using Ruby, for example:

scope.define('+') |*args|
  args.inject { |a,b| a + b }
end

See Function for more information.



113
114
115
# File 'lib/heist/runtime/scope.rb', line 113

def define(name, *args, &block)
  self[name] = Function.new(self, *args, &block)
end

#defined?(name) ⇒ Boolean

Returns true iff the given name is bound as a variable in the receiving scope or in any of its ancestor scopes.

Returns:

  • (Boolean)


62
63
64
65
# File 'lib/heist/runtime/scope.rb', line 62

def defined?(name)
  @symbols.has_key?(to_name(name)) or
      (Scope === @parent and @parent.defined?(name))
end

#each_var(&block) ⇒ Object

Calls the given block with the name of every variable visible in the Scope (given as a Symbol) and its corresponding value.



141
142
143
144
145
146
# File 'lib/heist/runtime/scope.rb', line 141

def each_var(&block)
  @parent.each_var(&block) if @parent.respond_to?(:each_var)
  @symbols.each do |key, value|
    block.call((key * '').to_sym, value)
  end
end

#eval(source) ⇒ Object Also known as: exec

Parses and executes the given string of source code in the receiving Scope. Accepts strings of Scheme source and arrays of Ruby data to be interpreted as Scheme lists.



151
152
153
154
# File 'lib/heist/runtime/scope.rb', line 151

def eval(source)
  source = Heist.parse(source)
  source.eval(self)
end

#expand_path(path) ⇒ Object

Returns an absolute path to a requested library based on searching the current load path. The file extension may be omitted, suitable extensions being listed in Heist::FILE_EXTS.



198
199
200
201
202
203
204
205
206
207
# File 'lib/heist/runtime/scope.rb', line 198

def expand_path(path)
  load_path.each do |dir|
    test_path = File.expand_path(File.join(dir, path))
    FILE_EXTS.each do |ext|
      full_path = test_path + ext
      return full_path if File.file?(full_path)
    end
  end
  nil
end

#innermost_binding(name) ⇒ Object

Returns a Scope object representing the innermost scope in which the given name is bound. This is used to find out whether two or more identifiers have the same binding.

outer = Scope.new
outer['foo'] = "a value"

inner = Scope.new(outer)
inner['bar'] = "something"

inner.innermost_binding('foo') #=> outer
inner.innermost_binding('bar') #=> inner


80
81
82
83
84
85
86
87
# File 'lib/heist/runtime/scope.rb', line 80

def innermost_binding(name)
  name = to_name(name)
  @symbols.has_key?(name) ?
      self :
  Scope === @parent ?
      @parent.innermost_binding(name) :
      nil
end

#load(file) ⇒ Object

Loads the given Scheme file and executes it in the global scope. Paths are treated as relative to the current file. If no local file is found, the path is assumed to refer to a module from the Heist standard library. The (load) primitive is a wrapper around this method.



184
185
186
187
# File 'lib/heist/runtime/scope.rb', line 184

def load(file)
  path = expand_path(file)
  runtime.run(path) if path
end

#longest_prefix(name) ⇒ Object

Returns the longest shared prefix match for the given variable name stub, used to support autocompletion in the REPL.



164
165
166
# File 'lib/heist/runtime/scope.rb', line 164

def longest_prefix(name)
  @symbols.longest_prefix(to_name(name))
end

#program(expressions) ⇒ Object

Executes an array of Scheme statements expressed as Ruby data.



158
159
160
# File 'lib/heist/runtime/scope.rb', line 158

def program(expressions)
  expressions.map { |expr| exec(expr) }.last
end

#run(_path) ⇒ Object

Runs the given Scheme or Ruby definition file in the receiving Scope. Note that local vars in this method can cause block vars to become delocalized when running Ruby files under 1.8, so make sure we use ‘obscure’ names here.



172
173
174
175
176
177
# File 'lib/heist/runtime/scope.rb', line 172

def run(_path)
  return instance_eval(File.read(_path)) if File.extname(_path) == '.rb'
  _source = Heist.parse(File.read(_path))
  _scope  = FileScope.new(self, _path)
  _source.eval(_scope)
end

#set!(name, value) ⇒ Object

Analogous to Scheme’s (set!) procedure. Assigns the given value to the given variable name in the innermost region in which name is bound. If the name does not exist in the receiving scope, the assignment is delegated to the parent. If no visible binding exists for the given name an exception is raised.

Raises:



94
95
96
97
98
# File 'lib/heist/runtime/scope.rb', line 94

def set!(name, value)
  scope = innermost_binding(name)
  raise UndefinedVariable.new("Cannot set undefined variable '#{name}'") if scope.nil?
  scope[name] = value
end

#syntax(name, &block) ⇒ Object

syntax is similar to define, but is used for defining syntactic forms. Heist’s parser has no predefined syntax apart from generic Lisp paren syntax and Scheme data literals. All special forms are defined as special functions and stored in the symbol table, making them first-class objects that can be easily aliased and overridden.

This method takes a name and a Ruby block. The block will be called with the calling Scope object and a Cons containing the section of the parse tree representing the parameters the form has been called with.

It is not recommended that you write your own syntax using Ruby since it requires too much knowledge of the plumbing for features like tail calls and continuations. If you define new syntax using Scheme macros you get correct behaviour of these features for free.

See Syntax for more information.



135
136
137
# File 'lib/heist/runtime/scope.rb', line 135

def syntax(name, &block)
  self[name] = Syntax.new(self, &block)
end