Files
LithosAnanke/src/starkernel/repl.c
T
Robert Allan JamesandClaude Sonnet 5 af267a52a6 Artemis Milestone 2h: hot-detach -- 2h complete
blk_subsys_detach_device() (block_subsystem.c) walks the device chain,
refuses removal of anything but the current tail (a mid-chain removal
would corrupt every later slot's start_lbn -- this architecture's own doc
already argues USB stays last specifically to avoid that), unlinks,
shrinks total_user_lbn, closes and frees the slot. Discards rather than
flushes dirty state -- the device is physically gone by the time this
runs (PORTSC disconnect only). Trigger wiring mirrors the attach path:
bot_msc_attached (set only once attach actually succeeds) gates a new
bot_msc_detach_pending flag set at PORTSC disconnect (not Disable Slot
completion, which is conditionally skipped and would miss concurrent
connect/disconnect pairs), consumed in sk_repl_idle().

Advisor flagged the real hazard ahead of time: block_words.c's VM block
window (blk_vm_lbn[]/blk_vm_cbuf[]) can go stale across a detach then a
same-LBN re-attach, and suggested a pointer-identity re-check in
blk_vm_load() as a minimal fix. That fix was implemented, then directly
falsified by its own designed-for-this test: attach a blank device, read
a block (populating the cache), detach, re-attach a device with distinct
content at the identical LBN, read again -- served stale content from
the first device. Root cause, confirmed live: glibc's allocator hands
free(slot) straight back to the very next same-size calloc(), so the
"fresh" and stale pointers were bitwise identical despite being two
different devices. Fixed properly with a monotonic blk_subsys_epoch()
counter (bumped on every attach/detach) checked by a new
blk_vm_check_epoch() helper at the one choke point (blk_vm_find(), plus
blk_vm_flush_all() which reads the same arrays directly) that covers
every path touching the window cache -- unfooled by address reuse.

Verified live with a new disk/usb-thumbdrive-test2.img fixture (distinct
content from the existing blank test image): attach A, read (cache hit
populated), detach, re-attach B at the same LBN, read again -- correctly
ran a fresh device read and returned B's real content, not A's stale
cached zeros. The failing pointer-comparison attempt's own capture log
kept as evidence, not deleted. All three architectures re-verified clean.
FABRIC-2.md Section X 2h marked complete -- enumeration through
hot-detach all live and verified; only WRITE(10) (2g's own still-open
item) remains unimplemented in the driver, not blocking anything here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
2026-08-25 14:10:05 -04:00

441 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 <stdint.h>
#include <string.h>
/* 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(void)
{
/* 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 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)
{
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();
}
/*
* 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));
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));
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);
}