Skip to content

ADR-0001: Lisp as the Sole Cartridge Authoring Language

Hardware retarget note (2026-04-21): This ADR was originally written when the production target was RP2350 / Pico 2 (520 KB SRAM, 4 MB flash). That target has been dropped; the KN-86 Deckline ships on the Pi Zero 2 W (512 MB RAM, SD filesystem). The Lisp-substrate decision below still holds — the memory budget constraints that shaped it are now trivially satisfied, so every tradeoff was made against a stricter envelope than we actually need. The numeric budgets in this doc are retained as-is for historical traceability; they are conservative on current hardware. See repo root CLAUDE.md for canonical hardware spec and ADR-0011 for the current system image update path.

The KN-86 Deckline presents a Lisp-flavored interaction model to the player — CAR / CDR / CONS / EVAL / QUOTE keys drive navigation and action across every capability module. The fiction is that the device is a Lisp deck. Cartridge Grammar Spec v1.1 (2026-04-02) explicitly rejected a Lisp runtime, defining cartridges as C compiled to native ARM via a macro authoring surface. That decision was correct for perf, but it left the device’s Lisp identity as a UX veneer with no substrate behind it, and forced cartridge authors into a two-language C-plus-data-tables source model.

This ADR commits the platform to a Lisp substrate.

  • Fiction / substrate alignment. A device that claims to be a Lisp deck should actually run Lisp — for the QUOTE key to have a referent, for the REPL to be real, for the authoring story to match the device’s own self-description.
  • Authoring accessibility. 14+ modules planned. Content authors (designers, not necessarily nOSh runtime engineers) need a surface that doesn’t require a C toolchain, linker, or dual-target build environment.
  • Fresh-eyes correction. The v1.1 decision was made under the assumption that “any Lisp means an interpreted hot path, which means perf risk.” That framing was too binary. A correctly-designed VM running off the render/audio critical path does not introduce perf risk, and the device does not actually require the perf envelope v1.1 was protecting.
  • Historical memory envelope (RP2350, 520 KB SRAM total): interpreter + firmware + display buffer + audio buffer + active cart data had to fit. This constraint is retained because it drove the arena/no-GC design; on Pi Zero 2 W (512 MB RAM) the envelope is trivially satisfied.
  • Perf envelope: display and audio callbacks stay in C. Handler dispatch (input events) runs in Lisp but must complete within the input latency budget (~1–5 ms typical).
  • Dual-target builds: cartridges must load on both the desktop SDL3 emulator and the device firmware without source change.
  • In-flight work: four launch-title gameplay specs, cell-architecture implementation tasks (GWP-86/90/94), and ~1,800 LOC of emulator scaffolding already assume the C handler surface. The decision must offer a migration path, not a throwaway.

Cartridges are authored entirely in Lisp. C is reserved for the nOSh runtime.

Concretely:

  1. Cartridge source is Lisp. Cell definitions, handlers, mission templates, phase chains, behavior tables, Cipher vocabulary, economy tuning, generation rules, and dialog all live as s-expressions in .lsp source. A cart compiles to is packaged as a .kn86 container holding Lisp bytecode Lisp source plus static data (sprites, PSG patterns, strings). (See the 2026-06-14 amendment: there is no bytecode artifact; the device parses and tree-walks source. AOT bytecode is a deferred future — see ADR-0006.)
  2. Lisp wraps the C FFI. The NoshAPI vtable (text_puts, draw_sprite, spawn_cell, drill_into, advance_phase, etc.) is exposed to cartridges as Lisp functions. Cart authors never see C — the platform’s C primitives appear as Lisp builtins. (Mechanism, per the 2026-06-14 amendment: the firmware vendors the standalone KEC Lisp language and registers these device primitives onto each Fe context via kec_bind_fe — NoshAPI is not part of the language, it is bound on top of it.)
  3. Interpreter: a small bytecode VM a small tree-walking interpreter (Fe). Arena-allocated , no GC with fixed-arena mark-sweep GC (no heap growth; see ADR-0004 2026-06-14 amendment). Arena is reset at cartridge load and at mission-instance boundaries, bounding memory deterministically and eliminating GC pauses entirely unbounded GC pauses. Bytecode compiled ahead of time by the desktop toolchain; the device loads bytecode, never source. The device parses Lisp source on load and tree-walks it; there is no Fe bytecode format. (REPL and nEmacs — see ADR-0002 — include on-device reader and compiler for player-facing use.)
  4. Handler dispatch via tagged union. The cell runtime accepts a handler as either a C function pointer (for runtime-level cells) or a Lisp lambda reference (for cartridge-authored cells). This lets the in-flight cell-architecture implementations land in C now and migrate to Lisp as the interpreter and authoring surface mature — the cell contract is identical either way.
  5. Redraw model shifts from fixed-60-fps to event-driven. Redraw fires on input, on explicit cart request, and at a 20 fps cap while an animation is active. Otherwise the render loop idles. This gives Lisp handlers ~50 ms of latency headroom per input event (vs. a 16 ms frame budget), and cuts device power draw. Audio callback stays at 44.1 kHz in C, independent of redraw.

Memory budget (original RP2350 envelope; trivially satisfied on Pi Zero 2 W)

Section titled “Memory budget (original RP2350 envelope; trivially satisfied on Pi Zero 2 W)”
ComponentBudget
Bytecode VM code≤ 48 KB flash
VM working state (stack, FFI bridge)≤ 8 KB SRAM
Arena per active cartridge16–32 KB SRAM, configurable per cart
Built-in REPL arena (see ADR-0002)24 KB SRAM when active
nEmacs edit buffer (see ADR-0002)16 KB SRAM when active

Totals are compatible with the 520 KB overall budget after display, audio, and firmware overhead.


Option A: Status Quo — Pure C Cartridge Grammar

Section titled “Option A: Status Quo — Pure C Cartridge Grammar”

Kept v1.1 authoring as-is. Lisp remains fictional veneer. Two-language source (C + data tables). C toolchain required. No REPL possible. Rejected: fiction/substrate drift, authoring inaccessibility, closed the Lisp-as-gameplay door.

Option B (refined): Lisp as sole authoring language (ACCEPTED)

Section titled “Option B (refined): Lisp as sole authoring language (ACCEPTED)”

Single-language cart source. Lisp wraps C FFI. Arena-allocated bytecode VM. Handlers run in Lisp. C is reserved for the nOSh runtime. Event-driven redraw (not fixed 60 fps). REPL and editor become built-in runtime capabilities (ADR-0002). Chosen: aligns fiction and substrate, collapses the authoring surface, preserves in-flight work via tagged dispatch, opens player-facing Lisp as a platform feature.

Option C (previous recommendation): Hybrid — Lisp for declarative data only, C for handlers

Section titled “Option C (previous recommendation): Hybrid — Lisp for declarative data only, C for handlers”

Lisp interpreter runs only at load/generation time, never during handler dispatch. Kept v1.1 C macros for handlers. Rejected in favor of B because the two-language source burden wasn’t worth the marginal perf savings. The perf savings were already notional — event-driven redraw removes the constraint that made Lisp-on-handlers scary.

Off-the-shelf uLisp with mark-sweep GC. ~40 KB code + dynamic heap. Rejected: GC pauses are the wrong risk profile for a device with real-time audio. Arena allocation gives us Lisp semantics without the GC hazard.


The core shift from the original ADR is realizing that v1.1’s perf framing assumed a 60 fps tick and a GC’d runtime. Both assumptions are removable. A text device does not need 60 fps — event-driven redraw with a 20 fps animation cap is plenty, and matches late-80s handheld aesthetic anyway. A bytecode VM does not need GC — arena allocation bounded by cart-load and mission-instance lifetimes gives predictable memory without ever running a collector. With both assumptions removed, the case for keeping C as the authoring surface collapses.

The remaining trade is interpreter footprint vs. authoring accessibility, and at a 48 KB code + 16–32 KB arena budget the math comfortably works on a 520 KB device.

The in-flight cost is real but contained: the cell-architecture tasks (GWP-86/90/94) land in C against the tagged dispatch contract, and the Lisp authoring surface ports them incrementally once the VM is live. No emulator work is thrown away.


  • Single-language cart authoring. No C toolchain required for content.
  • REPL, editor, and Lisp-scripted missions become coherent platform features rather than bolted-on extras (see ADR-0002).
  • Hot-reload of cart content on device becomes feasible.
  • The QUOTE key has a real semantic referent.
  • .kn86 format is more portable, more moddable, and more inspectable than ARM object code.
  • Dual-target builds simplify — the VM is the same on SDL3 and the device.
  • The platform owns a bytecode VM, reader, compiler, and debugger. Non-trivial firmware work.
  • Cross-language error paths (Lisp traceback into C FFI) require careful design.
  • Toolchain work: Lisp linter, bytecode disassembler, cart packaging tool.
  • Perf characterization: handler latency must be measured on-target before we commit to complex handlers.
  • Interpreter choice: uLisp (adapted for arena), Fe (~800 LOC), or custom from-scratch. Spike task.
  • FFI surface: exact enumeration of NoshAPI primitives exposed to Lisp.
  • Cart format v2.0: bytecode section, static data section, header revisions.
  • Cartridge Grammar Spec must be rewritten as v2.0, describing the Lisp authoring surface. v1.1 is marked Superseded.

  1. Spike: Bytecode VM selection (Embedded Systems, 3 days). Evaluate uLisp-adapted-for-arena, Fe, and a from-scratch minimal VM. Criteria: code size, handler-dispatch latency on representative Cipher/cell handlers, portability to SDL3 emulator, arena-allocation compatibility. Output: recommendation + micro-benchmarks.
  2. Design: FFI surface enumeration (Embedded Systems, 2 days, parallel). Full list of NoshAPI primitives exposed to Lisp, with Lisp signature and semantic contract. Reference nosh.h and nosh_stdlib.c.
  3. Design: Cartridge declarative surface prototype (Gameplay Design, 3 days, parallel). Re-express ICE Breaker’s cells, mission templates, phase handlers, and Cipher domain as Lisp. Validate every v1.1 authoring construct maps cleanly. Identify gaps.
  4. Design: Cart format v2.0 (Embedded Systems, 2 days, after #1). Bytecode section layout, static data, header.
  5. Spec: Write KN-86-Cartridge-Grammar-Spec.md v2.0 (after #1–#4 land). Mark v1.1 Superseded.
  6. Implementation: VM + cart loader (C Engineer, TDD + git-flow, after sign-off on spec v2.0).
  7. In parallel: GWP-86/90/94 proceed with C handlers against the tagged dispatch contract. No pause.
  8. QA: Paradigm compliance re-run against KN-86-Lisp-Paradigm-Revisions.md once the Lisp surface is live.

  • Frame-rate policy: event-driven redraw. Animations may request a 20 fps tick while active; idle otherwise. Audio callback is independent at 44.1 kHz.
  • Depthcharge sonar sweep is the only launch title that needs the animation tick. Verified against gameplay spec.
  • Handler latency budget: 5 ms target, 10 ms ceiling. Exceeding this is a bug to be profiled.
  • Player-facing Lisp surface (REPL, nEmacs, scripted missions) is scoped in ADR-0002.

2026-06-14 — Tree-walking runtime, no bytecode; language vendored as a library (clarification)

Section titled “2026-06-14 — Tree-walking runtime, no bytecode; language vendored as a library (clarification)”

Status effect: Accepted & Shipped (unchanged). Fe remains the selected substrate and Lisp remains the sole cart authoring language. This amendment corrects framing/terminology and records how the language is now packaged; it does not re-open Options Considered or change the Decision. Companion to the ADR-0004 2026-06-14 amendment and the ADR-0006 2026-06-14 amendment.

What was conflated. The Decision above (§1, §3) described carts that “compile to Lisp bytecode” run by “a small bytecode VM” with the device loading “bytecode, never source.” None of that was built. The selected runtime, Fe, evaluates by walking the cons-cell AST directly — it is a tree-walking interpreter with no compiler, no instruction set, and no bytecode format. Carts ship Lisp source in the .kn86/.kec container; the device parses and tree-walks it on load. The bytecode lines in §1/§3 are struck through. AOT bytecode is a deferred future (parked design at kec-lisp/docs/bytecode-vm.md; revisit triggers in the ADR-0004 amendment), not current behavior — see the ADR-0006 amendment for the cart-format consequence.

No GC → fixed-arena mark-sweep. §3’s “no GC” was the original aspiration. Fe is in fact a mark-sweep collector over a fixed-size object pool carved from the arena: it reclaims dead objects but never grows the pool, so there is no heap growth and no unbounded pause — the property §3 wanted, achieved with a collector rather than without one. The arena-reset-at-boundaries discipline is unchanged and real.

Language vendored as a library. As of the 2026-06-14 split, KEC Lisp is a standalone language repo (github.com/Kinoshita-Electronics-Consortium/kec-lisp): the Fe kernel (vendored rxi/fe), KEC Core (stdlib in Lisp), portable host primitives, the embedding API, and the kec CLI. The KN-86 firmware vendors that language and registers the NoshAPI device primitives (graphics, audio, save, missions, CIPHER) onto each Fe context through one FFI seam — kec_bind_fe. NoshAPI (ADR-0005) is therefore device primitives bound onto the vendored language, not part of the language itself. The standalone boundary is specified at the KEC Lisp site. A context’s capability tier is which primitives are bound into it (KEC_PROFILE_SANDBOX vs KEC_PROFILE_FULL, plus the firmware’s own cart / mission / system-render tiers) — see ADR-0005.

Memory-budget note. The “Bytecode VM code ≤ 48 KB flash” line in the Memory budget table is part of the frozen, historical RP2350 envelope (retained for traceability per the table header); read it as “interpreter code,” and trivially satisfied on the Pi Zero 2 W. The arena/working-set numbers are reconciled to current measurements in the ADR-0004 amendments.