ADR-0001: Base-Language Additions for knEmacs & Cart Authoring
- Status: Accepted
- Date: 2026-06-21
- Deciders: KEC Lisp maintainers
- Supersedes / superseded by: —
- Amended: 2026-06-22 by GWP-235 (strict integer contracts, nil-aware binding presence, context-local RNG, and one-character padding)
Hardening note. The original sprint deliberately avoided kernel changes. GWP-235 later added small, additive kernel APIs after integration review showed that true bound-to-
nildetection and composable foreign-pointer lifecycles could not be implemented safely through the original seams alone.
Context
Two reading studies — the GNU Emacs Manual and
Writing GNU Emacs Extensions —
were mined for what KEC Lisp the language must provide to host knEmacs
(the on-device editor, formerly nEmacs) and to serve cart authoring. The gap
analysis was then verified against the actual source (kernel/, core/,
host/, runtime/kec.c) rather than inferred. That verification matters because
it corrected several first-pass conclusions:
Already present (no work needed):
- Error catching:
try+raiseon a setjmp/longjmp guard stack (runtime/kec.c); errors are values —(:error . message)witherror?/error-message(core/35-error). - Feature registry:
provide/provided?/require(runtime/kec.c). gensym(hygienic macros),equal?(structural equality),let*/letrec/when/unless/case/dolist/dotimes(core/40-ctrl),eval/apply/macroexpand-1/read-string/read-all(runtime/kec.c), reflectionglobals/fn-params/bound?(host/),sortwith a comparator,reduce/fold-*(core/50-hof).
Enabling kernel facts:
- The kernel exposes a foreign-pointer type
FE_TPTRwith a GC handler hook (fe_ptr/fe_toptr,handlers.gc) — new aggregate types can be host objects without touching the frozen kernel. - The kernel carries an instruction budget and a setjmp/longjmp error seam;
numbers are single-precision
float(exact ≤ ±2²⁴);=/iscompare pairs by identity (equal?for structure); assignment isset;nilis the only false value and the empty list.
What remains splits cleanly by cost: pure-Lisp Core modules, simple host
number/string primitives, and (riskier) foreign-object containers. The
device discipline — arena allocation, no malloc in the runtime,
GCSTACKSIZE 256 — is the binding constraint that decides what is in scope now.
Decision
Accept the following for the immediate sprint. All of it is either a Core
.lsp module or a simple host number/string primitive; none touches the
frozen Fe kernel, and none introduces runtime malloc.
A. Error-recovery macros (Core, over the existing try/raise)
unwind-protect— run a body, then run cleanup forms on both normal return and a raised error (re-raising afterward to preserve error semantics). This is whatsave-excursion/save-restriction-style wrappers need.ignore-errors— evaluate a body, yieldingnilon any raised error.condition-case— catch a raised error and evaluate a handler (message-based; class dispatch is deferred — see below).
Reference shape: (unwind-protect body . cleanup) ⇒ catch with try, run
cleanup, re-raise if the result was an error?.
B. Small utilities (Core)
prog1— sequence; return the first subexpression’s value.defvar— define a global only if unbound ((if (bound? 'x) x (set 'x v))), so user/config values, includingnil, survive a later library load.macroexpand— full expansion (loopmacroexpand-1to a fixpoint).
C. String / char toolkit (Core, over existing host string primitives)
- Case:
string-upcase,string-downcase,char-upcase,char-downcase. - Layout for the fixed-cell text grid:
pad-left,pad-right,string-repeat. Padding accepts exactly one fill character. - Tests:
string-prefix?,string-suffix?,string-contains?.
D. Bitwise operators (host primitives)
bit-and,bit-or,bit-xor,bit-not,bit-shl,bit-shr, operating on validated 32-bit integer-valued numbers. Fractional/non-finite/unsafe inputs raise instead of narrowing. Needed for PSG register packing, RGB565 color math, the Universal Deck State history bitfield, and flag sets — none of which the editor-focused study surfaced.
E. Seedable RNG (host primitive)
- A deterministic seed control (e.g.
set-seed!/rng-seed) sorand/rand-intbecome reproducible per interpreter context. The mission board generates contracts from cartridge templates seeded by deck state; reproducible procedural generation is load-bearing for that core loop, not a nicety.
Deferred (accepted in principle; separate ADR + sprint)
- Containers — vectors and hash tables. High value (O(1) indexed grids/rings;
O(1) keyed behavior/generation/save tables), and implementable as
FE_TPTRhost objects — but their backing memory (hostmalloc+ a GC finalizer vs. an arena slab vs. a fixed pool) conflicts with the no-mallocarena invariant, and their key-equality semantics interact with KEC’s identity-vs-structural rules. They warrant a dedicated design decision rather than being rushed into a “complete it” sprint. - Typed/structured errors — enrich the error value to
(:error type . data)socondition-casecan dispatch by class; pairs with A when needed. autoload/eval-after-load— lazy load + post-load hooks (the registry,provide/require, already exists).- Regex subset +
regexp-quote— the deferred “expensive tier”; only literalstring-searchtoday. equal?cycle-safety — bounded/seen-set traversal so a cyclic structure cannot hang the device.- Exact integers beyond ±2²⁴, optional float trig (
sin/cos/atan2).
Rejected (won’t do)
- Tail-call optimization in the frozen kernel. Keep the deliberate
iterative-Core discipline (
GCSTACKSIZE256); TCO is invasive and against the frozen-kernel stance. - A numeric tower, or a namespace/module system beyond
provide/require.
Consequences
- knEmacs’s command loop, REPL error survival, and
save-excursion-class wrappers become expressible in Lisp (A). - Cart authoring gains bit manipulation (D) and reproducible procedural generation (E); the text-grid + editor case/layout ergonomics improve (C).
- The original sprint made no frozen-kernel changes and introduced no new
runtime
malloc; the later GWP-235 hardening amendment is described above. Containers were explicitly queued rather than dropped. - New Core modules wire into
CORE_SRCS(load order) inCMakeLists.txtand intomkembed; host primitives register inkec_host_register. Every new form ships conformance tests, and thedocs/language reference / builtins page is updated.
Acceptance criteria
ctestgreen on ubuntu + macos; every new form has conformance tests (tests/), andkec testover the new files passes.unwind-protectruns cleanup on normal return and on a raised error;condition-casereturns the handler value on error and the body value otherwise;ignore-errorsyieldsnilon error.bit-*results match a reference table;randunder a fixed seed is reproducible across separate runs.- The string toolkit handles empty strings and pad/truncate boundaries.
- Docs updated; the field-notes files corrected to reflect that
try/raiseandprovide/requirealready exist (and that error-recovery is a Core-macro task, not a kernel change).
References
- Field Notes: Writing GNU Emacs Extensions
- Field Notes: GNU Emacs Manual
- Source seams verified 2026-06-21:
runtime/kec.c(try/raise/provide/require),core/35-error.lsp,core/40-ctrl.lsp,host/host.c,kernel/fe.h/fe.c(FE_TPTR, instruction budget, error seam).