First class environments for SteelSeries/GoLisp

12 May 2015

by Dave Astels

Scheme (and thus GoLisp) is lexically scoped. This is implemented by the creation of a lexical environment (aka symbol table) for each lexical scope:

  • function/lambda/macro invocations, which holds parameters and any local definitions
  • let structures, which hold the let bindings
  • do structures, which hold the do bindings

Functions and lambdas capture a reference to the environment in which they were defined, so they always have access to its bindings (that’s a closure, btw).

Each environment has a connection to its containing environment, and can override/hide bindings in outer scopes. When a symbol is evaluated, the most local environment is searched first. If a binding for the system isn’t found there, the containing environment is searched. This continues until a binding for the sybol is found or we go all the way to the global environment and still can’t find a binding.

Section 3.2 of [1] does a great job of explaining environments in Scheme, which is the basis for environments in GoLisp.

In Scheme, some environments are more important than others, mainly as they tend to be larger, long lived, and serve as the root of many other environments as a program runs. These are known as top level environments. Specifially, these are the global environment (the only environment that is contained by nothing), and any environments directly below it in the environment tree. The REPL runs in one such environment, which effectively sandboxes it, protecting the bindings in the global environment from corruption.

In standard Scheme, environments are first class objects. That is, they can be passed around, returned, manipulated, etc. I’ve recently added this ability to GoLisp, along with the majority of the environment manipulation functions from Scheme. These are described below.

(environment? sexpr)

Returns #t if sexpr is an environment; otherwise returns #f.

(environment-has-parent? environment)

Returns #t if environment has a parent environment; otherwise returns #f.

(environment-parent environment)

Returns the parent environment of environment. It is an error if environment has no parent.

(environment-bound-names environment)

Returns a newly allocated list of the names (symbols) that are bound by environment. This does not include the names that are bound by the parent environment of environment. It does include names that are unassigned or keywords in environment.

(environment-macro-names environment)

Returns a newly allocated list of the names (symbols) that are bound to syntactic keywords in environment.

(environment-bindings environment)

Returns a newly allocated list of the bindings of environment; does not include the bindings of the parent environment. Each element of this list takes one of two forms: (symbol) indicates that symbol is bound but unassigned, while (symbol object) indicates that symbol is bound, and its value is object.

(environment-reference-type environment symbol)

Returns a symbol describing the reference type of symbol in environment or one of its ancestor environments. The result is one of the following:

  • normal means symbol is a variable binding with a normal value.
  • unassigned means symbol is a variable binding with no value.
  • macro means symbol is a keyword binding.
  • unbound means symbol has no associated binding.

(environment-bound? environment symbol)

Returns #t if symbol is bound in environment or one of its ancestor environments; otherwise returns #f. This is equivalent to

(not (eq? ’unbound
          (environment-reference-type environment symbol)))

(environment-assigned? environment symbol)

Returns #t if symbol is bound in environment or one of its ancestor environments, and has a normal value. Returns #f if it is bound but unassigned. Signals an error if it is unbound or is bound to a keyword.

(environment-lookup environment symbol)

symbol must be bound to a normal value in environment or one of its ancestor environments. Returns the value to which it is bound. Signals an error if unbound, unassigned, or a keyword.

(environment-lookup-macro environment symbol)

If symbol is a keyword binding in environment or one of its ancestor environments, returns the value of the binding. Otherwise, returns #f. Does not signal any errors other than argument-type errors.

(environment-assignable? environment symbol)

symbol must be bound in environment or one of its ancestor environments. Returns #t if the binding may be modified by side effect.

(environment-assign! environment symbol value)

symbol must be bound in environment or one of its ancestor environments, and must be assignable. Modifies the binding to have object as its value, and returns an unspecified result.

(environment-definable? environment symbol)

Returns #t if symbol is definable in environment, and #f otherwise.

(environment-define environment symbol value)

Defines symbol to be bound to object in environment, and returns an unspecified value. Signals an error if symbol isn’t definable in environment.

(eval sexpr environment)

Evaluates sexpr in environment. You rarely need eval in ordinary programs; it is useful mostly for evaluating expressions that have been created “on the fly” by a program.

system-global-environment

The variable system-global-environment is bound to the distinguished environment that’s the highest level ancestor of all other environments. It is the parent environment of all other top-level environments. Primitives, system procedures, and most syntactic keywords are bound in this environment.

(the-environment)

Returns the current environment. This form may only be evaluated in a top-level environment. An error is signalled if it appears elsewhere.

(procedure-environment procedure)

Returns the closing environment of procedure. Signals an error if procedure is a primitive procedure.

(make-top-level-environment [names [values]])

Returns a newly allocated top-level environment. extend-top-level-environment creates an environment that has parent environment, make-top-level-environment creates an environment that has parent system-global-environment, and make- root-top-level-environment creates an environment that has no parent.

The optional arguments names and values are used to specify initial bindings in the new environment. If specified, names must be a list of symbols, and values must be a list of objects. If only names is specified, each name in names will be bound in the environment, but unassigned. If names and values are both specified, they must be the same length, and each name in names will be bound to the corresponding value in values. If neither names nor values is specified, the environment will have no initial bindings.

Environments in GoLisp differ slightly from standard Scheme in that they have a name attached. For the various forms of let and do this is simply "let" and "do", respectively. Not of much use, but then these are just a byproduct of having lexical scopes. What’s more useful is the higher level environments. This brings us to the real reason for adding environment support: game integration sandboxes. When we were writing the game integration functionality for Engine3, we wanted each game’s event handling to live in a separate sandbox. This is implemented by creating a new top level environment under the global environment. The problem here is that it’s off in its own world, separate from the repl. By naming environments (in this case by the name of the game), we can add a function to return an environment given its name. That allows us to peek inside the sandbox from the repl, examining and manipulating the bindings there. And so we added a function to let us do that:

(find-top-level-environment name)

Returns the top level environment with the given name.

References

[1] Harold Abelson, Gerald Jay Sussman, and Julie Sussman. Structure and Interpretation of Computer Programs. MIT Press, Cambridge, Mass., 1985.