ADR 0007: Scheme scope: written from scratch, documented first
- Status: accepted
- Date: 2026-09-20
Context
No interpreter exists. The kernel is the surface everything else sits on, and
the model will read its documentation when writing .scm for nodwyn in P1b.
Decision
docs/scheme.md is written before the code and is the contract: syntax, data
types, special forms, builtins, the error model, the primitive-table contract,
and an explicit list of what is not in nodwyn Scheme. A builtin without a doc
entry and a conformance snippet fails the suite.
Bootstrap kernel: define lambda if let let* begin set! quote quasiquote cond
and or; int64 and float64; strings; lists; association lists; error;
proper tail calls. Phase 1b adds letrec case do, vectors, string ports,
guard, and define-macro only if the prelude needs it.
Never in v0: hygienic macros, call/cc, dynamic-wind.
Consequences
evalandapplyare two fixed Go functions; loading.scmcannot add special forms.TestKernelFixedchecks that loading Scheme grows the primitive table and no new form appears (amended 2026-10-16).- Scope growth is a documented decision, not drift.
Amendment 2026-10-16: no kernel fingerprint
The kernel fingerprint (a hash of a version string and the special-form
names) was said to check that loading code cannot change evaluation. It could
not: no Scheme code reaches either input, so the test passed by construction,
and a change to how define or guard evaluates left it unchanged. It is
removed. What TestKernelFixed really asserts stays (the primitive table grew
by exactly the registered tools; define-syntax stays a plain binding), and
evaluation semantics are held by the conformance suite.
Amendment 2026-10-16: nesting is bounded
The evaluator had no depth limit. A call that is not in tail position recurses
in Go, so a runaway recursion (a model's mistake, or an init.scm wrapper
that captured itself on (reload)) grew the Go stack to its gigabyte limit
and ended the process: the daemon and every session in it. So did a vector
that holds itself, through the printer and equal?, a file that loads itself,
and a datum nested deeply enough in the reader.
Evaluation now counts the Evals in progress and raises an ordinary,
catchable error past scheme.MaxDepth (10,000); tail calls loop inside one
Eval and do not count. A nested Eval costs one to five kilobytes of Go
stack, measured on 2026-10-16 with and without -race, so the limit stays
near 50 MB. The reader, the printer, equal? and json-emit take the same
bound, and load refuses a file it is already loading. The kernel's
semantics are otherwise unchanged (docs/scheme.md §3).
Amendment 2026-10-22: the interpreter is Brote
The interpreter was extracted from internal/scheme into its own module,
github.com/fclaude/brote (package scheme), so that nodwyn and the Coigüe
editor embed one implementation of this language. What this amendment decides:
- Brote owns the language. Values, reader,
eval/apply, builtins, the primitive table, the prelude, the conformance suite (TestConformance,TestDocCoverage,TestKernelFixed,TestTailCalls,TestGateIsTheSingleExitand the rest ofscheme_test.go) and the language reference now live in Brote. Nodwyn keeps the host primitives, the guard, credentials, the loop and everything that registers a primitive or installs a gate. - The extraction changed nothing. Brote's
scheme/isinternal/schemeat commit3846cd7f, file for file, but for the package comment and the docs path in one test. Error strings and doc strings that nodwyn's tests and prompts assert on are unchanged, and the API still carries what nodwyn needed (egress declarations, provenance tags, tool schemas, origin, the gate). Narrowing that API waits until a second host shows which parts it uses. - How nodwyn gets it.
go.modrequires Brote at its tag, v0.1.0 (a later change to the language is pinned by commit until it is tagged). Brote is public on GitHub, so a stock Go environment fetches it with noGOPRIVATEand no git configuration (a cleangit archiveof this tree builds that way). Brote's MIT licence is bundled intonodwyn licenseslike any dependency's. nodwyn replandnodwyn evalrun Brote'sreplpackage andscheme.LoadFile; the standalone interpreter for everyone else isbrote.docs/scheme.mdkeeps the language sections so that it reads whole, and says that Brote's reference wins where they differ. Two copies drift; once Brote's repository is public this file links to it for those sections instead.