x-lang is a language, not an implementation. x-engine-c is one engine; another
may be written in any language that can meet the terms below. This document is
those terms — what an engine must provide, what it must promise, what it merely
reports, and how the language finds out.
It is written for someone building a second engine. Everything here is checked by something in the tree; where a claim is not checked, it says so.
An engine describes itself in x-engine.xon. Three kinds of statement live there,
and they are compared three different ways. Collapsing them is the mistake the
vocabulary exists to prevent.
| kind | means | compared by |
|---|---|---|
| capability | a group of instructions is reachable | superset — a richer engine is never refused |
| guarantee | a behaviour the engine promises, usually by not doing something | must-hold |
| parameter | a value the engine reports (word size, byte order, arch) | never a requirement; checked where consumed |
A parameter must never appear in a requirements list. word-size = 8 as a global
requirement would lock out the 32-bit Pi, which is a supported target — and it
would be false besides, since the layout descriptors are expressed in words and the
library sizes a word at boot. Where a specific module genuinely does depend on a
value, it carries a row in tools/contract/constraints.x and a marker at the code.
Parameter values are vocabulary too. os is spelled darwin, not macos;
arch is arm64, not aarch64. Those spellings are declared beside the parameter
in features.x and checked by the gate — against every value constraints.x binds,
and against an engine’s x-engine-build.xon when it has been built. A closed value
set is not a requirement: it says which values this vocabulary can spell, and
only constraints.x may bind one. unknown is always legal and means the build
could not say.
The closed vocabulary is tools/contract/features.x. The language owns it: an
engine that defined the terms would be choosing the ones it is judged by — which
is exactly how the parameter spellings went wrong. They were real and enforced,
but only inside one engine’s build script, so a second implementation had to
reverse-engineer them from that script and a spec file.
Groups partition the engine’s instruction manifest (tools/contract/isa.x).
tools/check/engine-contract.sh proves that partition total and disjoint, so a
new instruction cannot appear unclassified.
A capability means the coordinates in that group resolve — (prim-ref 'ns 'method)
finds something callable, or the bare name is bound. It does not mean
“implemented natively”. lib/x/boot/reflect.x already replaces engine primitives
with x-level ones under the same catalog names. The contract is the coordinate, not
the language it is written in.
Two implementations exist, so this is a real question with a wrong answer
available. meta/identity is the group that answers it at runtime, and core
requires it:
| binding | what it answers |
|---|---|
x-release |
the release this build was cut as |
x-version |
the implementation’s own version number |
Both are non-empty strings; nothing else about their content is specified.
x-lang compares release strings for equality and never parses them, so any
spelling works — a tag, a hash, dev — as long as two different builds are two
different strings.
Your x-engine.xon answers the same question about the directory it sits in,
which is right for a build system and wrong for a bug report: a wrapper can be
pointed at one engine and run another. These bindings answer for the process.
x -V prints both, and the pin records the engine’s release so a project can be
paired with the engine it was verified against.
meta/platform (x-machine, the build triple as a bound value) is in the
vocabulary but in no profile. The declared (param os ...) / (param arch ...)
rows beside the binary are the platform door now; x-machine is the fallback for
an engine that ships no build params.
Values are part of the surface. %isa-values — the names an engine binds
rather than the instructions it registers — is partitioned like everything else.
It was skipped by every parser until 2026-08-23, which is how x-release came to
be something the wrapper depended on and no engine was obliged to have. If your
manifest lists an instruction group, list its bound values too: #t and #f
belong to isa/spine, args to invoke/argv, %token-eof to isa/tok,
%sigint-flag to isa/sys.
A profile is not a boot. Capabilities say which INSTRUCTIONS resolve. They
say nothing about what the engine’s base carries, and the library walks base
routes by name at runtime — type-alist, type-iter, err and a dozen
more. Decision L1 makes the STEPS an engine’s own business, so that a different
object model can arrange its base differently; the NAMES are not negotiable.
make check-base-routes derives the set from the library’s call sites and holds
the engine’s base-paths.x to it. A second engine reached core with two routes
declared and sixteen required, and nothing else noticed.
Coverage, not presence. An engine provides a group when it has every coordinate in it, measured against the reference engine’s manifest — the language’s own view of what instructions exist. One row does not buy a group. The direction is deliberate: a partly-built engine under-declares and is refused early by the contract gate, rather than being accepted and dying at runtime on the first instruction it never implemented.
A tag is not a group. The ffi tag carries eleven instructions that split three
ways, and treating it as one would make dlopen mandatory for every engine
including a sandboxed one:
reflect/ptr-casts — object↔pointer↔integer materialization. Mandatory.
The boot reads object header words through these.isa/ffi-call — dlopen, dlsym, calling through a pointer. Optional.isa/syscall — the raw kernel door. Optional.lib/x/boot reaches all six casts and reaches the foreign door zero times.
Behaviours no manifest can show, because they are things the engine does not do. The library’s correctness rests on them anyway.
gc/explicit-only — allocation never collects; only an explicit call does.gc/non-moving — a live object’s address is stable for its lifetime.eval/tco — proper tail calls, unbounded.str/nul-terminated — a string value is a C string; bytes past the NUL are
unobservable.int/ptr-same-width — the fixnum and the pointer are the same width.err/typed-raise — a raise delivers a value of a REGISTERED type carrying
two slots, (code . subject), and the base’s err row holds a value of
that same type.Guarantees run one way — the engine promising, the library relying. There is one obligation that runs the other way, and it belongs here because nothing else in the vocabulary carries it:
Operatives are banned inside the tokenizer’s read. Code reached from a reader
callback must use the primitive if, never an op (docs/syntax.md states this as
a ruling, and lib/x/reader/lit-reader.x is written to it).
This was previously mis-declared as a guarantee named tok/callback-no-alloc,
on the belief that callbacks must not allocate. That belief is obsolete —
lit-reader.x records that re-entering the tokenizer from a reader handler is safe
because collection is explicit-only — and the real constraint is about operatives
rather than allocation. It is a requirement on callback authors, so it is written
here rather than declared as something an engine provides.
The first two guarantees are not academic. Six sites in the library hold a raw pointer as an integer across an allocating expression on the collection promise alone. An engine that collected during allocation would break all six with no error and no crash at the point of damage.
eval/tco is semantic, not a performance note: the library recurses in tail
position throughout and binds a tail def globally because of it. An engine
without tail calls does not run slowly, it overflows the stack in ordinary code.
err/typed-raise exists because the language words errors and the engine does
not. An engine that flattens its message and the thing it is complaining about
into one English string leaves nothing to reword: the structure is gone before
x-lang sees it, and a value with no type has no dispatch stacks to push a
handler onto. So a raise must deliver the two facts apart, on a type — the code
(the raise site’s message) and the subject (the name it was about, or the empty
string when it names none). lib/x/type/err-io.x reaches that type through the
base’s err row and pushes the default prose onto its write/display stacks; a
lang pushes its own over that and pops it again, the same way char-io.x fills
CHARACTER’s. Spell the prose in C and every lang inherits English it cannot
replace.
What the guarantee does not require is identity. The reference engine reuses one
base-resident instance so a raise allocates nothing — which is why a caught error
must be read before the next raise overwrites it — but an engine that allocates
per raise conforms equally. tests/x/conformance/core/errors.spec.md is the
executable form of all of this, and tests no eq? between two raises.
Four tiers, so a partial engine has a target instead of an all-or-nothing wall. Each includes the one before it.
| profile | adds | boots |
|---|---|---|
core |
the spine, allocation, machine ops, raw memory, types, the reader, byte I/O, reflection, identity | the language |
gc |
collection | the REPL loop, the GC module |
posix |
OS facilities, the syscall door, the foreign door | File, Proc, sockets |
full |
coverage and profiling instrumentation | the tooling |
The interesting boundary is core|gc: an engine with no foreign door, no
syscalls and no collector still boots x-lang. Of roughly 150 files in lib/ and
apps/, eighteen need anything above core. That is the shape of the sandbox
dialect, and the first target worth aiming a new engine at.
The tiers are what the library is, not a tidy diagram. posix cannot be
separated from the foreign door because lib/x/sys/posix.x fetches dlopen
alongside syscall.
How the wrapper drives the engine. This was assumed everywhere and written down nowhere until this document.
The engine owes exactly three things:
args.*** ERROR: .That is all. In particular the engine parses no flags. --batch, --quiet
and --no-color are read by x-lang code (lib/x/repl/banner.x,
lib/x/tool/contract.x), and the reclaiming of terminal input from file
descriptor 3 is lib/x/repl/loop.x calling dup2 through the syscall door. Those
are conventions between the wrapper and the library; an engine that binds args
and offers the syscall door supports them without knowing they exist.
Two forms may be emitted ahead of the program as data, never evaluated by the shell: the install root, and a project’s pin manifest path. Both are consumed by library code after boot.
A bare engine has no printer — display and write are x-lang — and does not echo
results either. With no library loaded, the only way a program can be observed is
by raising an error. Both bare suites rely on this.
tools/contract/isa.x the instruction manifest, with a tag per row
tools/contract/obj-layout.x object header layout, in WORDS
tools/contract/base-paths.x interpreter state as first/rest walks
tools/contract/base-layout.x the base spine descriptor
tools/contract/claims.x the guarantees it asserts
x-engine.xon generated self-description
The layout descriptors travel with the engine because the library is reflective: it reads object header words directly. Every engine ships its own, and the library includes the booting engine’s. This makes word-addressability a permanent requirement — an engine must expose object↔pointer casts and word load/store over a flat arena — but it does not make any particular width a requirement.
x-engine.xon is generated by x-lang’s tools/contract/gen-engine-xon.sh run
against the engine directory. Capabilities, profiles and digests are derived from
the engine’s own files — a capability by covering the reference roster for its
group, as above; guarantees are copied from claims.x. An engine directory with
no x-engine.xon is refused, not skipped: there is nothing to satisfy. Parameters are
absent by design: word size and architecture are facts of a build, not of a
source tree, and are stamped beside the binary at install time.
Three suites, asking three different questions.
The contract gate (make check-engine-contract) reads only files. It holds the
capability partition against the instruction manifest, keeps profiles closed,
refuses a parameter in a requirements list, re-derives what the library needs from
its own call sites, and answers the resolver’s question: does this engine provide
what x-lang requires? The checks of the library run in x, on the tree’s own build,
and read its files as forms. The engine being judged is never run: its declaration
is read in shell, so an engine that cannot run x is still refused by name.
Conformance (make conformance) asks is this a correct x-lang evaluator? It
is the language’s definition of correct, it loads nothing, and it runs against any
engine via X_BIN. It lives here rather than in an engine because an implementation
that owned it would become the arbiter every other implementation is judged
against.
Compliance (make check-compliance) asks does this engine do what it claims?
Every check is generated from a row of the engine’s own x-engine.xon, so the
suite cannot drift from the declaration it audits. It matters because the contract
gate compares declarations as text: an engine that over-declares passes it, is
chosen, and fails in the field — loudly for a capability, silently for a guarantee.
Under-declaring is harmless; an engine is simply treated as less capable than it is. So compliance only ever tests in the over-declaring direction.
Coverage of the conformance suite is a ratchet, not a target: it may grow freely, and a row that loses coverage fails. Rows with no case carry their reason in prose beside the subject, because “no case exists” and “no case should exist” are indistinguishable in a report.
The order that gets you running soonest:
core profile, and ship the four contract files describing your own
layout. tools/contract/features.x lists what core names.claims.x for the guarantees you actually make. Claim less rather than
more: under-declaring costs you capability, over-declaring costs correctness.x-engine.xon with x-lang’s generator.X_BIN.gc, then posix, as you want the library tiers that need them.The bar for step 1 is lower than it looks — no collector, no syscalls, no foreign door — and higher in one specific way: the reflective library needs word-addressed objects, so an arena with raw word access is not optional.