/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. Licensed under the StarForth License, Version 1.0 */ /** * repl.c - Emergency FORTH REPL for LithosAnanke kernel * * Direct adaptation of src/repl.c for the freestanding kernel context. * Replaces libc stdio (fgets/printf/fflush) with HAL serial I/O: * - Input: console_getc() non-blocking poll with local echo and backspace * - Output: console_puts() / console_putc() * * Idle spin: polls console_getc() and services the adaptive heartbeat. * The timer ISR's top half (heartbeat_tick()) latches one * sample per interrupt; the idle spin drains it every * iteration via heartbeat_service() (item 0.8, FABRIC.md §26) * and calls sk_repl_idle() once per SK_IDLE_BEAT_INTERVAL ticks * for coarser subsystem dispatch. On QEMU TCG the ISR must fire * for ticks to advance — check "Heartbeat: N ticks" in the * serial log to confirm. * * Runs with interrupts enabled so the APIC heartbeat fires normally. * Designed as the last thing kernel_main does before the idle loop. */ #include "starkernel/repl.h" #include "console.h" #include "vm.h" #include "version.h" #include "starkernel/timer.h" #include "starkernel/arch.h" #include "starkernel/xhci_driver.h" #include "starkernel/blkio_usb.h" #include "block_subsystem.h" #include "word_source/include/keyboard_words.h" #include "word_source/include/block_words.h" #include #include /* FABRIC.md 4.4: "ok>" (including its trailing space) renders in bright * cyan, 0x55FFFF -- reuses FB_ANSI_PALETTE[14]. Sent as a real SGR escape * so it colors both the framebuffer (parsed by vt100.c's apply_sgr()) and * any ANSI-aware serial terminal, per 4.4c's "identical on both" goal. */ #define SK_PROMPT_TEXT "\x1b[38;2;85;255;255mok> \x1b[39m" const char lithos_version[64] = LITHOS_VERSION_STR; /*=========================================================================== * USE-word dispatch: which VM receives REPL input. * * NULL means "use the REPL's own vm parameter" (default — Mama). * Set via sk_repl_set_active_vm(); read by sk_repl_run() each iteration. *===========================================================================*/ static VM *g_repl_active_vm = (void *)0; void sk_repl_set_active_vm(VM *vm) { g_repl_active_vm = vm; } VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; } /*=========================================================================== * Idle heartbeat service * * Called from sk_readline when heartbeat_ticks() has advanced by at least * SK_IDLE_BEAT_INTERVAL since the last service call. Extend this function * as higher-level subsystems (msg_fabric, capsule scheduler) come online. * * TODO: cadence policy and subsystem dispatch belong in Compudynamics once * that layer governs cooperative VM execution. *===========================================================================*/ #define SK_IDLE_BEAT_INTERVAL 100u /* ticks between idle service calls (1 s at 100 Hz) */ static uint64_t g_last_beat_tick; /* zero-initialized (BSS) */ static void sk_repl_idle(VM *active_vm) { /* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the * "interrupt-driven, coarse cadence, cheap early-exit" trigger Section * U item 6 asked for -- xhci_poll_events() is a no-op read (loop * condition false immediately) whenever nothing is pending, and this * hook already runs at a deliberately coarser cadence than the raw * per-tick ISR (SK_IDLE_BEAT_INTERVAL, ~1s at 100Hz), matching "quick * check... done... ignore what we can... done." A no-op call if no * controller was found/brought up (xhci_bringup() never latched a * device). */ xhci_poll_events(); /* Milestone 2h: a Mass Storage/BOT device finished SET_CONFIGURATION * during the xhci_poll_events() call just above -- run the * synchronous capacity query + block-subsystem attach here, strictly * after that call has already returned (see bot_msc_attach_pending's * own doc comment in xhci_driver.h for why: xhci_bot_wait_for_idle()'s * busy-wait -- which blkio_usb_open_msc() uses internally -- must * never run from inside xhci_poll_events()'s own call frame). */ xhci_dev_t *xdev = xhci_get_dev(); static blkio_dev_t usb_blk_dev; /* single-device scope, matching the xHCI * driver's own; referenced by both the * attach and detach handling below. */ if (xdev && xdev->bot_msc_attach_pending) { xdev->bot_msc_attach_pending = 0; uint32_t slot_id = xdev->bot_msc_attach_slot_id; int rc = blkio_usb_open_msc(&usb_blk_dev, xdev, slot_id); if (rc == 0 && blk_subsys_attach_device(&usb_blk_dev) == BLK_OK) { xdev->bot_msc_attached = 1; } else { console_println("xhci: USB MSC block-subsystem attach failed"); } } /* Milestone 2h hot-detach: the device disconnected (PORTSC, inside the * xhci_poll_events() call above) after having actually attached. * blk_subsys_detach_device() is local block_subsystem.c bookkeeping -- * no device round-trip, so it wouldn't strictly need to run outside * xhci_poll_events()'s own call frame -- but handling it here anyway * matches the attach path's shape and keeps xhci.c decoupled from * block_subsystem.c (see bot_msc_detach_pending's own doc comment). */ if (xdev && xdev->bot_msc_detach_pending) { xdev->bot_msc_detach_pending = 0; blk_subsys_detach_device(&usb_blk_dev); } /* FABRIC.md/FABRIC-2.md Section V item 6: "a cheap 'anything dirty? * no? done' block-sync check", the same "interrupt-driven, coarse * cadence, cheap early-exit" trigger shape as the xHCI servicing * above -- this was the one piece of that design already fully * specified and waiting for this hook to actually be non-empty. * blk_vm_flush_all() (block_words.c, the same code SAVE-BUFFERS * itself runs) is cheap to call when nothing is dirty -- every * check inside is a small fixed-size scan, no disk I/O happens * unless something genuinely needs writing -- so no separate * "is anything dirty" pre-check is needed here. * * active_vm is passed in by the caller (sk_readline(), itself passed * through from sk_repl_run()/sk_repl_step()'s own already-resolved * VM) rather than read via sk_repl_get_active_vm() here -- that * accessor returns NULL whenever Tripod's USE word hasn't redirected * it, which is the common case, not "no VM is active." An earlier * version of this code called sk_repl_get_active_vm() directly and * silently no-op'd for exactly that reason, confirmed live: a BUFFER * write with no UPDATE, followed by an idle wait and an abrupt kill, * did not survive a reboot until this fix. */ blk_vm_flush_all(active_vm); } /*=========================================================================== * FABRIC.md item 4.4v: keyboard-to-REPL bridge. * * Translates sk_key_event_poll()'s converged Linux-keycode-namespace * stream (keyboard_words.c -- one implementation shared with KEY-EVENT, * live-verified on all three architectures per item 4.3.5f) into the same * byte stream sk_readline() already reads from console_getc(): -1 for * "nothing ready", else a raw ASCII byte with '\n'/0x7F meaning the same * thing they mean for the serial path below. * * Table covers exactly the keys a line editor needs -- letters, digits, * the standard US-QWERTY punctuation row, space, enter, backspace, tab * (for the Ctrl+TAB toggle interception, 4.4y/4.4u step 8) -- not full * keyboard coverage. Keycodes are Linux input-event-codes.h values, * confirmed against this build host's own header, not guessed (§25.0 * rule 4). Index 0 means "no mapping"; arrows/F-keys/etc. fall through * unmapped and are silently dropped, consistent with this REPL's * append/backspace-only editing model (4.4u: no mid-line cursor * movement). *===========================================================================*/ #define SK_KBD_TABLE_SIZE 98u /* highest keycode used below is KEY_RIGHTCTRL=97 */ static const char sk_kbd_unshifted[SK_KBD_TABLE_SIZE] = { [2]='1',[3]='2',[4]='3',[5]='4',[6]='5',[7]='6',[8]='7',[9]='8',[10]='9',[11]='0', [12]='-',[13]='=', [16]='q',[17]='w',[18]='e',[19]='r',[20]='t',[21]='y',[22]='u',[23]='i',[24]='o',[25]='p', [26]='[',[27]=']', [30]='a',[31]='s',[32]='d',[33]='f',[34]='g',[35]='h',[36]='j',[37]='k',[38]='l', [39]=';',[40]='\'',[41]='`',[43]='\\', [44]='z',[45]='x',[46]='c',[47]='v',[48]='b',[49]='n',[50]='m', [51]=',',[52]='.',[53]='/', [57]=' ', }; static const char sk_kbd_shifted[SK_KBD_TABLE_SIZE] = { [2]='!',[3]='@',[4]='#',[5]='$',[6]='%',[7]='^',[8]='&',[9]='*',[10]='(',[11]=')', [12]='_',[13]='+', [16]='Q',[17]='W',[18]='E',[19]='R',[20]='T',[21]='Y',[22]='U',[23]='I',[24]='O',[25]='P', [26]='{',[27]='}', [30]='A',[31]='S',[32]='D',[33]='F',[34]='G',[35]='H',[36]='J',[37]='K',[38]='L', [39]=':',[40]='"',[41]='~',[43]='|', [44]='Z',[45]='X',[46]='C',[47]='V',[48]='B',[49]='N',[50]='M', [51]='<',[52]='>',[53]='?', [57]=' ', }; #define SK_KEY_BACKSPACE 14u #define SK_KEY_TAB 15u #define SK_KEY_ENTER 28u #define SK_KEY_LEFTSHIFT 42u #define SK_KEY_RIGHTSHIFT 54u #define SK_KEY_LEFTALT 56u #define SK_KEY_RIGHTALT 100u static int g_kbd_shift_down; /* zero-initialized (BSS) */ static int g_kbd_alt_down; /* Drains and translates one physically-typed key. Modifier state persists * across calls (a real keyboard's shift/alt state is global, not * per-line). Alt+TAB is intercepted here and drives the graphics/text * toggle directly (console_fb_toggle_graphics(), the same state-machine * transition the ALT+TAB FORTH word calls) -- never reaches the line * buffer as a character either way. */ static int sk_kbd_getc(void) { uint16_t keycode; int pressed; while (sk_key_event_poll(&keycode, &pressed)) { if (keycode == SK_KEY_LEFTSHIFT || keycode == SK_KEY_RIGHTSHIFT) { g_kbd_shift_down = pressed; continue; } if (keycode == SK_KEY_LEFTALT || keycode == SK_KEY_RIGHTALT) { g_kbd_alt_down = pressed; continue; } if (!pressed) continue; /* only act on press/repeat */ if (keycode == SK_KEY_TAB) { if (g_kbd_alt_down) console_fb_toggle_graphics(); continue; /* bare TAB: not mapped, same as arrows/F-keys */ } if (keycode == SK_KEY_ENTER) return '\n'; if (keycode == SK_KEY_BACKSPACE) return 0x7F; if (keycode < SK_KBD_TABLE_SIZE) { char c = g_kbd_shift_down ? sk_kbd_shifted[keycode] : sk_kbd_unshifted[keycode]; if (c) return (unsigned char)c; } /* unmapped keycode -- drop and keep draining */ } return -1; } /*=========================================================================== * sk_readline - line read from serial console with echo * * Non-blocking poll of console_getc(). While no character is ready the idle * spin services the adaptive heartbeat at SK_IDLE_BEAT_INTERVAL tick cadence. * Supports backspace (0x7F and \b) and ignores other control characters. * Returns the number of characters placed in buf (not counting '\0'). *===========================================================================*/ static int sk_readline(char *buf, int size, VM *active_vm) { int n = 0; buf[0] = '\0'; console_fb_draw_cursor(); /* show the cursor at the bare prompt, before any input */ for (;;) { int c = console_getc(); /* non-blocking poll */ if (c < 0) c = sk_kbd_getc(); /* FABRIC.md 4.4v: second source, same buffer */ if (c < 0) { /* Service the heartbeat bottom half every idle iteration, not * gated by SK_IDLE_BEAT_INTERVAL (item 0.8, FABRIC.md §26): * heartbeat_service() drains at most one latched sample per * call, so a coarse gate here would silently lose or merge * samples between ISR-latched ticks. sk_repl_idle() below is * a separate, deliberately coarser cadence for higher-level * subsystem dispatch, unrelated to sample fidelity. */ heartbeat_service(); uint64_t now = heartbeat_ticks(); if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) { g_last_beat_tick = now; sk_repl_idle(active_vm); } /* * Do NOT use hlt here: QEMU single-threaded TCG can't process * its APIC timer callbacks while the guest CPU is halted (the * event loop and the TCG thread share the same OS thread). * Interrupts are delivered at TB boundaries in a tight loop. * On real hardware a wfi/hlt would be appropriate; add it here * under an #ifdef REAL_HARDWARE guard when that path is needed. */ arch_relax(); /* PAUSE — reduce power, maintain tight poll */ continue; } if (c == '\r' || c == '\n') { console_fb_erase_cursor(); /* leaving this cell without drawing a char over it */ console_putc('\n'); break; } /* backspace: DEL (0x7F) or BS (0x08) */ if ((c == 0x7F || c == '\b') && n > 0) { n--; buf[n] = '\0'; /* VT100 erase: move back, overwrite with space, move back again */ console_putc('\b'); console_putc(' '); console_putc('\b'); console_fb_draw_cursor(); continue; } if (c < 0x20) continue; /* ignore other control characters */ if (n >= size - 1) continue; /* buffer full — drop character */ buf[n++] = (char)c; buf[n] = '\0'; console_putc((char)c); /* echo */ console_fb_draw_cursor(); } buf[n] = '\0'; return n; } /*=========================================================================== * sk_repl - Emergency FORTH REPL * * Mirrors vm_repl() from src/repl.c: * - Sets vm->emergency_console = 1 for the duration (this IS the emergency * console; bypasses ACL so zuse authentication is not required to recover) * - Prints "zuse)ok> " when zuse_session=1, else "ok> " * - Reads a line via sk_readline (non-blocking, heartbeat-serviced) * - Calls vm_interpret * - Prints " ok" or " ERROR" * - When EMERGENCY_CONSOLE_ENABLED=1: resets vm->error and loops (recovery) * - When EMERGENCY_CONSOLE_ENABLED=0: halts VM on error (no fallthrough surface) *===========================================================================*/ #if !EMERGENCY_CONSOLE_ENABLED static void sk_fault_handler(VM *vm) { console_println("VM fault — emergency console disabled; halting"); vm->halted = 1; } #endif /*=========================================================================== * sk_repl_step - Execute one REPL turn on a VM and return. * * Prints the VM's prompt, reads one input line, interprets it, prints * ok/ERROR, then returns. Used by the Compudynamics VM-STEP primitive * so Hera can give a single REPL quantum to any child VM without * surrendering control for the full sk_repl_run() loop. * * Returns 1 if the VM is still running, 0 if it halted during this turn. *===========================================================================*/ int sk_repl_step(VM *vm) { char input[INPUT_BUFFER_SIZE]; /* FABRIC.md 4.4w: matches the strip's input width */ if (!vm || vm->halted) return 0; { /* Unified prompt (FABRIC.md 4.4a): console_putc()'s existing per-line * "[VMName] " prefix (console.c, g_active_vm_name) already supplies the * bracket -- print only "ok> " here, don't build a second one. The * emergency_console bypass is a security decision, not a display one -- * it still applies only to Hera's bare prompt, per FABRIC.md 4.4. */ const char *vn = console_get_vm_name(); int is_hera = (!vn || (vn[0]=='H' && vn[1]=='e' && vn[2]=='r' && vn[3]=='a' && vn[4]=='\0')); vm->emergency_console = is_hera ? (vm->zuse_session ? 0 : 1) : 0; console_puts(SK_PROMPT_TEXT); } sk_readline(input, sizeof(input), vm); if (input[0] == '\0') { console_puts(" ok\n"); return vm->halted ? 0 : 1; } vm_interpret(vm, input); /* ABORT stops mid-line but leaves the flag set for the caller to * consume -- this REPL step is that boundary. Clear it here so the * next line isn't silently refused by vm_interpret's own check. */ vm->abort_requested = 0; if (vm->error) { console_puts(" ERROR\n"); vm->error = 0; } else { console_puts(" ok\n"); } return vm->halted ? 0 : 1; } void sk_repl_run(VM *vm) { char input[INPUT_BUFFER_SIZE]; /* FABRIC.md 4.4w: matches the strip's input width */ VM *active; vm->halted = 0; while (!vm->halted) { /* USE may redirect input to a different VM each iteration */ active = g_repl_active_vm ? g_repl_active_vm : vm; /* Unified prompt (FABRIC.md 4.4a): console_putc()'s existing per-line * "[VMName] " prefix (console.c, g_active_vm_name) already supplies the * bracket -- print only "ok> " here, don't build a second one. The * emergency_console bypass is a security decision, not a display one -- * it still applies only to Hera's bare prompt, per FABRIC.md 4.4. */ { const char *vn = console_get_vm_name(); int is_hera = (!vn || (vn[0]=='H' && vn[1]=='e' && vn[2]=='r' && vn[3]=='a' && vn[4]=='\0')); active->emergency_console = is_hera ? (active->zuse_session ? 0 : 1) : 0; console_puts(SK_PROMPT_TEXT); } sk_readline(input, sizeof(input), active); if (input[0] == '\0') { console_puts(" ok\n"); continue; } vm_interpret(active, input); /* ABORT stops mid-line but leaves the flag set for the caller to * consume -- this REPL step is that boundary. Clear it here so the * next line isn't silently refused by vm_interpret's own check. */ active->abort_requested = 0; if (active->error) { console_puts(" ERROR\n"); active->error = 0; } else { console_puts(" ok\n"); } } } void sk_repl(VM *vm) { /* FABRIC.md item 4.4j: boot and POST (both already returned by the time * sk_repl() is called) stay on font_8x16.c/VT100 by design; the * interactive REPL -- this function -- is the boundary where TTF-TEXT * takes over. One-shot: console_fb_enable_ttf() no-ops on any later * call. */ console_fb_enable_ttf(); console_println(lithos_version); console_puts("StarForth Version "); console_println(STARFORTH_VERSION); console_println(""); console_println("StarForth Emergency CLI"); console_println("FORTH-79 interpreter — type BYE or power off to exit"); console_println(""); sk_repl_run(vm); }