Files
LithosAnanake/src/starkernel/repl.c
T
Robert Allan JamesandClaude Sonnet 5 8eaefeb9ee sk_repl_idle() auto-flush: implement Section V's "anything dirty? no? done" check
Makes blk_vm_flush_all() (block_words.c) non-static and declares it in
block_words.h -- it's already the entire implementation behind
SAVE-BUFFERS (block_word_save_buffers() is a one-line wrapper), so
sk_repl_idle() can call the exact same flush path outside word dispatch
without duplicating any logic. Cheap every idle tick regardless of dirty
state: every check inside is a small fixed-size scan, so no separate
pre-check was needed on top of it.

Caught a real bug via a live persistence test before trusting the
feature: the first version gated the flush on sk_repl_get_active_vm()
returning non-NULL, but NULL is that accessor's documented default
(Tripod's own USE-redirect override, "restore default dispatch") --
without an active USE redirect, the flush silently no-op'd for the
entire session. Confirmed live: wrote a byte via BUFFER (no
UPDATE/SAVE-BUFFERS), waited past the idle cadence, killed QEMU abruptly,
rebooted with the same disk image, read back 0 instead of the written
65. Fixed by threading the VM sk_repl_run()'s own loop already resolves
each iteration (g_repl_active_vm ? g_repl_active_vm : vm) down as a
parameter through sk_readline() into sk_repl_idle(), rather than trying
to re-derive it from an accessor with the wrong default. Re-ran the same
test after the fix: read back 65, matching the written byte -- the write
survived an abrupt kill with no explicit flush call anywhere in the
test, proving the idle-tick auto-flush genuinely ran.

All three architectures re-verified clean. FABRIC-2.md Section V item 6
and the corresponding Milestone 3 punch-list item marked done.

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

464 lines
19 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) 20232025 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 <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(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);
}