Behaviour the language requires of an engine that x-engine.xon does not
state and isa.x cannot express. A capability in
engine-contract.md says a group of instructions is
reachable; a law says what an instruction must do.
A check for a law asserts the observable that distinguishes doing it from not doing it — a hook that counts its calls, not a hook that survives a collection.
| # | law | distinguishing check | how absence looks |
|---|---|---|---|
| 1 | eq? compares the OPERAND WORD, not the type |
(eq? #\A 65) is #t |
the printer’s escape table misses every arm |
| 2 | a predicate answers the base’s TRUE/FALSE objects | (same? (eq? 1 1) #t) |
invisible to branching; only printing shows it |
| 3 | the tower operators offer their operands’ type-ops first | a type registering + receives it |
tower arithmetic answers raw words |
| 4 | the collector INVOKES registered hooks, marks before sweeping | a counting hook reaches N after N collections | nothing; the survival spec still passes |
| 5 | apply is a TAIL call |
50k frames of (let ((m (- n 1))) (self m)) |
stack overflow, whole batch dies |
| 6 | a set interrupt flag raises STOP under a handler |
set %sigint-flag inside a guard |
the flag is written and never read |
| 7 | a pointer into the engine’s heap crosses the foreign door as a REAL address | (Proc run! (list "/bin/sh" "-c" "exit 3")) is 3 |
a constant status, whatever the child did |
| 8 | str byte-sub ADDRESSES bytes; it does not slice the NUL-bounded value |
a byte past an embedded NUL | every dirent name reads empty |
| 9 | evaluation and application are TYPE HANDLERS | replacing a made type’s eval handler changes what (eval i) answers |
registration writes a row the evaluator never reads |
| 10 | an environment is ONE PAIR, (bindings . parent), and def binds in the current one |
(eval (list (lit def) n v) e) from an operative binds in e, and the binding is there after the operative returns |
an operative cannot define for its caller; every lang’s define is a tail-position trick |
eq? compares the operand wordx_prim_eq is one expression: a == b || (!isnil(a) && !isnil(b) && x_intval(a)
== x_intval(b)). It reads slot 0 of both operands without asking their types,
so a CHARACTER equals the INTEGER of its code. lib/x/boot/printer.x depends
on it: %print-str-esc? and %print-str-esc-byte match (str byte-ref s i) —
a character — against 34, 92, 10, 9, 13.
A sentinel must not carry a small integer in slot 0. x_token_eof_prim’s value
word is its own address; %token-eof is never eq? to a byte.
#t and #fNever a symbol, never nil. x_prim_eq, x_prim_same, x_prim_lt and type’s
predicates return the base’s true and false fields, which a child base inherits
(x-prim/base.c).
x_type_op_try: if either operand’s type registers a handler for the operator,
the handler is called as (handler a b) and owns the coercion. When both sides
carry one, the same type takes a’s; otherwise the side whose type declares a
conversion FROM the other absorbs it; with neither declaring, the operator falls
through. Offered by + - * / %, < and =, and not by the bitwise operators.
x_heap_mark_phase opens with x_heap_run_hooks(mark_hooks);
x_heap_sweep_phase opens with the free hooks. Mark hooks run BEFORE any
marking: a hook’s allocations are born unmarked, and a hook that runs after the
mark passes can have an allocation that escaped into reachable state freed by
the same sweep.
apply is a tail callx_prim_apply binds the parameters and returns x_eval_body_tco.
lib/x/core/control.x expands (let ...) to (apply (eval (fn ...)) vals);
an apply that settles grows the host stack under every let in tail position.
STOPx_eval_start reads the flag every iteration. Set, with an error handler
active, it clears the flag and raises STOP; it clears before raising, and it
raises only under an active handler. x-lang sets %sigint-flag directly to
test it.
An engine whose heap is its own array resolves an address inside it to a real
one before it crosses the foreign door, as it does for a string’s bytes.
Sys wait hands waitpid a four-byte region as (%str->ptr s) and reads the
status back.
byte-sub addresses bytesA buffer handed to a syscall is binary: (str make 4096) filled by
getdirentries64 holds a NUL in its fifth byte and records after it.
byte-sub addresses raw bytes, as byte-ref does.
A type make type with an eval handler decides what evaluating its
instances means; with a call handler its instances are callable; with
neither, an instance is itself and a form headed by one is data. Checked by
tests/x/conformance/core/handlers.spec.md.
An environment is (bindings . parent): bindings a chain of
(name . value) cells, or a tree of them; parent the enclosing
environment, or nil at the root. 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. Shipped in x-engine-c v0.2.10, which retired the frame marks,
the local boundary and the shadow list. (base def-global) stood in for it
and is retired too: a def evaluated with eval! binds in the root.
The rules:
bindings, then the parent’s, up to the root. The
frames the engine makes keep an alist; the root, and an environment whose
spine the language made with pair – a list pair, as a scoped module’s
is – keep a tree, so a module’s hundreds of names cost a lookup from
inside it one tree search, not a walk. pair? tells the two apart.def binds in the current environment: it updates the name’s cell
there if one exists, otherwise adds one. Never the parent.set! finds the name by lookup and updates the cell where it is found.
An unbound name is an error.eval with an environment makes it current, evaluates, and restores
the previous one. A def inside binds in the given environment and stays
bound. (eval (list (lit def) n v) e) is the definer every lang wanted.eval! evaluates in the root, as the loader evaluates a file’s forms.What the language builds on it: a module is an environment whose parent is
the root (Modules); a lang chooses its paradigm
with two operations, make a child and evaluate in an environment, so
Kernel’s $define! is def in a named environment and Scheme’s internal
defines get letrec semantics because a body’s definitions live in the
body’s own environment. None of it is decided in C. Checked by
tests/x/conformance/core/evaluation.spec.md and spine.spec.md.
The compiled-C lane is undeclared. x/tool/compile.x builds against the
engine’s own headers (-Iengine/include); nothing in x-engine.xon says an
engine hosts a compiled prim. boot/tower-compiled.x probes for the header
directories and keeps its interpreted analysers when they are absent;
ext/jit-*.spec.md and ext/asm.*.spec.md assume the lane.