REPL: onboard Lisp read-eval-print loop
First-party on-device program #3 (ADR-0042).
v1.0 | 2026-07-18
Ladder placement. This is the REPL’s Tier 2 program landing page: what the operator reaches, invokes, and gets. The REPL is a shipped runtime utility whose detailed design is Tier 4: the read-eval-print algorithm, loop semantics, history walking, and FFI access policy live in the implementer-facing reference
../api-reference/editor-tools/repl.mdand the governing ADRs (ADR-0002, ADR-0016), which win over this page. No T1 treatment or T3 detailed-design doc is owed at the program tier; the ADRs carry that design. This page distills the basics and links down; it does not restate the T4 content.
Canonical for: ADR-0002 §1 — the player-facing Lisp REPL as a release-default, first-class onboard utility on the KN-86 Deckline.
Companion docs:
../../adr/ADR-0002-player-facing-lisp.md— original commitment to a player-facing REPL, tutorial role, history budget, FFI exposure.../../adr/ADR-0016-nemacs-repl-input-model.md— unified input model with nEmacs.nemacs.md— the sibling program in the sameprograms/directory; one subsystem, two buffer types (ADR-0016).../runtime/bare-deck-content-brief.md— bare-deck teaches Lisp grammar; the REPL executes arbitrary Lisp. Two independent surfaces with explicit boundaries (see §9 of that doc).../api-reference/editor-tools/repl.md— implementer-facing reference for loop semantics, history walking, error handling, and cartridge access policy.
The Canonical Hardware Specification in ../../../CLAUDE.md governs the 128×75 grid, Row 0 / Row 74 firmware ownership, and the TERM key — see that table; values are not restated here.
1. Identity
Section titled “1. Identity”The REPL is a release-default, first-class onboard program on every shipping KN-86 Deckline. It is not a cartridge. It is not a developer-only build affordance. It is part of the device.
Per ADR-0002 §1: “A runtime-level utility reachable from the deck home screen (alongside Mission Board, Cipher, Deck State). Presents a standard read-eval-print loop over the same VM defined in ADR-0001.”
GWP-120 landed the toast overlay surface as a KN86_DEV_BUILD-gated dev affordance. GWP-315 promoted that surface to release-default, removed the build-flag gate, and added the persistent surface advertising and per-deck history persistence that ADR-0002 always required.
2. Invocation — the Quake-style drop-down (ADR-0053)
Section titled “2. Invocation — the Quake-style drop-down (ADR-0053)”The REPL is invoked by the TERM key (see CLAUDE.md key matrix). Press TERM over the bare deck or a system screen (board, SYS, …) and the REPL drops as a partial overlay over a frozen, pixel-preserved screen — the equake / Yakuake / Guake model realized (ADR-0053, reviving the ADR-0016 §3F “toast” vision):
- On TERM (drop): the runtime takes a byte-exact snapshot of the current screen (
render_snapshot()), pauses that screen’s tick, and the REPL console band slides down from Row 0 to a fixed ~40% height (30 cells / 240 px — not operator-configurable). The console band carries its own Row-0 modeline (REPL [TERM:EXIT]), the scrollback, a rule, and the prompt pinned to the band’s bottom edge. - What shows through: the frozen screen below the console reads dimmed — a dither scrim (
composite_scrim) over the revealed band — with a drop shadow (composite_shadow) under the console’s bottom edge for separation. The frozen firmware action bar (the base’s Row 74) shows through dimmed below. - On the second TERM (close): the console slides up, the screen is restored byte-exact (
render_restore()), and the underlying tick resumes.
The slide rides the GWP-644 idle-timer (run-with-timer, ~100 ms envelope, a tunable constant), lazily armed on first render like the FIREPLACE / animlab surfaces; the prompt cursor blinks on the same timer while the console is open.
Hide, don’t destroy. A second TERM hides the console but keeps the REPL session warm: the scrollback and any in-progress (uncommitted) input persist between toggles, on top of the per-deck history ring (§4). Re-opening restores the last state, exactly like equake.
Reach. The drop-down opens over the surfaces where the REPL is reachable today — the bare deck and system screens. A running cartridge keeps the TERM key (the cart’s binding); nEmacs keeps TERM as its own exit. The TERM context-sensitivity table is unchanged (see ADR-0016 §3C.1 priority table); higher-priority surfaces — CIPHER-LINE seed capture, hot-swap defer, Cipher freeze, cart-scoped binding — claim TERM first. The REPL drop-down is the priority-5 default binding.
3. Permanent surface advertising
Section titled “3. Permanent surface advertising”The REPL is always reachable, so the firmware always advertises it. The right edge of Row 74 (firmware action bar) carries a permanent indicator:
[TERM:LAMBDA]when the REPL is closed (the frozen base screen’s own action bar).[TERM:EXIT]carried on the console band’s own Row-0 modeline while the drop-down is in any visible phase (the console owns its Row 0; the frozen base’s Row 74 shows through dimmed below).
The advertisement occupies a right-edge slice of Row 74. It is painted by the runtime AFTER cartridge / bare-deck Row 74 owners write their action-bar content, so the right-edge slice always reflects the REPL state of the world. Bare-deck and cartridge action bars conventionally write Row 74 from the left edge rightward; the right-edge slice is firmware-runtime territory under spec hygiene rule 5. (Exact column extents recost at 128×75 per ADR-0027.)
This is the difference between GWP-315 and the GWP-120 scaffold: the toast itself is still transient (open/close on demand), but the affordance is permanently advertised, so the operator never has to discover that the REPL exists by accident.
4. History persistence
Section titled “4. History persistence”ADR-0002 §1: “History: last 32 expressions persisted to deck state. Recalled via CDR on a history cell.”
Two layers carry this:
- In-memory working ring (
nemacs_repl_history_*in../../../runtime/src/nemacs.h) — 32 slots × 256 bytes inside the editor Fe arena, used for live history walking withCDRwhile the REPL is open. Volatile; reset when the editor subsystem reinitializes. - Per-deck persistent ring (
repl_history_*in../../../runtime/src/repl_history.h) — 16 slots × 80 bytes living inside the Universal Deck State on the device’s microSD (4 KB region per the Canonical Hardware Specification). Survives power cycles, cart swaps, and firmware updates. Hydrated into the in-memory ring at boot.
The persistent ring is intentionally narrower (16 vs 32 slots, 80 vs 256 bytes per entry) to fit alongside the snippet library (1540 B) and the cipher persist blob (256 B) inside the 4 KB UDS budget. The per-entry cap (79 chars) is wider than REPL_TOAST_INPUT_MAX (72 chars), so every line the toast can submit fits with headroom.
Ring semantics:
- Empty submissions silently drop.
- Consecutive duplicates of the most-recent entry coalesce (no advance) — shell-style dedup.
- Strings longer than 79 chars are truncated; the truncated form is what persists.
- Saturates at 16 slots; the oldest entry is dropped on overflow.
Deck-state version: DECK_STRUCT_VERSION bumped from 3 → 4 by GWP-315 to reflect the appended repl_history[1284] blob. Older v3 files are rejected; the deck reinitializes fresh and the REPL history starts empty. (Consistent with the existing strict-mismatch policy in deck.c::deck_state_load.)
4B. Snippets — the named layer
Section titled “4B. Snippets — the named layer”ADR-0002 §Decisions: “Per-deck snippet library is in for v1.” History is the chronological layer; snippets are the named, reusable one. Both persist to the microSD as part of the Universal Deck State, and both are populated from the prompt.
Two operator commands carry the commit path, registered in the TERM palette alongside the Core builtins:
| Command | Effect |
|---|---|
(save-snippet NAME) | Keeps the previous submitted line under NAME. Returns a report string naming what was saved, or the reason it was refused. |
(load-snippet NAME) | Puts NAME’s body back in the composer, ready to edit or run, and returns it. |
Save takes the previous line rather than an explicit body because that is the order the operator works in: write an expression, run it, see it work, then name it. A line that invokes either command is not itself kept, so two saves in a row name the same expression.
The library beneath them is snippet.c, reached through the snippet_screen.c seam the screen router binds into the System context. Six primitives are available at the prompt directly:
| Primitive | Result |
|---|---|
(snippet/save! NAME SOURCE) | t, or nil on refusal. Overwrites when NAME exists. |
(snippet/load NAME) | the source string, or nil. |
(snippet/list) | list of names, or nil when empty. |
(snippet/delete! NAME) | t, or nil when NAME is not there. |
(snippet/count) | live slot count, 0..16. |
(snippet/name-valid? NAME) | t when NAME is a legal snippet name. |
Caps and refusals (enforced by snippet.h): 16 slots, a 16-character name starting with a letter over [A-Za-z0-9-_], 512 bytes per body, and a 1024-byte source pool shared across every snippet. Each refusal is a refusal rather than a truncation: an over-long name is rejected instead of being cut down to a name the operator never typed. The pool can run out before the slots do, which is the documented trade-off.
Write authority. Snippets are player-authored, and snippet/save! is a raw primitive with no gate. That is the opposite of the keystore, where ADR-0040 §3 makes a key land only as the earned consequence of a sanctioned outcome (keystore/commit!). The asymmetry is deliberate: hand-authoring a key would delete the reason capture is rewarding, while gating a snippet save would delete the feature. See ../../plans/2026-07-30-snippets-and-the-secret-store.md §4.
Both tiers share one library. The cart-tier NoshAPI v1 names (snippet-save, snippet-load, snippet-list, snippet-delete, ADR-0005) resolve to the same store, so a snippet saved from cartridge Lisp is a snippet the REPL lists. The System-tier names are namespaced to match the convention the surrounding seams keep (keystore/…, repl/…, deck/…, render/…); the cart names are frozen API and stay as they are.
4A. Mission Runner mode
Section titled “4A. Mission Runner mode”When the operator accepts a contract on the mission board (../runtime/mission-control.md §5), Mission Control hands the mission struct to the same REPL/nEmacs subsystem documented above, with additional bindings layered into the active environment. This is the Mission Runner (ADR-0029).
The Mission Runner is not a separate buffer or surface. It is the standard REPL with three extra bindings and one extra builtin available while a contract is active:
| Binding | Type | Provided by |
|---|---|---|
current-mission | mission struct (record, see ADR-0029 §3) | Mission Control on [EVAL]-accept |
mission-params | property list extracted from the struct (:threat, :seed, :objectives, …) | Mission Control on accept |
phase-chain | list of phase requirement records | Hydrated from UDS phase_chain on accept |
(load-capability ...) | builtin | NoshAPI Mission-context tier (ADR-0005) |
The standalone REPL (TERM key, no contract active) sees these bindings as unbound — calling current-mission outside Mission Runner mode raises :no-active-mission. Snippet library, history ring, keymap, animation envelope, and FFI access policy are all identical between the two modes; the only difference is which symbols are bound.
(load-capability :module :seed seed) invokes the named cartridge’s gameplay loop (Hot-Swap-prompting if the cart isn’t currently inserted) and returns a result struct ({:outcome :trace :extracted :turns :bonuses}). The Mission Runner program branches on the result to drive phase progression. See ../runtime/mission-control.md §5.3 and ADR-0005.
The Mission Runner program may be entered manually at the REPL (“REPL the mission”) or driven by an nEmacs-authored script (“script the mission”). Both paths are first-class. Scripts raise the ceiling for experienced operators; nothing in the spec gates the critical path on script authoring.
When the program calls (complete-mission current-mission) or (abandon-mission), Mission Control unbinds the Mission Runner symbols, finalizes the contract economy, and returns to the board.
5. Boundary with adjacent surfaces
Section titled “5. Boundary with adjacent surfaces”| Surface | Role | Relationship to the REPL |
|---|---|---|
Bare deck terminal (../runtime/bare-deck-content-brief.md) | Teaches Lisp grammar through small bounded exercises (1–3 minutes each). | Independent surface; tab content never invokes the REPL. The bare deck’s SYS tab “Restart Tutorial” row enters the REPL tutorial (ADR-0002 §1, GWP-224) — explicit operator action only. |
nEmacs structural editor (nemacs.md) | Authors snippets, scripts, and scripted-mission solutions. | Sibling buffer in the unified ADR-0016 subsystem. Shares keymap machinery, cursor model, FFI bindings. The REPL is the immediate-eval buffer; nEmacs is the deferred-eval editor. |
CIPHER-LINE (../runtime/cipher-voice.md) | Emits voice fragments on the auxiliary OLED. | The REPL toast does NOT paint CIPHER glyphs. CIPHER stays on the auxiliary OLED per ADR-0015. |
Per-deck snippet library (../../../runtime/src/snippet.h) | Persistent named expressions saved by the operator. | Snippets are the named, reusable layer; REPL history is the chronological, deduplicated layer. Both live on the deck’s microSD; both are populated through the REPL’s commit path, which §4B specifies. |
6. Implementation map
Section titled “6. Implementation map”| Concern | File |
|---|---|
| REPL screen — drop-down band render + slide machine + composer/eval (Fe) | ../../../runtime/system-image/lib/repl/repl.lsp (repl/overlay-open! / overlay-close! / overlay-closed? / overlay-reset!; *repl-overlay-offset* / *repl-overlay-phase*) |
| Overlay router — snapshot / restore / tick-pause / finalize (C) | ../../../runtime/src/screen_router.c (screen_router_overlay_toggle / overlay_render / overlay_active / overlay_blocks_tick) |
| Byte-exact surface snapshot seam (C) | ../../../runtime/src/render.c (render_snapshot / render_restore / render_snapshot_valid) |
| Scrim + shadow depth cues | ../../../runtime/src/composite.c; exposed System-tier as render/scrim / render/shadow in ../../../runtime/src/sys_render.c |
| History ring + eval seam (C) | ../../../runtime/src/repl_screen.c + ../../../runtime/src/repl_history.c |
| Per-deck history ring | ../../../runtime/src/repl_history.c |
| Snippet library seam (C) | ../../../runtime/src/snippet_screen.c (snippet_screen_bind / _reset / _lib; bound into the System context by screen_router_init) |
| Per-deck snippet library | ../../../runtime/src/snippet.c |
| Snippet commands (Fe) | ../../../runtime/system-image/lib/repl/repl.lsp (save-snippet / load-snippet; *repl-last* / *repl-staged* / *repl-library-cmd*) |
| Deck-state field | DeckState.repl_history[1284] and DeckState.snippet_library[1540] in ../../../runtime/src/types.h |
| Wire-up (TERM dispatch + overlay drive) | the emulator host loop (hosts/emulator/src/main.c — screen_router_overlay_toggle on TERM; screen_router_overlay_render + tick-pause in render_frame / the frame body) |
| Lifecycle / commit-persist plumbing (legacy) | ../../../runtime/src/repl_toast.c (the ReplToastPhase state machine + commit/persist wiring; the render path moved to the Fe screen) |
| Tests | tests/test_repl_screen.c, tests/test_screen_router.c (the overlay cases), tests/test_render.c (snapshot/restore), tests/test_sys_render.c (render/scrim / shadow / snapshot / restore), tests/test_repl_history.c, tests/lisp/test-snippet.lsp (the §4B surface, run by tests/test_program_lisp.c), tests/test_snippet.c (the storage model) |
| Demo | runtime/tools/kn86rec/demos/repl-quake.gif (kn86rec --screen repl --overlay); demos/repl-snippets.gif for the §4B save/load path (runtime/tools/kn86rec/demos/repl-snippets.rec) |
The unified ADR-0016 subsystem (nEmacs + REPL as one) covers the structural editing primitives, keymap-as-Lisp-alist, context-polymorphic dispatch, and Nokia multi-tap. Read ../api-reference/editor-tools/repl.md for the loop semantics, the read-eval-print algorithm, and the Tier 3 FFI access policy.
7. Future work
Section titled “7. Future work”- Snippet management screen. §4B gives the library a commit path. Naming and recall both go through typed commands, and no surface lists what is saved.
../api-reference/editor-tools/nemacs.md§“SYS → Load” specifies one. The design reference is../../influences/inspiration/clipse.md: list plus top-filter plus preview, with pin as an orthogonal item state. That reference is assigned to Kommander today and covers a clipboard manager, which is a snippet manager with a different producer. Separate story. - The library-backed REPL cannot reach the commands. Under
KN86_NEMACS_LIB(default off) a submit evaluates in the engine’s own warm session against the ADR-0005 Tier-3 read-only binding-set, where neithersave-snippetnorsnippet/save!is bound. Closing that means deciding whether a player-authored snippet write belongs in a tier documented as read-only, and it has to close before the flag flips on by default. - REPL ↔ nEmacs handoff key binding — ADR-0016 §“Known Unknowns” #6 leaves the snippet hand-off binding TBD.
- Tutorial completion marker semantics — ADR-0002 sign-off left “whether tutorial-mode completion grants a capability bit” as an implementation-time decision; GWP-224 carries the persistence flag (
tutorial_completed) but the capability hookup is open. - Geometry settled at 128×75 (ADR-0053). The drop-down band is a fixed 30-cell (~40%) console on the native-renderer grid; the prior ADR-0027 “recost the toast row span” item is closed by the equake overlay model.
- Persistent ring widening — if the 4 KB UDS budget loosens (post-launch firmware update with a re-laid-out flash region), the persistent ring should widen to match the in-memory 32-slot working ring.
Inspirations
Section titled “Inspirations”Assigned by Josh in the kn86-inspo workbench. Full map: cart-inspiration-map.md.
| Source | Category |
|---|---|
| Build Your Own Lisp | lisp |
| Fe — Embedded Lisp by rxi | lisp |
| McCarthy — Recursive Functions of Symbolic Expressions (1960) | lisp |
| cl-termbox2 | lisp |
| mal — Make a Lisp | lisp |