x-lang

First-Class Environments

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.

Why

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.

The model

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:

What it removes

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.

What it gives the language

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:

What it costs

Measurements before it lands

Sequence

  1. This note, reviewed.
  2. The engine change on a branch, with C and bare specs, measured against x-lang’s suite from source.
  3. The contract rows, the declaration regenerated, a release cut, and the pin bump in x-lang with its few readers moved.
  4. The langs drop their define tricks and def-global doors.
  5. Namespaces, on the environment the engine now has.

Open questions