x-lang: computational expressions over a minimal, type-agnostic engine.
This is a design proposal for the engine’s environment model. Nothing in it is implemented. It sits beside The Engine Contract because it changes the base-layout contract, and the language owns the terms an engine is judged by. The language-level design it serves is Namespaces.
An environment today is a cons chain of binding cells. A frame is the run of cells at the head that carry a frame mark, and the only thing that identifies a frame is the head pointer the current activation holds. Six mechanisms exist to compensate for that one fact:
| mechanism | what it compensates for |
|---|---|
| the frame and function-frame flag bits on cells | lookup cannot tell a frame from the global region without a mark |
| the local boundary | the restore protocol needs to know where the frame region ends |
| the shadow list | an earlier fix for locals hiding globals, retired in spirit by the marks, still restored |
| the operative restore’s walk to the operative’s head | the body may have grown the caller’s chain, or may not, and only a walk can tell |
| the top-level bracket that strips the leading frame run and parks it | a file’s forms and eval! must bind globally from inside frames |
def-global |
a definition made inside a frame that must land in the global tree |
And one consequence the language keeps meeting: an operative cannot define
for its caller. Its def extends its own frame, which the restore drops. A
tail-evaluated def grows the caller’s chain in front of a head the
caller’s own saved compound still points at, which the next restore drops.
That is jonruttan/x-lang#527, the reason every lang’s define is a
tail-position trick, and the reason x-engine-c#46 asked for one more
primitive, def-in, before this note replaced it.
A closure also captures the global tree’s root at creation and reinstalls it on each call, so a closure made in a child base can miss a global defined after it. That is a second identity problem: the global environment has no object either, only a root pointer.
An environment is one pair:
(bindings . parent)
bindings is a chain of (name . value) cells, or the root’s tree.
parent is the enclosing environment, or nil at the root. That is the whole
representation. It is a pair tree like everything else in the base, so
first and rest walk it and the collector marks it with no special case.
The rules, in full:
bindings, then the parent’s, up to the root. The
root’s bindings may be a tree for speed; that is an implementation detail
of one environment, not a separate global region.def. Bind in the current environment: update the name’s cell in this
environment’s bindings if it has one, otherwise add one. Never the parent.set!. Find the name by lookup and update the cell where it is found.
An unbound name is an error, as now.eval with an environment. Make that environment current, evaluate,
restore the previous one. A def inside binds in the given environment
and stays bound, because the environment is an object and the binding is
in it. (eval (list 'def n v) e) from an operative is the definer every
lang wanted, with nothing added.eval!. Evaluate in the root. The loader evaluates a file’s forms in
the root the same way.guard, call/cc, the operative return.base bind. Bind in the target base’s root. def-global becomes
def evaluated with the root as its environment and needs no primitive.From the engine: the two flag bits and every test of them, the local
boundary, the shadow list, the reachability walk in the operative restore,
the top-level bracket’s parking of frames on the root chain, the captured
tree in every closure, and def-global. The ISA manifest loses one row and
gains none.
From the language: the tail-position define trick in every lang, the
def-global door in x-python and x-sweet, and the whole of issue #527 and
issue #644 as concepts rather than as cases. A definer written as an
operative, doc, def-class, def-record, def-generic, a lang’s
define, binds where its caller’s environment says.
An environment is a value the language can make, hand around, and evaluate in. That is the mechanism Namespaces needs, and it is smaller than what that note proposed:
provide copies the listed bindings into the root, or records them in
the registry as the note describes. import binds the chosen names into
the importer’s environment, which is a def evaluated there.$define! is def in a named
environment. Scheme’s internal defines get letrec semantics because a
body’s definitions live in the body’s own environment. A lang that wants
Kernel’s flat answer for a parameterless body evaluates in the parent
instead. None of this is decided in C.first and rest.def.Base cell door. They move in the same pin bump.tests/x/specs/core/env-scope.spec.md.define tricks and def-global doors.guard binds its error variable in a fresh child or in the
current environment. A child is the consistent answer.