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.mdfor canonical hardware spec and ADR-0011 for the current system image update path.
Context
Section titled “Context”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.
Forcing functions
Section titled “Forcing functions”- 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.
Constraints
Section titled “Constraints”- 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 SDL2 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.
Decision
Section titled “Decision”Cartridges are authored entirely in Lisp. C is reserved for the nOSh runtime.
Concretely:
- 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
.lspsource. A cart compiles to a.kn86container holding Lisp bytecode plus static data (sprites, PSG patterns, strings). - Lisp wraps the C FFI. The
NoshAPIvtable (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. - Interpreter: a small bytecode VM. Arena-allocated, no GC. Arena is reset at cartridge load and at mission-instance boundaries, bounding memory deterministically and eliminating GC pauses entirely. Bytecode compiled ahead of time by the desktop toolchain; the device loads bytecode, never source. (REPL and nEmacs — see ADR-0002 — include on-device reader and compiler for player-facing use.)
- 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.
- 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)”| Component | Budget |
|---|---|
| Bytecode VM code | ≤ 48 KB flash |
| VM working state (stack, FFI bridge) | ≤ 8 KB SRAM |
| Arena per active cartridge | 16–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.
Options Considered
Section titled “Options Considered”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.
Option D: Full uLisp with GC
Section titled “Option D: Full uLisp with GC”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.
Trade-off Analysis
Section titled “Trade-off Analysis”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.
Consequences
Section titled “Consequences”What becomes easier
Section titled “What becomes easier”- 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.
.kn86format is more portable, more moddable, and more inspectable than ARM object code.- Dual-target builds simplify — the VM is the same on SDL2 and the device.
What becomes harder
Section titled “What becomes harder”- 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.
What we’ll need to revisit
Section titled “What we’ll need to revisit”- Interpreter choice: uLisp (adapted for arena), Fe (~800 LOC), or custom from-scratch. Spike task.
- FFI surface: exact enumeration of
NoshAPIprimitives 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.
Action Items
Section titled “Action Items”- 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 SDL2 emulator, arena-allocation compatibility. Output: recommendation + micro-benchmarks.
- Design: FFI surface enumeration (Embedded Systems, 2 days, parallel). Full list of
NoshAPIprimitives exposed to Lisp, with Lisp signature and semantic contract. Referencenosh.handnosh_stdlib.c. - 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.
- Design: Cart format v2.0 (Embedded Systems, 2 days, after #1). Bytecode section layout, static data, header.
- Spec: Write
KN-86-Cartridge-Grammar-Spec.mdv2.0 (after #1–#4 land). Mark v1.1 Superseded. - Implementation: VM + cart loader (C Engineer, TDD + git-flow, after sign-off on spec v2.0).
- In parallel: GWP-86/90/94 proceed with C handlers against the tagged dispatch contract. No pause.
- QA: Paradigm compliance re-run against
KN-86-Lisp-Paradigm-Revisions.mdonce 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.