Skip to content

KN-86 Deckline — Cartridge Grammar Specification

The cartridge grammar is a C-based domain-specific framework — not a language you interpret, but a set of structures, macros, and conventions that make C read like the Deckline’s interaction model while compiling to native ARM code on the Pi Zero 2 W (and native x86/ARM64 code in the desktop emulator). No interpreter overhead. No garbage collector. Full speed.

The framework enforces the interaction paradigm at the authoring level: every cell type declares what happens when each key is pressed in its context. The cell definition IS the game design document.

Capability model context: In the capability model, cartridges do not own their main loop. The nOSh runtime (nOSh) is the orchestrator — it runs the mission board, accepts bids, initiates phases, and calls into cartridge handlers when a phase is active. The cartridge grammar provides the macros for registering capabilities (CAPABILITY_DECLARE), defining mission templates (TEMPLATE_DEFINE), implementing phase handlers (PHASE_HANDLER), and contributing Cipher domain vocabulary (CIPHER_DOMAIN). The cell definitions, event handlers, and standard library functions described below remain unchanged — what changes is the ownership model.


┌─────────────────────────────────────────────────────────┐
│ CARTRIDGE SOURCE (developer-authored) │
│ #include "nosh_cart.h" │
│ Uses: DECK_TITLE, CAPABILITY_DECLARE, CELL_DEFINE, │
│ TEMPLATE_DEFINE, PHASE_HANDLER, CIPHER_DOMAIN, │
│ ON_CAR, ON_EVAL, etc. │
├─────────────────────────────────────────────────────────┤
│ CARTRIDGE RUNTIME (nosh_runtime.c) — NOSH RUNTIME CODE │
│ Input dispatch, cell pool, nav stack, quote/lambda │
│ Phase handler dispatch, template parser │
├─────────────────────────────────────────────────────────┤
│ STANDARD LIBRARY (nosh_stdlib.c) │
│ drill_into(), next_sibling(), trace_advance(), etc. │
├─────────────────────────────────────────────────────────┤
│ nOSh NOSH RUNTIME (ORCHESTRATOR) │
│ Mission board, phase chain, economy, Cipher voice │
│ Display, sound, input, save, link APIs │
└─────────────────────────────────────────────────────────┘

All layers are C. The cartridge compiles to native ARM (for the Pi Zero 2 W) or x86/ARM64 (for the desktop emulator) depending on the build target. Same source, both targets: make device or make emulator. The runtime (nosh_runtime.c) is nOSh runtime code — it bridges the nOSh runtime’s mission board and the cartridge’s event handlers.

Firmware vtable: Cartridge code calls nOSh runtime functions through a NoshAPI function table passed at init time. The nosh_cart.h macros hide this — nosh_text_puts(...) expands to _nosh_api->text_puts(...). This decouples nOSh runtime versions from cartridge binaries on both targets. On the desktop emulator (where cartridge code loads via dlopen), the vtable is populated with direct function pointers. See KN-86-Capability-Model-Spec.md § Cartridge Code Execution Model for the full vtable definition.


Layer 1: nosh_cart.h — The Authoring Surface

Section titled “Layer 1: nosh_cart.h — The Authoring Surface”

This is the header a cartridge developer includes. It provides macros that create a declarative, readable authoring grammar while expanding to efficient C structures and function pointers.

#include "nosh_cart.h"
DECK_TITLE("ICE_BREAKER", "ICE Breaker v1.3", "NETWORK INTRUSION")

Expands to: a CartridgeHeader struct populated with the program name, display name, and module class, plus a registration call that nOSh invokes on cartridge load.

CELL_TYPE(contract) {
CELL_FIELDS {
char name[24];
uint8_t threat;
uint16_t payout;
uint32_t network_seed;
};
ON_CAR { drill_into_network(self, self->network_seed); }
ON_CDR { next_sibling(); }
ON_EVAL { accept_contract(self); }
ON_INFO { show_contract_details(self); }
ON_QUOTE { quote_cell(self); }
ON_BACK { navigate_back(); }
ON_DISPLAY {
nosh_text_puts(0, 0, "CONTRACT: ");
nosh_text_puts(10, 0, self->name);
nosh_text_puts(0, 1, "THREAT: ");
draw_threat_bar(self->threat, 8, 1);
nosh_text_puts(0, 2, "PAYOUT: ");
nosh_text_printf(8, 2, "%d CR", self->payout);
}
}
// CELL_TYPE(name) expands to:
typedef struct cell_##name cell_##name;
struct cell_##name {
CellBase base; // Common cell header (type, flags, id, nav links)
// ... CELL_FIELDS content inserted here ...
};
static const CellHandlers cell_##name##_handlers;
// ON_CAR { ... } expands to:
// A static function definition:
static void cell_##name##_on_car(cell_##name *self)
// ... with the braced body ...
// And a slot assignment in the handlers struct:
// .on_car = cell_##name##_on_car
// ON_DISPLAY { ... } expands to:
// A render function called by the display manager when this cell has focus
static void cell_##name##_render(cell_##name *self)

Every cell type can define handlers for any combination of the 14 function keys plus the numpad:

// Lisp Primitives (amber legends)
ON_CAR { /* Enter/examine/drill in */ }
ON_CDR { /* Next/traverse/advance */ }
ON_CONS { /* Attach/combine/construct */ }
ON_NIL { /* Discard/clear/cancel */ }
ON_ATOM { /* Test if current cell is a leaf (no children) */ }
ON_EQ { /* Compare two quoted elements, report match/delta */ }
// Action Verbs (white legends)
ON_EVAL { /* Execute/confirm/commit */ }
ON_QUOTE { /* Bookmark/defer/hold */ }
ON_LAMBDA { /* Custom lambda behavior (rare — firmware handles recording) */ }
ON_APPLY { /* Deploy/use tool against target */ }
// System (gray legends)
ON_BACK { /* Return to parent */ }
ON_INFO { /* Inspect/scan/details */ }
ON_LINK { /* Initiate link action */ }
ON_SYS { /* System menu. Tap = menu. Hold 2s = abort/cancel/hard disconnect */ }
// Numpad
ON_NUMPAD(digit) { /* Handle numeric input */ }
// Rendering
ON_DISPLAY { /* Draw this cell's screen representation */ }
// Lifecycle
ON_ENTER { /* Called when cursor enters this cell */ }
ON_EXIT { /* Called when cursor leaves this cell */ }

Unimplemented handlers default to sensible behavior:

  • ON_CAR with no handler: attempt drill_into(self) — if self has children, navigate to first child; if not, beep (error sound)
  • ON_CDR with no handler: next_sibling() — move to next element in current list
  • ON_BACK with no handler: navigate_back() — pop navigation stack
  • ON_ATOM with no handler: returns is_leaf(self) — true if cell has no children, false otherwise
  • ON_EQ with no handler: compares current cell with first quoted element — reports match or delta
  • ON_QUOTE with no handler: quote_cell(self) — standard firmware quote behavior
  • ON_SYS with no handler: SYS tap = system menu, SYS hold 2s = abort/cancel (replaces old ESC)
  • All others with no handler: nosh_sfx_error() — key not active in this context

This means a minimal cell type with NO handlers still supports basic list navigation. CAR/CDR/BACK just work. The developer only writes handlers for keys that do something domain-specific.

When ATOM or EQ is pressed during a LAMBDA recording, it inserts a conditional branch point. If the test fails during playback, the LAMBDA skips to the next step or aborts. This allows recorded sequences to adapt to cell state without manual intervention — for example, a LAMBDA can test is_leaf(self) and branch accordingly, or compare two cells and proceed conditionally.

// Static cell (defined at compile time, typical for templates)
CELL_INSTANCE(contract, meridian_extract) {
.name = "MERIDIAN EXTRACT",
.threat = 4,
.payout = 800,
.network_seed = 0xA7F3,
};
// Dynamic cell (created at runtime from procedural generation)
cell_contract *c = spawn_cell(contract);
strncpy(c->name, generated_name, 24);
c->threat = lfsr_range(2, 6);
c->payout = c->threat * lfsr_range(100, 200);
c->network_seed = lfsr_next();
// Build a contract list from procedural generation
CellList *board = list_create();
for (int i = 0; i < contract_count; i++) {
cell_contract *c = spawn_cell(contract);
generate_contract(c, deck_state->reputation);
list_push(board, (CellBase *)c);
}
// Set as the current navigable list
set_root(board);
// Operator now sees the first contract; CDR scrolls through them
MISSION_DEFINE("MERIDIAN EXTRACT") {
.class = MISSION_NETWORK | MISSION_CRYPTO,
.threat_range = {2, 5},
.phases = 2,
PHASE(1, "NETWORK INTRUSION") {
.requires = MODULE_ICE_BREAKER,
.generator = generate_intrusion_network,
.objective = OBJECTIVE_EXTRACT,
}
PHASE(2, "DECRYPT PAYLOAD") {
.requires = MODULE_SIGNAL,
.generator = generate_cipher_puzzle,
.objective = OBJECTIVE_DECRYPT,
}
.payout_formula = mission_payout_standard,
}

Layer 2: nosh_runtime.c — The Dispatcher

Section titled “Layer 2: nosh_runtime.c — The Dispatcher”

Framework code written once, linked into every cartridge. The cartridge developer never touches this.

void nosh_runtime_tick(void) {
// 1. Poll input queue
KeyEvent ev;
while (input_dequeue(&ev)) {
// 2. Resolve current cell
CellBase *current = nav_stack_peek();
if (!current) continue;
// 3. Dispatch to handler
const CellHandlers *handlers = current->handlers;
switch (ev.key) {
case KEY_CAR: CALL_HANDLER(handlers->on_car, current); break;
case KEY_CDR: CALL_HANDLER(handlers->on_cdr, current); break;
case KEY_CONS: CALL_HANDLER(handlers->on_cons, current); break;
case KEY_NIL: CALL_HANDLER(handlers->on_nil, current); break;
case KEY_ATOM: CALL_HANDLER(handlers->on_atom, current); break;
case KEY_EQ: CALL_HANDLER(handlers->on_eq, current); break;
case KEY_EVAL: CALL_HANDLER(handlers->on_eval, current); break;
case KEY_QUOTE: CALL_HANDLER(handlers->on_quote, current); break;
case KEY_APPLY: CALL_HANDLER(handlers->on_apply, current); break;
case KEY_BACK: CALL_HANDLER(handlers->on_back, current); break;
case KEY_INFO: CALL_HANDLER(handlers->on_info, current); break;
case KEY_LINK: CALL_HANDLER(handlers->on_link, current); break;
case KEY_SYS: CALL_HANDLER(handlers->on_sys, current); break;
default:
if (ev.key >= KEY_NUM0 && ev.key <= KEY_NUM9) {
CALL_NUMPAD(handlers->on_numpad, current, ev.key - KEY_NUM0);
}
break;
}
}
// 4. Render current cell
CellBase *current = nav_stack_peek();
if (current && current->handlers->on_display) {
current->handlers->on_display(current);
}
// 5. Render status bar (runtime-level, always present)
render_status_bar();
}
#define NAV_STACK_DEPTH 32
static CellBase *nav_stack[NAV_STACK_DEPTH];
static uint8_t nav_depth = 0;
void drill_into(CellBase *target) {
if (nav_depth >= NAV_STACK_DEPTH) { nosh_sfx_error(); return; }
CellBase *current = nav_stack_peek();
if (current && current->handlers->on_exit) {
current->handlers->on_exit(current);
}
nav_stack[nav_depth++] = target;
if (target->handlers->on_enter) {
target->handlers->on_enter(target);
}
}
void navigate_back(void) {
if (nav_depth <= 1) { nosh_sfx_error(); return; } // Can't back past root
CellBase *leaving = nav_stack[--nav_depth];
if (leaving->handlers->on_exit) {
leaving->handlers->on_exit(leaving);
}
CellBase *returning = nav_stack_peek();
if (returning->handlers->on_enter) {
returning->handlers->on_enter(returning);
}
}
#define CELL_POOL_SIZE 4096 // Max cells in memory simultaneously
static uint8_t cell_pool_memory[CELL_POOL_SIZE * CELL_MAX_SIZE];
static CellBase *free_list = NULL;
// Allocate a cell of a specific type
void *spawn_cell_raw(uint16_t type_id, size_t size) {
// Allocate from free list or pool
CellBase *cell = free_list_pop(size);
if (!cell) { /* pool exhausted — handle gracefully */ return NULL; }
memset(cell, 0, size);
cell->type_id = type_id;
cell->handlers = get_handlers_for_type(type_id);
cell->id = next_cell_id++;
return cell;
}
// The spawn_cell(typename) macro wraps this:
#define spawn_cell(type) \
((cell_##type *)spawn_cell_raw(CELL_TYPE_ID_##type, sizeof(cell_##type)))

Layer 3: nosh_stdlib.c — Standard Library

Section titled “Layer 3: nosh_stdlib.c — Standard Library”

Common operations that cartridge code calls inside handlers. These are the verbs of the framework.

void drill_into(CellBase *target); // Push target onto nav stack, trigger ON_ENTER
void navigate_back(void); // Pop nav stack, trigger ON_EXIT/ON_ENTER
void next_sibling(void); // Move cursor to cdr of current cell
void prev_sibling(void); // Move cursor backward in list (if doubly linked)
void jump_to(CellBase *target); // Direct navigation (for QUOTE jumps)
void set_root(CellList *list); // Set the root navigable list
CellBase *current_cell(void); // Return the cell the cursor is on
void *spawn_cell(type); // Allocate a new cell of given type
void destroy_cell(CellBase *cell); // Return cell to free pool
void link_cells(CellBase *a, CellBase *b); // Create a relation between two cells
void unlink_cell(CellBase *cell); // Remove from its list
CellList *list_create(void); // Create empty list
void list_push(CellList *list, CellBase *cell); // Add to end
void list_insert(CellList *list, int index, CellBase *cell);
CellBase *list_get(CellList *list, int index);
int list_length(CellList *list);
void list_sort(CellList *list, CellCompare cmp); // Sort by comparison function
void lfsr_seed(uint32_t seed); // Seed the LFSR
uint32_t lfsr_next(void); // Next pseudo-random value
uint32_t lfsr_range(uint32_t min, uint32_t max); // Random in range
void lfsr_shuffle(void **array, int count); // Fisher-Yates shuffle
void draw_threat_bar(uint8_t level, uint8_t x, uint8_t y); // ████░░ style bar
void draw_progress_bar(uint8_t pct, uint8_t x, uint8_t y, uint8_t width);
void draw_bordered_box(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const char *title);
void draw_list_view(CellList *list, uint8_t start_row, uint8_t visible_rows, int selected);
void draw_split_panes(CellList *left, CellList *right, int sel_l, int sel_r);
void sfx_select(void); // Short click for selection
void sfx_confirm(void); // Confirmation tone
void sfx_error(void); // Error buzz
void sfx_alert(void); // Warning ping
void sfx_ambient_drone(uint16_t freq, uint8_t vol); // Set background drone
void sfx_sting(uint16_t freq, uint8_t duration_ms); // Sharp one-shot
DeckState *deck_state(void); // Access universal deck state
void credit_add(int32_t amount); // Modify credit balance
void credit_deduct(int32_t amount);
void rep_modify(int16_t delta); // Modify reputation
bool has_capability(uint8_t module_bit); // Check cartridge history (up to bit 31)
uint8_t knowledge_index(void); // The Vault bonus
uint8_t logic_index(void); // Takezo bonus

Note on coordinate types: All bitmap graphics API functions (nosh_gfx_pixel, nosh_gfx_line, nosh_gfx_rect, nosh_gfx_circle, nosh_gfx_blit) use uint16_t for both x-coordinates and y-coordinates to address the full 1024×600 framebuffer. Widths and heights are also uint16_t.

bool cart_save(const void *data, uint32_t size); // Save cartridge-specific data
bool cart_load(void *data, uint32_t size); // Load cartridge-specific data
bool cart_save_exists(void);

A fully functional cartridge that creates a navigable list of items:

#include "nosh_cart.h"
DECK_TITLE("DEMO", "Demo v1.0", "DIAGNOSTIC")
// --- Cell Types ---
CELL_TYPE(item) {
CELL_FIELDS {
char label[24];
int value;
};
ON_CAR {
nosh_text_clear();
nosh_text_puts(0, 0, "EXAMINING:");
nosh_text_puts(0, 1, self->label);
nosh_text_printf(0, 3, "VALUE: %d", self->value);
sfx_select();
}
ON_EVAL {
credit_add(self->value);
nosh_text_printf(0, 5, "COLLECTED %d CR", self->value);
sfx_confirm();
destroy_cell((CellBase *)self);
navigate_back();
}
ON_INFO {
nosh_text_printf(0, 7, "DETAIL: %s (%d)", self->label, self->value);
}
ON_DISPLAY {
// Default list item rendering
nosh_text_puts(2, 0, self->label);
nosh_text_printf(20, 0, "%4d CR", self->value);
}
}
// --- Initialization ---
CART_INIT {
// Called once when cartridge loads
CellList *items = list_create();
lfsr_seed(deck_state()->credit_balance ^ 0xDEAD);
for (int i = 0; i < 8; i++) {
cell_item *it = spawn_cell(item);
snprintf(it->label, 24, "DATA PACKET %02X", lfsr_next() & 0xFF);
it->value = lfsr_range(50, 500);
list_push(items, (CellBase *)it);
}
set_root(items);
nosh_text_puts(0, 14, "CDR: next | CAR: examine | EVAL: collect");
}

That’s it. ~50 lines of code for a playable cartridge. The framework handles input dispatch, list navigation, BACK/CDR default behavior, rendering, QUOTE/LAMBDA integration, and save state. The developer defined things and what happens to them.


my-cartridge/
├── Makefile # or CMakeLists.txt
├── cart.c # Cartridge source (includes nosh_cart.h)
├── generators.c # Procedural generation functions
├── generators.h
└── assets/
├── label.txt # Glyph-to-verb reference text
└── data/ # Lookup tables, text banks, etc.
# Build for desktop emulator
make emulator
# Produces: build/cart_emu (executable, links against nosh-emulator)
# Run with: ./build/cart_emu
# Build for device (Pi Zero 2 W)
make device
# Produces: build/cart.kn86 (cartridge image)
# Copy to the device's SD card under ~/.nosh/cartridges/
# Build both
make all

The same cartridge source compiles for both targets because:

  1. nosh_cart.h, nosh_runtime.c, and nosh_stdlib.c contain no platform-specific code
  2. The nOSh platform API (nosh_platform.h) is implemented twice:
    • platform_linux.c — Device (Pi Zero 2 W, SDL2 fullscreen, SDL_audio, USB HID input)
    • platform_sdl2.c — Desktop emulator (SDL2 window, SDL_audio, keyboard input)
  3. The Makefile selects the right implementation based on the target:
ifeq ($(TARGET),device)
CC = aarch64-linux-gnu-gcc
CFLAGS += -DTARGET_PI_ZERO $(shell sdl2-config --cflags)
LDFLAGS += $(shell sdl2-config --libs)
NOSH_IMPL = nosh_linux.c
else
CC = cc
CFLAGS += -DTARGET_EMULATOR $(shell sdl2-config --cflags)
LDFLAGS += $(shell sdl2-config --libs)
NOSH_IMPL = nosh_sdl.c
endif

  • Every cartridge follows the same structure: define cell types, declare key behaviors, wire into lists. A developer can’t accidentally build a cartridge that ignores the interaction model.
  • Cell definitions are readable as design documents. You can scan the ON_CAR / ON_EVAL / ON_INFO slots and understand the interaction without tracing control flow.
  • Default behaviors handle the 80% case. A cell with no explicit handlers still supports CAR/CDR/BACK navigation. The developer only writes the domain-specific parts.
  • The framework IS the interaction model. If you author a cell, you’re thinking in the Deckline’s grammar. The macros make it difficult to NOT think this way.
  • Native compiled speed. No interpreter. No GC pauses. Procedural generation runs at full 150MHz.
  • Dual-target compilation. Same source builds for device and emulator. Massive iteration speed advantage.
  • Zero runtime overhead. The macros expand to plain C structs and function pointers. The dispatcher is a switch statement. This compiles to the fastest possible code.
  • Debuggable. GDB, printf debugging, memory inspection — standard C tooling. No Lisp stack traces to decode.

Community accessibility narrows. A Lisp SDK means game designers write configuration-like code. The cell grammar means they need a C toolchain and basic pointer fluency. For the first four titles — written by the device builder — this doesn’t matter. For a hypothetical future community — it narrows the pool. But anyone building cartridges for a hand-soldered cyberdeck already knows C.

uLisp remains a valid Phase 2 goal: a Lisp interpreter that sits on top of the cell grammar, translating Lisp expressions into spawn_cell() and handler registration calls. The grammar’s clean separation makes this layering possible without modifying the runtime.


The framework should have a name. Proposals:

  • DeckC — C for the Deck. Clear, functional.
  • CellScript — Describes what it does (define cells and their scripts).
  • nosh_cart — The practical name (it’s the header you include).

Recommendation: Call the header nosh_cart.h, call the framework “the cartridge grammar” in documentation, and don’t overthink branding for an internal tool.


The cartridge grammar is the Deckline’s interaction model expressed as C. It doesn’t interpret lists — it compiles them.