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 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.

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 a .kn86 container holding Lisp bytecode plus static data (sprites, PSG patterns, strings).
  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.
  3. 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.)
  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 SDL2 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 SDL2 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.