boards/BRINGUP.md A1, FABRIC-3 SXLII. xhci.c keeps a table of controllers (up to 8) and services each; kernel_main brings up index 0, 1, ... until there is none. repl.c's and blkio_usb.c's per-slot state is per controller now (a slot ID is a controller's own), and each USB keyboard keeps its own last report and lights; an unplugged keyboard releases what it held. Test first: keyboard and identity on a second controller behind a bridge, Zuse on the first -- before, only Zuse; after, all three, on all three ISAs. Hot-plug and unplug on the second controller. Acceptance unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1759 lines
88 KiB
C
1759 lines
88 KiB
C
/*
|
||
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-0.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 "log.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 "starkernel/kmalloc.h"
|
||
#include "starkernel/homeblocks_sig.h"
|
||
#include "starkernel/artemis_sig.h"
|
||
#include "starkernel/capsule_birth.h"
|
||
#include "starkernel/capsule_zuse_boot.h"
|
||
#include "starkernel/capsule_wirebind.h"
|
||
#include "starkernel/capsule_run.h"
|
||
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
|
||
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
|
||
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_service_pending() */
|
||
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
|
||
#include "block_subsystem.h"
|
||
#include "word_source/include/keyboard_words.h"
|
||
#include "starkernel/usb_kbd.h" /* usb_kbd_set_leds() -- the lock lights, A3 */
|
||
#include "word_source/include/block_words.h"
|
||
#include "word_registry.h"
|
||
#include "freestanding/stdio.h"
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
|
||
/* FABRIC-0.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;
|
||
|
||
/* FABRIC-3.md SXXV follow-up (2026-09-13): was "[VM name] (user) ok>"
|
||
* (e.g. "[Hera] (zuse) ok>") -- the VM-name bracket and the user segment
|
||
* were computed independently, and neither one told you whether the
|
||
* bracketed VM was the console proxy WIREBIND births per identity or the
|
||
* actual restricted identity VM behind it (e.g. "rajames" vs.
|
||
* "rajames~user") -- confirmed live to cause real confusion debugging
|
||
* the std79 lockdown. Unified into the single "[user@VM name] ok>" line
|
||
* prefix (console.c's emit_prefix(), via console_set_user_prefix_
|
||
* provider() below) -- one tag, always accurate about both who's at the
|
||
* console AND which VM a line actually came from/a command actually
|
||
* reaches. This function now only prints the bare prompt text; the user
|
||
* segment moved into the line prefix, which is a different call path per
|
||
* console.c's own commenting elsewhere but reaches every line including
|
||
* this prompt. */
|
||
static void sk_print_prompt(void) {
|
||
console_puts(SK_PROMPT_TEXT);
|
||
}
|
||
|
||
/* console_user_prefix_fn provider (console.h). 2026-09-22, Captain
|
||
* Bob's own instruction, restated precisely across several corrections
|
||
* this session: once a WIREBIND identity's own console is active (the
|
||
* physical console has been `USE`'d into it), the bracket must show
|
||
* that SAME name on both sides -- "[rajames@rajames]", not
|
||
* "[zuse@rajames]" -- because the identity console_get_vm_name()
|
||
* already tracks (WIREBIND births the console VM literally named after
|
||
* the human, mama_forth_words.c's capsule_console_birth()) IS her own
|
||
* VM, not a separate "who's driving" label layered on top of it.
|
||
* "R.A. James is also a VM" was the exact reasoning given.
|
||
*
|
||
* Below the top level (console_get_vm_name() != "Hera", i.e. USE or a
|
||
* BIRTH/RUN/CONNECT-* redirect has pointed the console somewhere else),
|
||
* that target's own name already answers "who/what is this" -- return
|
||
* it directly, and emit_prefix() ends up printing it on both sides
|
||
* (this function's return @ console_get_vm_name(), unchanged). At the
|
||
* top level (still on Hera, nothing has redirected the console yet),
|
||
* her own name doesn't say WHO is driving her, so this keeps the
|
||
* original zuse_session/WIREBIND-username logic for that one case --
|
||
* unchanged from before this fix, still correct: a live WIREBIND
|
||
* attach announces itself before USE is ever typed
|
||
* ("WIREBIND: <name> attached and ready -- USE it to begin"), and the
|
||
* prompt should already reflect that. */
|
||
static const char *sk_console_user_prefix(void) {
|
||
const char *vn = console_get_vm_name();
|
||
if (vn && strcmp(vn, "Hera") != 0) return vn;
|
||
VM *mama_vm = (VM *)sk_get_mama_vm();
|
||
if (mama_vm && mama_vm->zuse_session) return "zuse";
|
||
return capsule_wirebind_attached_username();
|
||
}
|
||
|
||
|
||
/*===========================================================================
|
||
* 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; }
|
||
|
||
/*===========================================================================
|
||
* Headless-until-login gate, decided 2026-09-05: no console for the
|
||
* running system unless a thumbdrive is present.
|
||
*
|
||
* Revised 2026-09-06: this was originally a one-way sticky flag
|
||
* (sk_console_mark_login(), set once by either login path and never
|
||
* cleared), gating only the very first entry into sk_repl_run() at boot.
|
||
* That let a real security gap through, found live during this session's
|
||
* own repeated identity-verification workflow: once anyone logged in even
|
||
* once, the console stayed visible for the rest of the boot -- a later
|
||
* full logout (nobody attached at all) fell through to a bare,
|
||
* unauthenticated "ok>" instead of going silent again. sk_console_
|
||
* identity_present() replaces the sticky flag with a live check (mirrors
|
||
* sk_print_prompt()'s own zuse_session/WIREBIND-username check exactly),
|
||
* and sk_repl_run()'s own main loop now re-checks it every iteration, not
|
||
* just once before the loop starts -- see its own call site below. */
|
||
static int sk_console_identity_present(void) {
|
||
VM *mama_vm = (VM *)sk_get_mama_vm();
|
||
if (mama_vm && mama_vm->zuse_session) return 1;
|
||
if (capsule_wirebind_attached_username() != (const char *)0) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/*===========================================================================
|
||
* Currently attached home-blocks device: mirrors g_repl_active_vm's own
|
||
* shape (FABRIC-2.md §F.9's own precedent for this exact accessor). Set
|
||
* once sk_repl_idle()'s own attach handling confirms HOMEBLOCKS_SIG_OK
|
||
* below; cleared on detach. RUNCAP (§F.6/§F.18) and, later, BINDSTEP's
|
||
* re-verify-live check (§F.9) both need this -- neither lives in this
|
||
* file, and usb_blk_dev/xdev below are function-static, invisible outside
|
||
* sk_repl_idle() without an accessor like this one.
|
||
*===========================================================================*/
|
||
|
||
static blkio_dev_t *g_homeblocks_dev = (void *)0;
|
||
static homeblocks_sig_t g_homeblocks_sig;
|
||
static int g_homeblocks_sig_valid = 0;
|
||
|
||
blkio_dev_t *sk_repl_get_homeblocks_dev(void) {
|
||
return g_homeblocks_sig_valid ? g_homeblocks_dev : (void *)0;
|
||
}
|
||
const homeblocks_sig_t *sk_repl_get_homeblocks_sig(void) {
|
||
return g_homeblocks_sig_valid ? &g_homeblocks_sig : (void *)0;
|
||
}
|
||
|
||
/* The currently attached USB block device, regardless of whether it
|
||
* checks out as a recognized home-blocks drive -- MINT (§F.8/§F.19)
|
||
* targets a blank/unminted drive, which by definition never sets
|
||
* g_homeblocks_dev above (that only latches on HOMEBLOCKS_SIG_OK).
|
||
* Set once blk_subsys_attach_device() succeeds below, cleared on detach
|
||
* alongside g_homeblocks_dev. */
|
||
static blkio_dev_t *g_attached_blk_dev = (void *)0;
|
||
|
||
blkio_dev_t *sk_repl_get_attached_blk_dev(void) {
|
||
return g_attached_blk_dev;
|
||
}
|
||
|
||
/* FABRIC-3.md §XXVI follow-on (2026-09-13): Artemis's own disk, once found
|
||
* generically via USB-MSC content signature (artemis_sig_t, 'ARTM') rather
|
||
* than the QEMU-only PCI virtio-blk vendor/device scan kernel_main.c still
|
||
* does synchronously at boot. Mirrors g_homeblocks_dev's own accessor
|
||
* shape. NULL on QEMU (virtio-blk finds Artemis before this file's idle
|
||
* loop ever runs) and on any boot where no USB-MSC device presents the
|
||
* 'ARTM' signature -- real bare-metal hardware is the case this exists
|
||
* for. Set once sk_word_blk_attach_ack() below confirms the storage-attach
|
||
* succeeded for a device this loop already recognized as Artemis's own;
|
||
* cleared on detach alongside g_homeblocks_dev/g_attached_blk_dev. */
|
||
static blkio_dev_t *g_artemis_usb_dev = (void *)0;
|
||
|
||
blkio_dev_t *sk_repl_get_artemis_usb_dev(void) {
|
||
return g_artemis_usb_dev;
|
||
}
|
||
|
||
/* Storage-attach messaging migration (Bob, 2026-09-07): Hera keeps
|
||
* polling/sig-checking, but no longer registers a newly-attached drive
|
||
* into the block subsystem herself -- that's Artemis's own domain now,
|
||
* reached via a real message (HERA-BLK-ATTACH-REQ, artemis:init.4th)
|
||
* instead of a direct blk_subsys_attach_device() call. Hera cannot use
|
||
* her own MSG-SEND for the outbound leg (kernel_main.c's own comment,
|
||
* ~line 784: loading common:messaging.4th into her dictionary was
|
||
* already tried and confirmed to silently drop every colon-definition
|
||
* touching a STADIUM-* primitive) -- she uses VM-EXEC directly instead,
|
||
* the same mechanism she already pumps MSG-TICK through. The reply
|
||
* leg needs no such workaround: Artemis's own MSG-TICK delivers her
|
||
* ack via VM-EXEC into Hera, which only requires BLK-ATTACH-ACK below
|
||
* to exist as an ordinary word here -- not a full messaging vocabulary.
|
||
*
|
||
* usb_blk_dev_slots/usb_blk_dev_slot_count were function-local statics
|
||
* inside sk_repl_idle() until now -- promoted to file scope so
|
||
* sk_word_blk_attach_ack() below (a real dictionary word, called from a
|
||
* completely different call stack than the idle loop) can resolve an
|
||
* incoming ack's raw pointer back to the slot it belongs to. */
|
||
|
||
/* One pending entry per slot, indexed the same way msc_slots[]/
|
||
* usb_blk_dev_slots[] already are (index 0 unused, matches precedent).
|
||
* Holds the sig-check result from the moment the storage-attach request
|
||
* was sent, so the deferred Zuse/WIREBIND birth calls -- which need that
|
||
* result -- can run once Artemis's ack confirms storage succeeded,
|
||
* without re-reading the drive a second time. */
|
||
typedef struct {
|
||
int pending;
|
||
homeblocks_sig_result_t sig_rc;
|
||
homeblocks_sig_t sig;
|
||
/* FABRIC-3.md §XXVI follow-on: set when the attach loop below already
|
||
* recognized this device as Artemis's own disk (artemis_sig_t 'ARTM'
|
||
* check, only attempted when sig_rc is HOMEBLOCKS_SIG_BLANK -- a
|
||
* device can't be both an identity thumbdrive and Artemis's disk).
|
||
* The ack handler uses this to run capsule_zuse_boot_load_root_pubkey()
|
||
* once storage-attach is confirmed, same as kernel_main.c's own
|
||
* virtio-blk path already does synchronously. */
|
||
int is_artemis;
|
||
} sk_blk_attach_pending_t;
|
||
|
||
/* Per xHCI controller (2026-10-14): slot IDs are a controller's own, so
|
||
* slot 1 on one and slot 1 on another are different devices. Each entry
|
||
* holds that controller's blkio_dev_t per slot and its pending attaches,
|
||
* the controller being xhci_dev_at() of the same index. */
|
||
typedef struct {
|
||
blkio_dev_t *slots;
|
||
uint32_t count;
|
||
sk_blk_attach_pending_t *pending;
|
||
} sk_usb_ctrl_t;
|
||
static sk_usb_ctrl_t g_usb_ctrl[XHCI_MAX_CONTROLLERS];
|
||
|
||
/* BLK-ATTACH-ACK ( dev-addr ok? -- ): VM-EXEC'd into Hera by Artemis's
|
||
* own MSG-TICK once HERA-BLK-ATTACH-REQ's BLK-ATTACH call resolves.
|
||
* Finds which slot the raw pointer belongs to, and -- only on success --
|
||
* runs the same Zuse/WIREBIND attach logic sk_repl_idle() used to run
|
||
* immediately and synchronously, now deferred until storage is
|
||
* confirmed ("wait for ack, safer for identity data" -- Bob, 2026-09-07).
|
||
* On failure, logs the same error sk_repl_idle() already logged for a
|
||
* failed blk_subsys_attach_device() call, and simply never births
|
||
* anything for this attach. */
|
||
static void sk_word_blk_attach_ack(VM *vm) {
|
||
if (vm->dsp < 1) {
|
||
log_message(LOG_ERROR, "BLK-ATTACH-ACK: stack underflow");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
cell_t ok_flag = vm_pop(vm);
|
||
cell_t dev_addr = vm_pop(vm);
|
||
blkio_dev_t *dev = (blkio_dev_t *)(uintptr_t)dev_addr;
|
||
|
||
/* Hera's own, run from Artemis's reply (2026-10-14): every VM has this
|
||
* word (word_registry.c), and it births identities -- from any other
|
||
* VM it is refused (FABRIC-3.md SXXXVII). */
|
||
if (vm != (VM *)sk_get_mama_vm()) {
|
||
console_println("BLK-ATTACH-ACK: refused -- Hera's own, from Artemis's reply");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
uint32_t found_ctrl = 0, found_slot = 0;
|
||
for (uint32_t c = 0; c < XHCI_MAX_CONTROLLERS && found_slot == 0; c++) {
|
||
if (!g_usb_ctrl[c].slots || !g_usb_ctrl[c].pending) continue;
|
||
for (uint32_t i = 1; i < g_usb_ctrl[c].count; i++) {
|
||
if (&g_usb_ctrl[c].slots[i] == dev) { found_ctrl = c; found_slot = i; break; }
|
||
}
|
||
}
|
||
if (found_slot == 0) return;
|
||
sk_blk_attach_pending_t *pending = g_usb_ctrl[found_ctrl].pending;
|
||
if (!pending[found_slot].pending) return;
|
||
pending[found_slot].pending = 0;
|
||
|
||
if (!ok_flag) {
|
||
log_message(LOG_ERROR, "xhci: USB MSC block-subsystem attach failed");
|
||
return;
|
||
}
|
||
|
||
xhci_dev_t *xdev = xhci_dev_at(found_ctrl);
|
||
xhci_msc_slot_t *ms = xdev ? xhci_msc_slot_for(xdev, found_slot) : (void *)0;
|
||
if (ms) ms->bot_msc_attached = 1;
|
||
g_attached_blk_dev = dev;
|
||
|
||
/* FABRIC-3.6.md task 3.8 evidence: this handler IS the real drain
|
||
* target -- sk_hermes_drain_checkpoint() (task 3.4) called
|
||
* vm_interpret() on the ack payload, which is how we got here.
|
||
* stadium_conserved() (task 0.7/2.7's four-term form) holds at
|
||
* every instant, mid-hold included, so printing it here -- while
|
||
* this message's heat is still held, before sk_hermes_drain_
|
||
* checkpoint()'s own release/pop run just after this function
|
||
* returns -- is real evidence, not a synthetic self-test. Note the
|
||
* limit: this is evidence for the mid-hold instant, not the
|
||
* post-release state (release/pop happen after this function
|
||
* returns, in the caller) -- see FABRIC-3.6.md task 3.8's own
|
||
* write-up. console_println, not log_message(): confirmed live
|
||
* (this repl.c's own log_message() calls, at every level, do not
|
||
* appear anywhere in a real serial-log boot capture in this build
|
||
* -- log_message()'s fprintf(stderr, ...) is not wired to the
|
||
* serial console here) -- log_message() would have been silently
|
||
* invisible, defeating the point of evidence. One line, not two
|
||
* (dropped the earlier pre-send line from KH-BLK-ATTACH-SEND
|
||
* below): this fires once per real USB attach, not per word, so
|
||
* it is not the console_println-overuse case memory
|
||
* project_production_logging_cleanup_needed flags. */
|
||
{
|
||
VMRegistryEntry artemis_entry;
|
||
uint64_t held, pulled, returned, consumed;
|
||
char line[160];
|
||
int conserved = -1; /* -1 = Artemis not found */
|
||
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
|
||
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
|
||
artemis_entry.vm_ptr) {
|
||
conserved = stadium_conserved(artemis_entry.vm_id);
|
||
}
|
||
snprintf(line, sizeof(line),
|
||
"Kernel-Hermes BLK-ATTACH-EVENT (real, Stage C): ledger held=%llu "
|
||
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(Artemis)=%s",
|
||
(unsigned long long)held, (unsigned long long)pulled,
|
||
(unsigned long long)returned, (unsigned long long)consumed,
|
||
conserved < 0 ? "UNKNOWN" : (conserved ? "true" : "FALSE"));
|
||
console_println(line);
|
||
}
|
||
|
||
homeblocks_sig_result_t sig_rc = pending[found_slot].sig_rc;
|
||
homeblocks_sig_t sig = pending[found_slot].sig;
|
||
int is_artemis = pending[found_slot].is_artemis;
|
||
|
||
if (is_artemis) {
|
||
/* FABRIC-3.md §XXVI follow-on: bus-agnostic Artemis discovery.
|
||
* Mirrors kernel_main.c's own virtio-blk-found branch exactly
|
||
* (blk_subsys_attach_device() there is this device's equivalent,
|
||
* already done for us above via HERA-BLK-ATTACH-REQ/BLK-ATTACH --
|
||
* ok_flag being true is that confirmation). Safe to call even if
|
||
* virtio-blk already found Artemis first (the common QEMU case,
|
||
* since that path runs synchronously before this idle loop ever
|
||
* gets a beat): capsule_zuse_boot_load_root_pubkey() is a no-op
|
||
* once mama_vm->zuse_root_pubkey_known is already set. */
|
||
g_artemis_usb_dev = dev;
|
||
log_message(LOG_INFO, "xhci: Artemis's own disk attached via USB-MSC");
|
||
capsule_zuse_boot_load_root_pubkey((VM *)sk_get_mama_vm());
|
||
}
|
||
|
||
capsule_zuse_boot_try_attach(dev, sig_rc, &sig, (VM *)sk_get_mama_vm());
|
||
if (sig_rc == HOMEBLOCKS_SIG_OK) {
|
||
capsule_wirebind_try_attach(dev, &sig, (VM *)sk_get_mama_vm());
|
||
}
|
||
}
|
||
|
||
/* FABRIC-3.6.md task 3.8 (Stage C, SXXXIV.2/.3): BLK-ATTACH-EVENT's real
|
||
* cutover -- the reply leg (Artemis -> Hera ack) that used to flow
|
||
* through common:messaging.4th's MSG-SEND/MSG-TICK now goes through
|
||
* kernel-Hermes's sk_hermes_send_one()/sk_hermes_drain_checkpoint()
|
||
* (tasks 3.3/3.4/3.6) instead. FORTH Hermes never sees a BLK-ATTACH-
|
||
* EVENT message again -- exactly SXXXIV.2's partition rule ("one owner
|
||
* per message, never shared"). The request leg (Hera -> Artemis,
|
||
* kernel_main.c's own "S\" <dev-addr> HERA-BLK-ATTACH-REQ\" S\"
|
||
* Artemis\" VM-EXEC" a few lines up) was never real FORTH messaging
|
||
* traffic to begin with -- no type tag, no arena, a direct VM-EXEC
|
||
* forced by Hera's own STADIUM-* colon-word limitation documented
|
||
* above -- so it is untouched here; this task cuts over the one
|
||
* message type that actually flowed through a messaging layer.
|
||
*
|
||
* sk_hermes_drain_checkpoint() (task 3.4) calls vm_interpret() directly
|
||
* on a message's payload_addr, so it must be NUL-terminated; since
|
||
* 2026-10-14 the kernel writes the whole reply text into this buffer
|
||
* itself (KH-BLK-ATTACH-SEND, below), from two numbers.
|
||
*
|
||
* CORRECTED 2026-09-22 (real defect, this task's own findings log):
|
||
* this comment used to claim "static, not stack-local" was load-bearing
|
||
* because sk_hermes_send_one() stored payload_addr out-of-line, the
|
||
* caller having to keep it alive until drained -- and that a second
|
||
* attach before the first drains overwriting this buffer was "the same
|
||
* shape ATTACH-ACK-BUF itself already had, not a new hazard." That
|
||
* claim was wrong: it WAS a new, real payload-aliasing defect (two
|
||
* sends before either drains both pointing at this same buffer,
|
||
* whichever wrote last silently winning), confirmed live and fixed at
|
||
* the source (kernel_hermes.c's sk_hermes_send_one() now copies into
|
||
* the message's own storage immediately, kernel_hermes.h's own doc
|
||
* comment on SkHermesMessage has the full account). This buffer no
|
||
* longer needs to survive past the KH-BLK-ATTACH-SEND call that sends
|
||
* it -- staying `static` here is now just a convenience (avoids an
|
||
* 80-byte stack frame), not a correctness requirement. */
|
||
#define SK_KH_BLK_ATTACH_BUF_SIZE 80
|
||
static char g_kh_blk_attach_buf[SK_KH_BLK_ATTACH_BUF_SIZE];
|
||
|
||
/* KH-BLK-ATTACH-SEND ( dev-addr ok? -- sent? ): Artemis's storage-attach
|
||
* reply to Hera. The kernel builds the message text itself --
|
||
* "<dev-addr> <ok?> BLK-ATTACH-ACK" -- from the two numbers, and only for
|
||
* Artemis. Until 2026-10-14 it took ( paddr plen ), any text at all, from
|
||
* any VM, and Hera ran that text in her own dictionary, Zuse's: a
|
||
* WIREBIND identity could have any FORTH run as Zuse (shown live,
|
||
* FABRIC-3.md SXXXVII). `from`/`to` are derived from the calling VM and
|
||
* sk_get_mama_vm() -- the caller never names a VMUuid. Refusal
|
||
* (reservoir/arena/destination-queue exhaustion) is logged rather than
|
||
* silently dropped -- FABRIC-3.5.md SXXXV.0 names silent failure as this
|
||
* project's single most common defect shape, and identity birth gates on
|
||
* this message. */
|
||
static void sk_word_kh_blk_attach_send(VM *vm) {
|
||
VMRegistryEntry artemis;
|
||
VM *mama_vm = (VM *)sk_get_mama_vm();
|
||
cell_t ok_flag, dev_addr;
|
||
int n, rc;
|
||
|
||
if (vm->dsp < 1) {
|
||
console_println("KH-BLK-ATTACH-SEND: expects dev-addr ok? -- nothing on the stack");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
ok_flag = vm_pop(vm);
|
||
dev_addr = vm_pop(vm);
|
||
|
||
if (capsule_vm_find_by_name_nocase("Artemis", &artemis) != 0 ||
|
||
artemis.vm_ptr != (void *)vm) {
|
||
console_println("KH-BLK-ATTACH-SEND: refused -- only Artemis replies to a storage attach");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
n = snprintf(g_kh_blk_attach_buf, sizeof(g_kh_blk_attach_buf),
|
||
"%llu %u BLK-ATTACH-ACK",
|
||
(unsigned long long)(uint64_t)dev_addr, ok_flag ? 1u : 0u);
|
||
if (n <= 0 || (size_t)n >= sizeof(g_kh_blk_attach_buf)) {
|
||
console_println("KH-BLK-ATTACH-SEND: FAILED -- reply does not fit");
|
||
vm_push(vm, 0);
|
||
return;
|
||
}
|
||
|
||
rc = sk_hermes_send_one(vm->stadium_vm_id, mama_vm->stadium_vm_id,
|
||
SK_HERMES_MSG_TYPE_BLK_ATTACH, 0,
|
||
g_kh_blk_attach_buf, (uint32_t)(n + 1));
|
||
if (rc != 0) {
|
||
log_message(LOG_ERROR, "KH-BLK-ATTACH-SEND: kernel-Hermes refused the send");
|
||
}
|
||
vm_push(vm, rc == 0 ? 1 : 0);
|
||
}
|
||
|
||
/* KH-ELEVATE-SEND was here until 2026-10-14: it sent any text to Hera as
|
||
* an ELEVATE-REQUEST, and Hera ran it as Zuse. Removed with word-ACL
|
||
* elevation itself (FABRIC-3.7, closed as not needed; FABRIC-3.md
|
||
* SXXXVII). */
|
||
|
||
void sk_repl_register_words(VM *vm) {
|
||
register_word(vm, "BLK-ATTACH-ACK", sk_word_blk_attach_ack);
|
||
register_word(vm, "KH-BLK-ATTACH-SEND", sk_word_kh_blk_attach_send);
|
||
}
|
||
|
||
/*===========================================================================
|
||
* Idle heartbeat service
|
||
*
|
||
* Called from sk_console_readline()/sk_console_getkey() 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) */
|
||
|
||
/* Cursor blink, 2026-09-05: the framebuffer cursor (now a thin vertical
|
||
* bar, vt100.c's vt100_draw_cursor()) blinks on/off every
|
||
* SK_CURSOR_BLINK_INTERVAL ticks while sk_console_readline()'s idle loop
|
||
* is spinning -- i.e. whenever nothing has been typed for that long,
|
||
* whether sitting at a bare prompt or paused mid-edit. g_cursor_visible
|
||
* tracks which half of the blink cycle is current; sk_cursor_show() below
|
||
* is the single place that resets the cycle back to "on" and redraws --
|
||
* every deterministic draw site (fresh prompt, echoed character,
|
||
* backspace) calls it instead of vt100_draw_cursor() directly, so typing
|
||
* always shows a solid cursor rather than possibly landing mid-blink. */
|
||
#define SK_CURSOR_BLINK_INTERVAL 50u /* ticks between blink toggles (500 ms at 100 Hz) */
|
||
|
||
static uint64_t g_cursor_blink_tick; /* zero-initialized (BSS) */
|
||
static int g_cursor_visible = 1;
|
||
|
||
static void sk_cursor_show(void)
|
||
{
|
||
g_cursor_visible = 1;
|
||
g_cursor_blink_tick = heartbeat_ticks();
|
||
console_fb_draw_cursor();
|
||
}
|
||
|
||
/* Reentrancy guards for the kernel-Hermes drain pump inside sk_repl_idle().
|
||
*
|
||
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
|
||
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
|
||
* Phase C, common:messaging.4th, now removed). The mechanism changed
|
||
* twice -- to a direct sk_hermes_drain_checkpoint() per VM (2026-09-22),
|
||
* then to one switch per beat plus Hera's own drain (2026-10-14, one
|
||
* path) -- but the reentrancy hazard these two flags exist for is
|
||
* unchanged for Hera's own drain: it calls vm_interpret() internally
|
||
* (on the drained message's own payload), and sk_repl_idle() is itself
|
||
* called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()/
|
||
* sk_console_readline(), which run *from inside* the executing VM's own
|
||
* vm_interpret once a FORTH word reads input mid-line). vm_interpret()
|
||
* is not reentrant -- it resets the VM's single input_buffer/input_pos/
|
||
* input_length (vm_core.c) on entry. Calling it on mama at that point
|
||
* would re-enter her interpreter mid-parse and silently truncate the
|
||
* rest of the line. Two flags keep the pump off every path that could
|
||
* re-enter an interpreter:
|
||
* - g_mama_interpreting: set while Hera is executing a dispatched line, so
|
||
* the pump defers to the next safe (prompt) boundary.
|
||
* - g_idle_pump_active: belt-and-suspenders; stops recursive re-entry
|
||
* from inside the pump's own drain call.
|
||
*/
|
||
static int g_mama_interpreting; /* zero-initialized (BSS) */
|
||
static int g_idle_pump_active; /* zero-initialized (BSS) */
|
||
|
||
/* FABRIC-3.md SXXII (2026-09-12) recorded a real O(N) bottleneck here --
|
||
* the messaging pump below used to walk the entire live-VM registry
|
||
* every idle beat and dispatch a full VM-EXEC "MSG-TICK" into every one
|
||
* of them, live-caught as a wall-time-dominating cost at just 9 VMs on
|
||
* riscv64 -- and fixed it with round-robin batching (a persistent
|
||
* cursor, a fixed number of dispatches per beat). FABRIC-3.6.md Phase 4
|
||
* (Stage E), 2026-09-22: that whole mechanism (MSG-TICK, the batching
|
||
* cap, and this comment's own prior form) is gone along with
|
||
* common:messaging.4th itself. Since 2026-10-14 a beat makes at most one
|
||
* switch (the one the switch signal has pending) and drains only Hera:
|
||
* O(1), no cap needed. */
|
||
|
||
static void sk_repl_idle(VM *active_vm)
|
||
{
|
||
/* Close any dangling output line before this bottom half emits its own
|
||
* chatter (xhci attach/detach progress, block-subsystem notices). If we
|
||
* are mid-prompt-line -- the REPL started the attach while sitting at
|
||
* "ok> " -- a bare console_println() would otherwise glue its text onto
|
||
* the prompt and inherit no {VMName} prefix (g_line_start is 0). A
|
||
* fresh line first keeps every idle line prefix-tagged and readable,
|
||
* matching what an interactive typing session expects. No-op when the
|
||
* console is already at a line boundary.
|
||
*
|
||
* FABRIC-2.md §I.9 fix, 2026-09-05: this newline is now deferred (see
|
||
* console_ensure_line_start()'s own doc comment) -- it only actually
|
||
* reaches the console if something below really prints. tx_before_idle
|
||
* lets this function tell "nothing happened" apart from "something did"
|
||
* the same way the reanchor check further up this file already does,
|
||
* so a beat with nothing to report can cancel the deferred newline
|
||
* before returning, leaving the bare prompt line completely untouched
|
||
* instead of visibly snapping it to a fresh blank line every ~1s. */
|
||
console_ensure_line_start();
|
||
uint64_t tx_before_idle = console_tx_count();
|
||
|
||
/* 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). */
|
||
/* Every controller in turn (2026-10-14): each has its own slots. */
|
||
for (uint32_t xc = 0; xc < xhci_dev_count() && xc < XHCI_MAX_CONTROLLERS; xc++) {
|
||
xhci_dev_t *xdev = xhci_dev_at(xc);
|
||
sk_usb_ctrl_t *uc = &g_usb_ctrl[xc];
|
||
/* FABRIC-3.md §VII (2026-09-05): was `static blkio_dev_t usb_blk_dev`
|
||
* ("single-device scope, matching the xHCI driver's own") -- now a
|
||
* per-slot registry, same sizing/allocation precedent as xhci_dev_t's
|
||
* own msc_slots[] (sized off xdev->max_slots, allocated once on first
|
||
* idle tick after xdev is known, since this file has no bringup-time
|
||
* hook of its own). Every slot with a pending attach/detach flag is
|
||
* serviced this tick, not just one -- a single `if` here used to mean
|
||
* a second device's pending flag would sit unnoticed until the first's
|
||
* flag was consumed and cleared. */
|
||
if (xdev && (!uc->slots || uc->count < xdev->max_slots + 1)) {
|
||
size_t bytes = (size_t)(xdev->max_slots + 1) * sizeof(blkio_dev_t);
|
||
blkio_dev_t *fresh = (blkio_dev_t *)kmalloc_aligned(bytes, 64);
|
||
size_t pending_bytes = (size_t)(xdev->max_slots + 1) * sizeof(sk_blk_attach_pending_t);
|
||
sk_blk_attach_pending_t *fresh_pending = (sk_blk_attach_pending_t *)kmalloc_aligned(pending_bytes, 64);
|
||
if (fresh && fresh_pending) {
|
||
memset(fresh, 0, bytes);
|
||
memset(fresh_pending, 0, pending_bytes);
|
||
uc->slots = fresh;
|
||
uc->count = xdev->max_slots + 1;
|
||
uc->pending = fresh_pending;
|
||
}
|
||
}
|
||
for (uint32_t slot_id = 1; xdev && uc->slots && slot_id <= xdev->max_slots; slot_id++) {
|
||
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
|
||
if (!ms || !ms->bot_msc_attach_pending) continue;
|
||
ms->bot_msc_attach_pending = 0;
|
||
blkio_dev_t *usb_blk_dev = &uc->slots[slot_id];
|
||
|
||
int rc = blkio_usb_open_msc(usb_blk_dev, xdev, slot_id);
|
||
if (rc == 0) {
|
||
/* FABRIC-2.md Milestone 4: warn on blank/foreign/unrecognized
|
||
* media -- the "warn" half. No "refuse" half yet: blkio_usb.c
|
||
* has no SCSI WRITE(10) support at all (Milestone 2's biggest
|
||
* open item), so there is no write path today to refuse --
|
||
* only read-only attach, which is also the general-purpose USB
|
||
* block I/O path this repo already relies on for unrelated
|
||
* testing, not exclusively a home-blocks identity workflow.
|
||
* Refusing attach on blank media here would break that
|
||
* legitimate use without protecting anything real yet. Refuse
|
||
* belongs on the write path, once WRITE(10) gives it something
|
||
* to gate.
|
||
*
|
||
* HOMEBLOCKS_SIG_START_FBLOCK (devblock 1): the real, final
|
||
* location -- GPT was dropped permanently, this is not an
|
||
* interim value (FABRIC-2.md §F.8/§F.13). */
|
||
homeblocks_sig_t sig;
|
||
homeblocks_sig_result_t sig_rc =
|
||
homeblocks_sig_check(usb_blk_dev, HOMEBLOCKS_SIG_START_FBLOCK, &sig);
|
||
switch (sig_rc) {
|
||
case HOMEBLOCKS_SIG_OK:
|
||
log_message(LOG_DEBUG, "xhci: USB drive recognized as a home-blocks drive");
|
||
/* FABRIC-3.md §VII (2026-09-05): g_homeblocks_dev/
|
||
* g_attached_blk_dev stay single "most recently
|
||
* attached" pointers by deliberate, scoped choice --
|
||
* the multi-device fix's target was the driver/backend
|
||
* corrupting each other's live state when two devices
|
||
* are attached at once (fixed above and in xhci.c/
|
||
* blkio_usb.c), not making every console-facing FORTH
|
||
* word (RUNCAP et al, mama_forth_words.c) multi-device
|
||
* aware -- the console still interacts with one device
|
||
* at a time, matching its own single-active-REPL
|
||
* design. Revisit if a real use case needs otherwise. */
|
||
g_homeblocks_dev = usb_blk_dev;
|
||
g_homeblocks_sig = sig;
|
||
g_homeblocks_sig_valid = 1;
|
||
break;
|
||
case HOMEBLOCKS_SIG_BLANK:
|
||
log_message(LOG_DEBUG, "xhci: USB drive not recognized (blank or foreign media) -- read-only general use only");
|
||
break;
|
||
case HOMEBLOCKS_SIG_BAD_VERSION:
|
||
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header of an unrecognized version -- read-only general use only");
|
||
break;
|
||
case HOMEBLOCKS_SIG_BAD_CRC:
|
||
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header that fails its checksum (corrupt or tampered) -- read-only general use only");
|
||
break;
|
||
case HOMEBLOCKS_SIG_READ_ERROR:
|
||
log_message(LOG_ERROR, "xhci: USB drive signature check failed to read the device -- read-only general use only");
|
||
break;
|
||
}
|
||
|
||
/* FABRIC-3.md §XXVI follow-on: a device isn't an identity
|
||
* thumbdrive (sig_rc above came back BLANK, i.e. no 'LAHB'
|
||
* magic) -- check whether it's Artemis's own disk instead
|
||
* (distinct 'ARTM' magic, same devblock-1 convention). Only
|
||
* attempted on BLANK, not on every device: a drive that
|
||
* already checked out as home-blocks (or failed a home-blocks
|
||
* version/CRC check) can't also be Artemis's disk, and
|
||
* skipping the second read keeps the common identity-drive
|
||
* case down to one signature check per attach, same as
|
||
* before this feature existed. */
|
||
int is_artemis_disk = 0;
|
||
if (sig_rc == HOMEBLOCKS_SIG_BLANK) {
|
||
artemis_sig_t asig;
|
||
artemis_sig_result_t art_rc = artemis_sig_check(usb_blk_dev, &asig);
|
||
if (art_rc == ARTEMIS_SIG_OK) {
|
||
log_message(LOG_DEBUG, "xhci: USB drive recognized as Artemis's own disk");
|
||
is_artemis_disk = 1;
|
||
}
|
||
}
|
||
|
||
/* FABRIC-2.md §F.20/§F.21 / §F.5/§F.23 (WIREBIND): Zuse
|
||
* genesis-mint/attach-authenticate and regular-identity
|
||
* verify-then-birth-then-pair both used to run synchronously,
|
||
* right here, before storage was even registered. Moved
|
||
* (Bob, 2026-09-07, "wait for ack, safer for identity data")
|
||
* to sk_word_blk_attach_ack() above, run only once Artemis
|
||
* confirms the storage-attach succeeded -- identity birth
|
||
* no longer happens on top of storage that might not have
|
||
* registered. Stash what that deferred call needs. */
|
||
if (uc->pending) {
|
||
uc->pending[slot_id].pending = 1;
|
||
uc->pending[slot_id].sig_rc = sig_rc;
|
||
uc->pending[slot_id].sig = sig;
|
||
uc->pending[slot_id].is_artemis = is_artemis_disk;
|
||
}
|
||
|
||
/* Storage-attach registration (blk_subsys_attach_device(),
|
||
* BLK-ATTACH C primitive) is Artemis's own domain now, not
|
||
* Hera's -- she keeps polling/sig-checking but no longer
|
||
* performs this step herself. Hera can't use her own
|
||
* MSG-SEND (see uc->slots's own doc comment
|
||
* above for why), so this is a direct VM-EXEC into
|
||
* Artemis's dictionary -- the same mechanism the MSG-TICK
|
||
* pump below already uses -- rather than a real enqueued
|
||
* message. HERA-BLK-ATTACH-REQ (capsules/artemis/init.4th)
|
||
* runs BLK-ATTACH then replies via her own real MSG-SEND,
|
||
* delivered back to Hera by the ordinary MSG-TICK pump. */
|
||
{
|
||
char cmd[96];
|
||
int n = snprintf(cmd, sizeof(cmd),
|
||
"S\" %llu HERA-BLK-ATTACH-REQ\" S\" Artemis\" VM-EXEC",
|
||
(unsigned long long)(uintptr_t)usb_blk_dev);
|
||
if (n > 0 && (size_t)n < sizeof(cmd)) {
|
||
vm_interpret((VM *)sk_get_mama_vm(), cmd);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 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).
|
||
* Per-slot loop now (FABRIC-3.md §VII, 2026-09-05), same reasoning as
|
||
* the attach loop above. */
|
||
for (uint32_t slot_id = 1; xdev && uc->slots && slot_id <= xdev->max_slots; slot_id++) {
|
||
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
|
||
if (!ms || !ms->bot_msc_detach_pending) continue;
|
||
ms->bot_msc_detach_pending = 0;
|
||
blkio_dev_t *usb_blk_dev = &uc->slots[slot_id];
|
||
|
||
blk_subsys_detach_device(usb_blk_dev);
|
||
if (g_homeblocks_dev == usb_blk_dev) {
|
||
g_homeblocks_dev = (void *)0;
|
||
g_homeblocks_sig_valid = 0;
|
||
}
|
||
if (g_attached_blk_dev == usb_blk_dev) {
|
||
g_attached_blk_dev = (void *)0;
|
||
}
|
||
|
||
/* FABRIC-2.md §F.10 decision 2 (UNCLEAN, closed alongside EJECT):
|
||
* the device is already gone -- no-op if WIREBIND never had
|
||
* anything tracked (general-purpose USB use, not a home-blocks
|
||
* identity drive), OR if the device that left wasn't the one
|
||
* WIREBIND tracks (FABRIC-3.md §VII follow-on, 2026-09-06 --
|
||
* genuine multi-device attach means it might be a different
|
||
* device leaving while a WIREBIND user's own stays attached). */
|
||
capsule_wirebind_unclean_detach(usb_blk_dev);
|
||
|
||
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: Zuse logs out on device
|
||
* removal exactly like a WIREBIND user -- no-op if the device
|
||
* that just left wasn't hers (FABRIC-3.md §VII follow-on,
|
||
* 2026-09-06: that no-op is now real, see capsule_zuse_boot_
|
||
* logout()'s own updated doc comment). */
|
||
capsule_zuse_boot_logout((VM *)sk_get_mama_vm(), usb_blk_dev);
|
||
}
|
||
}
|
||
|
||
/* FABRIC-0.md/FABRIC-1.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_console_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-2.md §I.2, built 2026-09-04: heat/wear-leveling migration
|
||
* trigger -- one linear scan of Artemis's own device per idle tick
|
||
* (same ~1 Hz SK_IDLE_BEAT_INTERVAL cadence this whole function
|
||
* already runs at, chosen so a hot devblock is caught proactively
|
||
* rather than only on a failed write). See block_subsystem.c's own
|
||
* doc comment on blk_migration_idle_check() for what's built (heat-
|
||
* based relocation) vs. deliberately left open (overflow-triggered
|
||
* migration, needs a call site threaded from WIREBIND).
|
||
*
|
||
* FABRIC-3.md, 2026-09-09: skipped while any g_usb_ctrl[].pending
|
||
* entry is still pending -- this scan and the storage-attach message
|
||
* round-trip (HERA-BLK-ATTACH-REQ/BLK-ATTACH-ACK) both touch the
|
||
* block subsystem/Artemis's own virtio-backed storage, and live-
|
||
* caught the two interleaving is where a real vblk_io() request stops
|
||
* getting a used-ring completion (root cause not fully isolated, see
|
||
* virtio_blk.c's own doc comment on vblk_io()'s reduced spin bound --
|
||
* that's the safety net; this is the actual avoidance). One deferred
|
||
* scan is harmless -- next idle tick retries, same as any other tick
|
||
* with nothing to do. */
|
||
{
|
||
int attach_in_flight = 0;
|
||
for (uint32_t c = 0; c < XHCI_MAX_CONTROLLERS && !attach_in_flight; c++) {
|
||
uint32_t pi;
|
||
if (!g_usb_ctrl[c].pending) continue;
|
||
for (pi = 0; pi < g_usb_ctrl[c].count; pi++) {
|
||
if (g_usb_ctrl[c].pending[pi].pending) { attach_in_flight = 1; break; }
|
||
}
|
||
}
|
||
if (!attach_in_flight) blk_migration_idle_check();
|
||
}
|
||
|
||
/* FABRIC-2.md §I.2's own overflow trigger, closed 2026-09-05: the
|
||
* call site named above, now built. Same cadence, same idle-tick
|
||
* neighbor -- see capsule_wirebind_overflow_idle_check()'s own doc
|
||
* comment for what it does and why it's a one-time extension, not a
|
||
* growth loop. */
|
||
capsule_wirebind_overflow_idle_check();
|
||
|
||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): same cadence, cleans up
|
||
* the per-device live-identity table once the Stage 3 checkpoint has
|
||
* actually reaped a pending_reap VM -- see that function's own doc
|
||
* comment. */
|
||
capsule_wirebind_reap_idle_check();
|
||
|
||
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14). Hera has the only
|
||
* persistent idle beat, so this is where a VM with messages waiting
|
||
* gets its turn while she sits at her prompt: the switch the switch
|
||
* signal has pending is made here, from whichever VM owns this stack,
|
||
* and the target takes its own messages on its own stack (switch.c's
|
||
* sk_vm_switch_entry()). Until 2026-10-14 this walked every live VM
|
||
* and ran each one's messages for it, here, on Hera's stack -- the
|
||
* second delivery path, now gone (and refused: kernel_hermes.c). Before
|
||
* 2026-09-22 it VM-EXEC'd MSG-TICK into each VM instead (FABRIC-2.md
|
||
* Phase C, common:messaging.4th, removed). */
|
||
VM *mama = (VM *)sk_get_mama_vm();
|
||
|
||
/* Reentrancy guard: not while Hera is mid-line (a dispatched line, or
|
||
* recursively from within this beat's own work). A switch made from
|
||
* inside a KEY/EXPECT/QUERY wait would be safe -- the whole stack is
|
||
* parked and comes back intact -- but Hera's own messages run in her
|
||
* interpreter, which must not be entered mid-line; and nothing here is
|
||
* urgent: it resumes next beat, at the prompt. */
|
||
if (g_mama_interpreting || g_idle_pump_active) {
|
||
if (console_tx_count() == tx_before_idle) {
|
||
console_cancel_deferred_line_start();
|
||
}
|
||
return;
|
||
}
|
||
|
||
g_idle_pump_active = 1;
|
||
{
|
||
VM *cur = sk_vm_switch_current_vm();
|
||
if (cur) (void)sk_vm_switch_service_pending(cur);
|
||
/* Hera's own messages, when this beat is running on her stack:
|
||
* she is the VM switched in, at her prompt -- her own context. */
|
||
if (!cur || cur == mama) (void)sk_hermes_drain_checkpoint(mama);
|
||
}
|
||
g_idle_pump_active = 0;
|
||
|
||
if (console_tx_count() == tx_before_idle) {
|
||
console_cancel_deferred_line_start();
|
||
}
|
||
}
|
||
|
||
/*===========================================================================
|
||
* FABRIC-0.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_console_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
|
||
#define SK_KEY_CAPSLOCK 58u
|
||
#define SK_KEY_NUMLOCK 69u
|
||
#define SK_KEY_KPENTER 96u
|
||
#define SK_KEY_KPSLASH 98u
|
||
#define SK_KEY_102ND 86u
|
||
|
||
static int g_kbd_shift_down; /* zero-initialized (BSS) */
|
||
static int g_kbd_alt_down;
|
||
|
||
/* A full-size keyboard (Captain Bob, 2026-10-14: "include caps lock,
|
||
* numlock, keypad and arrow"). Caps Lock and Num Lock are toggles, held
|
||
* here for every keyboard alike and shown on a USB keyboard's lights
|
||
* (usb_kbd_set_leds()). Num Lock starts on, as a PC's firmware usually
|
||
* leaves it. Caps Lock acts on letters only; Shift reverses it. */
|
||
static int g_kbd_caps_lock;
|
||
static int g_kbd_num_lock = 1;
|
||
|
||
static void sk_kbd_show_locks(void)
|
||
{
|
||
usb_kbd_set_leds((uint8_t)((g_kbd_num_lock ? USB_KBD_LED_NUM : 0) |
|
||
(g_kbd_caps_lock ? USB_KBD_LED_CAPS : 0)));
|
||
}
|
||
|
||
/* The keypad: what each key types. With Num Lock on, 0-9 and '.'; with it
|
||
* off those are Home/arrows/PgUp/... -- navigation, which this REPL's
|
||
* append/backspace editing has no use for, so they type nothing (as the
|
||
* arrow keys do). / * - + always type; keypad Enter is Enter. */
|
||
static int sk_kbd_keypad(uint16_t keycode)
|
||
{
|
||
switch (keycode) {
|
||
case 55u: return '*';
|
||
case 74u: return '-';
|
||
case 78u: return '+';
|
||
case SK_KEY_KPSLASH: return '/';
|
||
case SK_KEY_KPENTER: return '\n';
|
||
default: break;
|
||
}
|
||
if (!g_kbd_num_lock) return 0;
|
||
switch (keycode) {
|
||
case 71u: return '7'; case 72u: return '8'; case 73u: return '9';
|
||
case 75u: return '4'; case 76u: return '5'; case 77u: return '6';
|
||
case 79u: return '1'; case 80u: return '2'; case 81u: return '3';
|
||
case 82u: return '0'; case 83u: return '.';
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
/* 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_CAPSLOCK) { g_kbd_caps_lock = !g_kbd_caps_lock; sk_kbd_show_locks(); continue; }
|
||
if (keycode == SK_KEY_NUMLOCK) { g_kbd_num_lock = !g_kbd_num_lock; sk_kbd_show_locks(); continue; }
|
||
{
|
||
int k = sk_kbd_keypad(keycode);
|
||
if (k) return k;
|
||
}
|
||
if (keycode == SK_KEY_102ND) return g_kbd_shift_down ? '|' : '\\';
|
||
|
||
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 lower = sk_kbd_unshifted[keycode];
|
||
int shift = g_kbd_shift_down;
|
||
if (lower >= 'a' && lower <= 'z' && g_kbd_caps_lock) shift = !shift;
|
||
char c = shift ? sk_kbd_shifted[keycode] : lower;
|
||
if (c) return (unsigned char)c;
|
||
}
|
||
/* unmapped keycode -- drop and keep draining */
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* One raw byte from either input source (serial console or the keyboard-
|
||
* event bridge), non-blocking, -1 if neither has one ready right now. Not
|
||
* itself a FORTH word -- the shared byte-fetch underneath sk_console_getkey()/
|
||
* sk_console_key_available() (the standard dictionary's KEY/?TERMINAL, wired
|
||
* through shim.c's getchar()) and sk_console_readline() (QUERY/EXPECT, wired
|
||
* through shim.c's fgets()) alike. */
|
||
static uint64_t g_kbd_poll_tick;
|
||
|
||
static int sk_console_getc_raw(void)
|
||
{
|
||
/* A USB keyboard's reports come in on the xHCI event ring: while one is
|
||
* running, service the ring once per timer tick here, not only on the
|
||
* once-a-second idle beat -- keys arrive at typing speed (A3). */
|
||
if (xhci_keyboard_running()) {
|
||
uint64_t t = heartbeat_ticks();
|
||
if (t != g_kbd_poll_tick) {
|
||
g_kbd_poll_tick = t;
|
||
xhci_poll_events();
|
||
}
|
||
}
|
||
int c = console_getc();
|
||
if (c < 0) c = sk_kbd_getc(); /* FABRIC-0.md 4.4v: second source, same buffer */
|
||
return c;
|
||
}
|
||
|
||
/* One-byte pushback so sk_console_key_available() can peek without losing
|
||
* the byte -- ?TERMINAL must be non-destructive (a caller checks readiness,
|
||
* then still expects KEY to return that same key). */
|
||
static int g_console_pending_key = -1;
|
||
|
||
/* KEY's real body (shim.c's getchar() calls this): blocks until a key is
|
||
* available, servicing the heartbeat/idle loop while waiting -- same
|
||
* cadence sk_console_readline() already uses below, so a KEY call mid-word
|
||
* never stalls the heartbeat or Hera's own idle dispatch. No echo -- that's
|
||
* the caller's job, same as any standard KEY implementation. */
|
||
int sk_console_getkey(VM *active_vm)
|
||
{
|
||
/* No longer used directly -- the idle branch below re-resolves the
|
||
* live active VM itself (FABRIC-3.md SXIII, 2026-09-10) rather than
|
||
* trusting this parameter, which can go stale mid-block. Kept in the
|
||
* signature: repl.h declares it, other callers still pass one. */
|
||
(void)active_vm;
|
||
for (;;) {
|
||
int c;
|
||
if (g_console_pending_key >= 0) {
|
||
c = g_console_pending_key;
|
||
g_console_pending_key = -1;
|
||
} else {
|
||
c = sk_console_getc_raw();
|
||
}
|
||
if (c >= 0) return c;
|
||
|
||
heartbeat_service();
|
||
uint64_t now = heartbeat_ticks();
|
||
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
|
||
g_last_beat_tick = now;
|
||
/* Same use-after-free class fixed in sk_console_readline()'s
|
||
* idle branch (FABRIC-3.md SXIII, 2026-09-10) -- `active_vm`
|
||
* is a parameter captured once by the caller before this
|
||
* (possibly long) blocking wait for a key began, and can be
|
||
* killed mid-wait. Re-resolve fresh every tick instead. */
|
||
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
|
||
sk_repl_idle(live_active);
|
||
}
|
||
arch_relax();
|
||
}
|
||
}
|
||
|
||
/* sk_repl_headless_wait - see repl.h's own doc comment. Same idle-service
|
||
* shape as sk_console_getkey() above, minus the key-reading entirely: no
|
||
* banner, no prompt, no console_getc()/readline of any kind -- this is
|
||
* exactly the "no console for the running system unless a thumbdrive is
|
||
* present" boundary, decided 2026-09-05. Exits the moment sk_console_
|
||
* identity_present() becomes true -- called both once at boot
|
||
* (kernel_main.c, before the first ever login) and again from inside
|
||
* sk_repl_run()'s own main loop whenever the last attached identity logs
|
||
* out mid-boot (2026-09-06 revision, see sk_console_identity_present()'s
|
||
* own doc comment for why the boot-only version wasn't enough). */
|
||
void sk_repl_headless_wait(VM *mama)
|
||
{
|
||
while (!sk_console_identity_present()) {
|
||
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(mama);
|
||
}
|
||
arch_relax();
|
||
}
|
||
}
|
||
|
||
/* ?TERMINAL's real body (sf_terminal_ready(), shim.c): non-blocking peek --
|
||
* a single poll, no idle-servicing loop (a false result must return
|
||
* immediately, not block). Buffers a found byte in g_console_pending_key so
|
||
* a following sk_console_getkey() returns the exact same key, not a
|
||
* different/later one. */
|
||
int sk_console_key_available(void)
|
||
{
|
||
if (g_console_pending_key >= 0) return 1;
|
||
int c = sk_console_getc_raw();
|
||
if (c >= 0) { g_console_pending_key = c; return 1; }
|
||
return 0;
|
||
}
|
||
|
||
/*===========================================================================
|
||
* sk_console_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'), or
|
||
* -1 (2026-09-06) when called with reanchor_prompt nonzero and the
|
||
* identity that was attached when the caller's prompt was printed logs
|
||
* out while this call is still blocked waiting for input with nothing yet
|
||
* typed (n == 0) -- callers with reanchor_prompt nonzero (the REPL's own
|
||
* top-level prompt sites) must check for this and route back to
|
||
* sk_repl_headless_wait() rather than treating it as an empty line; buf
|
||
* is left as an empty string in this case too, matching a real empty
|
||
* line, so a caller that doesn't check the return value degrades to the
|
||
* pre-fix behavior (an extra harmless " ok") rather than misbehaving.
|
||
* shim.c's fgets() (reanchor_prompt == 0) never receives -1.
|
||
*
|
||
* Public (declared in repl.h): shim.c's fgets()/QUERY's own real body call
|
||
* this directly -- same line-editing behavior for a mid-word EXPECT/QUERY as
|
||
* for the REPL's own top-level prompt, since it's the same underlying
|
||
* console. Any g_console_pending_key left over from a ?TERMINAL peek is
|
||
* consumed first so a line read never drops a byte ?TERMINAL already saw.
|
||
* @param reanchor_prompt nonzero from the REPL's own prompt sites (which
|
||
* print SK_PROMPT_TEXT immediately before): re-print the prompt whenever an
|
||
* idle bottom half wrote to the console while this call blocked at the bare
|
||
* prompt (see the re-anchor block in the idle branch). shim.c's fgets()
|
||
* passes 0 -- its prompt context is caller-owned.
|
||
*===========================================================================*/
|
||
|
||
int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
|
||
{
|
||
/* No longer used directly -- see sk_console_getkey()'s matching comment;
|
||
* the idle branch below re-resolves the live active VM itself instead
|
||
* of trusting this parameter across a potentially long block. Kept in
|
||
* the signature: repl.h declares it, other callers still pass one. */
|
||
(void)active_vm;
|
||
int n = 0;
|
||
/* TX counter value right after the caller printed its prompt. Any
|
||
* console output that lands while this readline blocks (heartbeat
|
||
* status, sk_repl_idle()'s USB attach/detach chatter) pushes the
|
||
* counter past this mark and away from a bare prompt; when that
|
||
* happens, re-anchor the prompt (below). */
|
||
uint64_t prompt_tx_mark = console_tx_count();
|
||
|
||
buf[0] = '\0';
|
||
sk_cursor_show(); /* show the cursor at the bare prompt, before any input */
|
||
|
||
for (;;) {
|
||
int c;
|
||
if (g_console_pending_key >= 0) {
|
||
c = g_console_pending_key;
|
||
g_console_pending_key = -1;
|
||
} else {
|
||
c = sk_console_getc_raw();
|
||
}
|
||
|
||
if (c < 0) {
|
||
/* Service the heartbeat bottom half every idle iteration, not
|
||
* gated by SK_IDLE_BEAT_INTERVAL (item 0.8, FABRIC-0.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();
|
||
/* n == 0 gate: sk_repl_idle() opens with console_ensure_line_start(),
|
||
* closing off a dangling prompt line before any chatter it might
|
||
* print (xhci attach/detach, block-sync, MSG-TICK pump). Before the
|
||
* FABRIC-2.md §I.9 fix (2026-09-05), that newline was unconditional
|
||
* and immediate, so it fired on every elapsed SK_IDLE_BEAT_INTERVAL
|
||
* regardless of whether sk_repl_idle() actually had anything to
|
||
* print -- including mid-edit (n > 0, characters typed but Enter
|
||
* not yet pressed), visually snapping the in-progress line to a
|
||
* fresh blank one, indistinguishable from Enter having been
|
||
* pressed. console_ensure_line_start()'s newline is now deferred
|
||
* and self-cancelling when nothing follows it (console.c), which
|
||
* fixes that regardless of n -- but this gate is kept for its own,
|
||
* independent reason: deferring the *whole* idle beat while a line
|
||
* is being edited (same n > 0 guard the prompt reanchor below
|
||
* already uses) means a genuine xhci/block-sync/MSG-TICK event
|
||
* cannot interrupt output mid-line while the user is actively
|
||
* typing, only delaying that servicing by at most one more
|
||
* interval, which its own "coarse cadence, cheap early-exit"
|
||
* design already tolerates. */
|
||
if (n == 0 && now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
|
||
g_last_beat_tick = now;
|
||
/* Found live 2026-09-10 (FABRIC-3.md SXIII): `active_vm` is
|
||
* this call's parameter, captured once by the caller before
|
||
* this (possibly very long) block began -- see the matching
|
||
* comment at this function's dispatch-side fix, below, for
|
||
* the full mechanism. That fix re-resolves `active` fresh
|
||
* right before dispatch, but every idle tick serviced
|
||
* *during* this same blocked call used to pass the stale
|
||
* parameter straight into sk_repl_idle() -> blk_vm_flush_all(),
|
||
* which reads and writes vm->blk_vm_lbn[]/cbuf[]/dirty[]/
|
||
* epoch on whatever `active_vm` points at. If that VM was
|
||
* killed (WIREBIND detach) while this call sat idle, those
|
||
* fields live in a kmalloc block already back on the free
|
||
* list -- and blk_vm_check_epoch()'s unconditional field
|
||
* writes silently corrupt that block's own free-list
|
||
* metadata (next/size/free), observed live as free_blocks/
|
||
* largest_free collapsing to 0 a tick or two after a third
|
||
* WIREBIND identity attached following two prior attach/
|
||
* detach cycles. Re-resolve fresh from the global here too,
|
||
* same pattern as the dispatch-side fix -- sk_get_mama_vm()
|
||
* is the safe fallback (never freed) matching sk_repl_run()'s
|
||
* own `g_repl_active_vm ? g_repl_active_vm : vm` shape. */
|
||
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
|
||
sk_repl_idle(live_active);
|
||
}
|
||
|
||
/* Blink the cursor while idle (no key ready this iteration),
|
||
* regardless of n -- a real terminal blinks whether sitting at
|
||
* a bare prompt or paused mid-edit. sk_cursor_show() (called
|
||
* from every deterministic draw site below and at entry) resets
|
||
* this cycle to "on" on every real keystroke, so typing never
|
||
* looks like it landed mid-blink. */
|
||
if (now - g_cursor_blink_tick >= SK_CURSOR_BLINK_INTERVAL) {
|
||
g_cursor_blink_tick = now;
|
||
g_cursor_visible = !g_cursor_visible;
|
||
if (g_cursor_visible) console_fb_draw_cursor();
|
||
else console_fb_erase_cursor();
|
||
}
|
||
|
||
/*
|
||
* Re-anchor the prompt (FABRIC-0.md 4.4a unified prompt: print
|
||
* only "ok> " here -- console_putc() auto-prefixes the current
|
||
* [VMName] on a fresh line). When an idle bottom half above
|
||
* pushed output past prompt_tx_mark, the console cursor is now
|
||
* below/after new lines and the "ok> " the caller printed has
|
||
* been scrolled or buried -- once the flood passes, the screen
|
||
* and serial log would end on a stale line with no prompt
|
||
* (FABRIC-2.md: the bare prompt must be the last thing shown
|
||
* while the REPL sits idle). Reprinting it restores that
|
||
* invariant. Skipped while a line is being edited (n > 0) so
|
||
* partial echo stays attached to its own prompt; shim.c's
|
||
* fgets() (QUERY/EXPECT/ACCEPT) calls in with reanchor_prompt
|
||
* == 0 for the same reason -- its prompt line is caller-owned
|
||
* text, not the REPL's. Each silent beat leaves the mark
|
||
* unchanged, so the final state after the chatter dies down is
|
||
* a fresh prompt on the last visible line, cursor on it.
|
||
*/
|
||
/* Headless-until-login gate, 2026-09-06: the identity that was
|
||
* attached when the caller printed its prompt (sk_print_prompt(),
|
||
* reflected in reanchor_prompt callers only -- shim.c's fgets()
|
||
* passes 0 and is unaffected) may have logged out while we sat
|
||
* here blocked waiting for input -- WIREBIND EJECT/unclean
|
||
* detach, or Zuse's own logout, both reachable from
|
||
* sk_repl_idle() just above. Re-printing the prompt in that
|
||
* case (the block below) would just show a *correct* bare
|
||
* "ok>" -- true to current state, but still an unauthenticated
|
||
* interactive surface sitting on screen, which the headless-
|
||
* until-login design (Kconfig.heartbeat's EMERGENCY_CONSOLE_
|
||
* ENABLED) exists specifically to prevent. Bail out instead so
|
||
* the caller (sk_repl_run()'s own main loop) can drop back into
|
||
* sk_repl_headless_wait() -- confirmed live as a real gap
|
||
* before this fix (a bare, unauthenticated prompt stayed on
|
||
* screen after every logout for the rest of the boot). n == 0
|
||
* only: never abandon a line the user is actively typing. */
|
||
#if !EMERGENCY_CONSOLE_ENABLED
|
||
if (reanchor_prompt && n == 0 && !sk_console_identity_present()) {
|
||
return -1;
|
||
}
|
||
#endif
|
||
/* Not in the bring-up build (EMERGENCY_CONSOLE_ENABLED): its prompt
|
||
* needs no login, and sk_repl_run() takes this return as an empty
|
||
* line there -- with no identity attached the prompt answered " ok"
|
||
* and asked again without end, 1.4 million times in 200 seconds
|
||
* (found 2026-10-13 rehearsing boards/BRINGUP.md; logs/20261013-210442). */
|
||
|
||
if (reanchor_prompt && n == 0 &&
|
||
console_tx_count() != prompt_tx_mark)
|
||
{
|
||
sk_print_prompt();
|
||
sk_cursor_show();
|
||
prompt_tx_mark = console_tx_count();
|
||
}
|
||
|
||
/*
|
||
* 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');
|
||
/* CRLF pairing, found live 2026-09-22: a terminator sent as
|
||
* both bytes (a real terminal in CRLF mode, or any scripted
|
||
* sender writing "\r\n") used to submit TWICE -- this line
|
||
* on the '\r' (or '\n'), then an immediate empty line on the
|
||
* OTHER byte the next time this function is called, each
|
||
* producing its own " ok" -- confirmed live as the doubled
|
||
* "ok" this project's own session interaction showed, not an
|
||
* async-relay artifact as first suspected. Non-blocking peek
|
||
* for the paired byte right here, before returning -- discard
|
||
* it if present, push it back via g_console_pending_key
|
||
* (already the mechanism sk_console_key_available() uses for
|
||
* exactly this "peeked but not this call's to consume" case)
|
||
* if it's unrelated input for the NEXT line. A byte that
|
||
* hasn't arrived yet by this point is not waited for --
|
||
* matches every other non-blocking read in this function. */
|
||
{
|
||
int pair = (c == '\r') ? '\n' : '\r';
|
||
int next = sk_console_getc_raw();
|
||
if (next >= 0 && next != pair) g_console_pending_key = next;
|
||
}
|
||
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');
|
||
sk_cursor_show();
|
||
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 */
|
||
sk_cursor_show();
|
||
}
|
||
|
||
buf[n] = '\0';
|
||
return n;
|
||
}
|
||
|
||
/*===========================================================================
|
||
* sk_repl - FORTH REPL
|
||
*
|
||
* FABRIC-2.md §F.20/§F.21 (2026-08-28): the unauthenticated emergency-CLI
|
||
* ACL bypass this REPL used to grant itself on Hera's own bare prompt is
|
||
* retired -- every word runs under ordinary ACL enforcement here now,
|
||
* console identity included. emergency_console still exists as a field
|
||
* (vm.h) and is still set, briefly, by the genuine C-level VM fault
|
||
* handler (EMERGENCY_CONSOLE_ENABLED build flag) for crash recovery --
|
||
* that's a distinct, narrower mechanism this REPL no longer touches.
|
||
*
|
||
* Mirrors vm_repl() from src/repl.c:
|
||
* - Reads a line via sk_console_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: an interactive REPL-turn fault on
|
||
* any VM (Hera included, as of FABRIC-3.md §XXXII.1) recovers the same
|
||
* way -- prints a message, clears vm->error/halted/abort_requested, and
|
||
* the loop continues at that VM's own next prompt. Boot-time faults
|
||
* (capsule load, birth scripts) never reach this code at all; they are
|
||
* caught and cleared directly in kernel_main.c before sk_repl_run() is
|
||
* ever entered, so no "no fallthrough surface" halt is needed or given
|
||
* up here -- the C-level `sk_fault_handler()` this comment used to
|
||
* describe was retired the same day, having no remaining caller.
|
||
*===========================================================================*/
|
||
|
||
/*===========================================================================
|
||
* sk_repl_dispatch_line - console-VM + user-VM pair relay (FABRIC-2.md
|
||
* Phase F, 2026-08-28).
|
||
*
|
||
* If `vm`'s own registered name has a live "<name>~user" counterpart,
|
||
* this is a console session: relay the raw line as a real, async
|
||
* CONSOLE-CMD-EVENT message (common:messaging.4th) instead of
|
||
* interpreting it directly -- "every line is a message," not a
|
||
* C-level redirect. This is one particular consumer of the general
|
||
* VM-to-VM messaging system built in Phase C: any VM can already
|
||
* MSG-SEND to any other VM for its own reasons regardless of a human
|
||
* ever being at a physical console at all; this hook only wires the
|
||
* physical-terminal-input path into that same general mechanism, it
|
||
* doesn't gate or replace it.
|
||
*
|
||
* Falls back to direct vm_interpret() (today's unchanged behavior) when
|
||
* there's no live paired user VM, or when the line contains a `"`
|
||
* character this simple S"-embedding can't safely carry yet (a known
|
||
* v1 limitation -- warned about, not silently mishandled).
|
||
*===========================================================================*/
|
||
|
||
/* USE (FABRIC-2.md §F.24) is a REPL-control word, not a command for
|
||
* whatever VM happens to be paired to a console -- it must always run
|
||
* on the active VM directly, never get relayed as a message. Real
|
||
* FORTH syntax always puts USE last (S" name" USE), so a trailing-
|
||
* token match is a reliable, non-tokenizing-required check: trim
|
||
* trailing whitespace, then confirm the line ends with "USE" as its
|
||
* own word (preceded by whitespace or the whole line). */
|
||
static int sk_repl_line_calls_use(const char *input)
|
||
{
|
||
size_t len = strlen(input);
|
||
while (len > 0 && (input[len - 1] == ' ' || input[len - 1] == '\t')) len--;
|
||
if (len < 3) return 0;
|
||
if (input[len - 3] != 'U' || input[len - 2] != 'S' || input[len - 1] != 'E') return 0;
|
||
return (len == 3) || (input[len - 4] == ' ' || input[len - 4] == '\t');
|
||
}
|
||
|
||
/* FABRIC-3.6.md task 3.9. Sized to the largest single payload
|
||
* kernel-Hermes will ever accept (SK_HERMES_CHUNK_MAX_PAYLOAD, task
|
||
* 3.5's one-block bound) -- console lines up to INPUT_BUFFER_SIZE-1
|
||
* (1024) content bytes are silently capped to SK_HERMES_CHUNK_MAX_
|
||
* PAYLOAD-1 (1023) to leave room for the NUL terminator
|
||
* sk_hermes_drain_checkpoint() requires; chunking a console line across
|
||
* multiple messages is out of this task's scope (task 3.5 built the
|
||
* primitives, not a sender for this use) and the cap is not expected to
|
||
* be reached by ordinary interactive typing.
|
||
*
|
||
* CORRECTED 2026-09-22 (real defect, found while starting this task,
|
||
* fixed at the source): this comment originally justified `static, not
|
||
* stack-local` on the claim that sk_hermes_send_one() stored
|
||
* payload_addr out-of-line and the caller had to keep it alive until
|
||
* drained -- which was true, and which g_kh_blk_attach_buf (task 3.8)
|
||
* turned out to make into a genuine payload-aliasing defect (two sends
|
||
* before either drains, whichever wrote last silently winning). Fixed
|
||
* at the source (kernel_hermes.c's sk_hermes_send_one() now copies into
|
||
* the message's own storage immediately, kernel_hermes.h's own doc
|
||
* comment on SkHermesMessage has the full account) -- this buffer no
|
||
* longer needs to survive past the send call, for this or any future
|
||
* console-proxy session (console.c's own singleton today, per memory
|
||
* project_multiseat_console_idea). Staying `static` is now just a
|
||
* convenience (avoids a 1024-byte stack frame), not a correctness
|
||
* requirement. */
|
||
static char g_kh_console_cmd_buf[SK_HERMES_CHUNK_MAX_PAYLOAD];
|
||
|
||
static void sk_repl_dispatch_line(VM *vm, const char *input)
|
||
{
|
||
/* H1 reentrancy guard: while this dispatched line executes on Hera
|
||
* herself, sk_repl_idle() must defer its MSG-TICK pump -- calling
|
||
* vm_interpret(mama, ...) from inside a mid-line KEY/EXPECT would
|
||
* re-enter mama's interpreter and clobber its in-flight input buffer
|
||
* (see the guard's comment at sk_repl_idle()). A child VM's console
|
||
* turn leaves mama idle, so the pump stays safe there and the guard is
|
||
* only latched for Hera. */
|
||
int on_mama = (vm == (VM *)sk_get_mama_vm());
|
||
int saved = g_mama_interpreting;
|
||
if (on_mama) g_mama_interpreting = 1;
|
||
|
||
if (sk_repl_line_calls_use(input)) {
|
||
vm_interpret(vm, input);
|
||
goto out;
|
||
}
|
||
const char *vn = console_get_vm_name();
|
||
if (vn) {
|
||
char paired_name[VM_NAME_MAX + 8];
|
||
size_t vnlen = strlen(vn);
|
||
if (vnlen + 6 <= sizeof(paired_name)) {
|
||
memcpy(paired_name, vn, vnlen);
|
||
memcpy(paired_name + vnlen, "~user", 6); /* includes NUL */
|
||
|
||
VMRegistryEntry paired;
|
||
if (capsule_vm_find_by_name(paired_name, &paired) == 0 &&
|
||
paired.state == VM_STATE_LIVE) {
|
||
|
||
if (strchr(input, '"')) {
|
||
console_println("console: line contains '\"' -- can't relay "
|
||
"as a message safely yet, interpreting directly");
|
||
} else {
|
||
/* FABRIC-3.6.md task 3.9 (Stage D, SXXXIV.2/.3):
|
||
* CONSOLE-CMD-EVENT's real cutover -- the FORTH
|
||
* "CONSOLE-CMD-EVENT 0 3 S\" ...\" 0 MSG-SEND" string
|
||
* interpret is gone; this now calls kernel-Hermes
|
||
* directly. `paired.vm_id` (just resolved above) is
|
||
* the real target VMUuid -- no name-index translation
|
||
* needed, unlike the FORTH convention's local "index
|
||
* 3" hack. `from` is the console-proxy VM's own real
|
||
* identity (`vm->stadium_vm_id`), not the FORTH
|
||
* convention's hardcoded "0" (which read as Hera in
|
||
* every console-proxy's shared VM-NAMES-INIT table,
|
||
* regardless of which console actually sent it) --
|
||
* more correct provenance, a deliberate refinement
|
||
* task 3.9 makes possible, not a silent behavior
|
||
* change: the receiving side never read `from` for
|
||
* anything but bookkeeping. The `"` guard above is
|
||
* kept even though kernel-Hermes's payload is a raw
|
||
* pointer+length, not a FORTH string literal, and no
|
||
* longer needs it -- removing a documented v1
|
||
* limitation is its own decision, not a side effect
|
||
* of this one (per advisor() review). */
|
||
size_t ilen = strlen(input);
|
||
if (ilen >= SK_HERMES_CHUNK_MAX_PAYLOAD)
|
||
ilen = SK_HERMES_CHUNK_MAX_PAYLOAD - 1; /* leave room for the NUL --
|
||
* see g_kh_console_cmd_buf's
|
||
* own doc comment */
|
||
memcpy(g_kh_console_cmd_buf, input, ilen);
|
||
g_kh_console_cmd_buf[ilen] = '\0';
|
||
if (sk_hermes_send_one(vm->stadium_vm_id, paired.vm_id,
|
||
SK_HERMES_MSG_TYPE_CONSOLE_CMD, 0,
|
||
g_kh_console_cmd_buf, (uint32_t)(ilen + 1)) != 0) {
|
||
log_message(LOG_ERROR, "console: kernel-Hermes refused CONSOLE-CMD-EVENT send");
|
||
}
|
||
goto out;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
vm_interpret(vm, input);
|
||
|
||
out:
|
||
g_mama_interpreting = saved;
|
||
}
|
||
|
||
/*===========================================================================
|
||
* 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-0.md 4.4w: matches the strip's input width */
|
||
|
||
if (!vm || vm->halted) return 0;
|
||
|
||
{
|
||
/* Unified prompt (FABRIC-0.md 4.4a): console_putc()'s existing per-line
|
||
* "[user@VMName] " prefix (console.c's emit_prefix(), FABRIC-3.md
|
||
* SXXV follow-up) already supplies the bracket -- print only
|
||
* "ok> " here, don't build a second one. emergency_console is no
|
||
* longer set from here (FABRIC-2.md §F.20/§F.21: the emergency-CLI
|
||
* ACL bypass is retired) -- it's driven only by the genuine C-level
|
||
* fault handler now (vm.c's own emergency-fault-recovery use,
|
||
* EMERGENCY_CONSOLE_ENABLED). Every word run from this REPL,
|
||
* Hera's bare prompt included, goes through ordinary ACL
|
||
* enforcement. */
|
||
sk_print_prompt();
|
||
}
|
||
|
||
sk_console_readline(input, sizeof(input), vm, 1);
|
||
|
||
if (input[0] == '\0') {
|
||
console_puts(" ok\n");
|
||
return vm->halted ? 0 : 1;
|
||
}
|
||
|
||
sk_repl_dispatch_line(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) {
|
||
#if EMERGENCY_CONSOLE_ENABLED
|
||
console_puts(" ERROR\n");
|
||
vm->error = 0;
|
||
#else
|
||
/* FABRIC-3.md §XXXII.1, 2026-09-15: an ordinary interactive
|
||
* mistake (a typo, an unrecognized word, a stack-convention
|
||
* violation) at Zuse's own console used to hard-halt the entire
|
||
* kernel here, because this branch treated Hera's own session
|
||
* as a "no fallthrough surface" emergency and every other VM's
|
||
* session as recoverable (2026-09-09 fix, see git history). That
|
||
* asymmetry was never actually protecting a boot-time fault --
|
||
* boot/capsule-load errors are caught and cleared entirely
|
||
* separately, in kernel_main.c, well before sk_repl_run() is
|
||
* ever entered -- so by the time this function runs at all, any
|
||
* vm->error here is by construction an interactive REPL-turn
|
||
* fault, on any VM including Hera. Recovers unconditionally now,
|
||
* matching every other VM's own session exactly. */
|
||
console_println("VM fault -- session recovered, resuming");
|
||
vm->error = 0;
|
||
vm->halted = 0;
|
||
vm->abort_requested = 0;
|
||
#endif
|
||
} else {
|
||
console_puts(" ok\n");
|
||
}
|
||
|
||
return vm->halted ? 0 : 1;
|
||
}
|
||
|
||
void sk_repl_run(VM *vm)
|
||
{
|
||
char input[INPUT_BUFFER_SIZE]; /* FABRIC-0.md 4.4w: matches the strip's input width */
|
||
VM *active;
|
||
|
||
/* FABRIC-3.md SXXV follow-up: one-time registration so console.c's
|
||
* emit_prefix() can compose "[user@VMName] " instead of the bare
|
||
* "[VMName] " it falls back to while this is still unregistered
|
||
* (early boot output, before sk_repl_run() is ever reached). */
|
||
console_set_user_prefix_provider(sk_console_user_prefix);
|
||
|
||
vm->halted = 0;
|
||
|
||
while (!vm->halted) {
|
||
#if !EMERGENCY_CONSOLE_ENABLED
|
||
/* Headless-until-login gate, revised 2026-09-06: re-checked every
|
||
* iteration, not just once before this loop starts (kernel_main.c's
|
||
* own sk_repl_headless_wait() call, still in place, only covers the
|
||
* very first login of the boot). Whoever was attached may have
|
||
* logged out since the last iteration (WIREBIND EJECT/unclean
|
||
* detach, Zuse's own logout) -- if nobody is attached right now,
|
||
* go back to silent waiting instead of falling through to a bare,
|
||
* unauthenticated prompt. See sk_console_identity_present()'s own
|
||
* doc comment for the live bug this closes. */
|
||
if (!sk_console_identity_present()) {
|
||
sk_repl_headless_wait(vm);
|
||
if (vm->halted) break;
|
||
continue;
|
||
}
|
||
#endif
|
||
/* USE may redirect input to a different VM each iteration */
|
||
active = g_repl_active_vm ? g_repl_active_vm : vm;
|
||
|
||
/* Unified prompt (FABRIC-0.md 4.4a): console_putc()'s existing per-line
|
||
* "[user@VMName] " prefix (console.c's emit_prefix(), FABRIC-3.md
|
||
* SXXV follow-up) already supplies the bracket -- print only "ok> "
|
||
* here, don't build a second one. emergency_console is no longer
|
||
* set from here (FABRIC-2.md §F.20/§F.21: the emergency-CLI ACL
|
||
* bypass is retired) -- see sk_repl_step()'s matching comment
|
||
* above. */
|
||
sk_print_prompt();
|
||
|
||
int n = sk_console_readline(input, sizeof(input), active, 1);
|
||
#if EMERGENCY_CONSOLE_ENABLED
|
||
/* n only consumed below under !EMERGENCY_CONSOLE_ENABLED (the
|
||
* logged-out-mid-read bailout doesn't apply when the emergency
|
||
* console bypasses login entirely) -- silence -Wunused-variable
|
||
* rather than drop the assignment (sk_console_readline()'s return
|
||
* value is still meaningful, just not acted on in this build). */
|
||
(void)n;
|
||
#endif
|
||
|
||
#if !EMERGENCY_CONSOLE_ENABLED
|
||
/* n < 0: sk_console_readline() bailed out because the identity
|
||
* that was attached when this prompt was printed logged out
|
||
* while we were still blocked waiting for input (2026-09-06 --
|
||
* see sk_console_readline()'s own doc comment on this return
|
||
* value). No " ok" here -- nothing was typed, nothing ran --
|
||
* just loop back to the top, where the check above re-enters
|
||
* headless silence immediately instead of showing yet another
|
||
* prompt first. */
|
||
if (n < 0) {
|
||
continue;
|
||
}
|
||
#endif
|
||
|
||
if (input[0] == '\0') {
|
||
console_puts(" ok\n");
|
||
continue;
|
||
}
|
||
|
||
/* Found live 2026-09-10: `active` was captured once, above, before
|
||
* sk_console_readline() blocked for this line -- but that call can
|
||
* block for an arbitrarily long time, during which the VM `active`
|
||
* points at can be killed (WIREBIND detach) and its memory freed.
|
||
* The n<0 bailout above is supposed to catch a logout mid-read, but
|
||
* it only fires on sk_console_identity_present() -- a generic "is
|
||
* ANYONE attached" boolean, not "is the specific identity `active`
|
||
* belonged to still attached" -- so a fast detach-then-reattach of
|
||
* a *different* identity while this call was blocked (n still 0)
|
||
* never trips it: presence reads true throughout, no gap is ever
|
||
* observed. The stale `active` then gets dispatched into freed
|
||
* memory. Confirmed live via targeted probes: g_repl_active_vm is
|
||
* correctly reset to NULL by the kill/teardown path the moment it
|
||
* happens, but this loop iteration's *local* `active` was already
|
||
* snapshotted and never re-read. Re-resolve fresh from the global
|
||
* right before dispatch -- cheap, and closes the race regardless
|
||
* of whether the bailout above catches it first. */
|
||
active = g_repl_active_vm ? g_repl_active_vm : vm;
|
||
|
||
sk_repl_dispatch_line(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) {
|
||
#if EMERGENCY_CONSOLE_ENABLED
|
||
console_puts(" ERROR\n");
|
||
active->error = 0;
|
||
#else
|
||
/* FABRIC-3.md §XI.4 (2026-09-09) scoped this recovery to any
|
||
* *redirected* (WIREBIND/USE'd) identity, keeping Hera's own
|
||
* direct session (active == vm) on the strict "no fallthrough
|
||
* surface" halt -- reasoned at the time as protecting against
|
||
* a genuine full-system emergency. §XXXII.1 (2026-09-15) found
|
||
* that reasoning didn't hold: boot/capsule-load errors are
|
||
* caught and cleared entirely separately in kernel_main.c,
|
||
* before this loop is ever entered, so any active->error seen
|
||
* here -- Hera's own session included -- is by construction
|
||
* an ordinary interactive REPL-turn fault (a typo, an
|
||
* unrecognized word), not a boot-time catastrophe. An
|
||
* unqualified typo at Zuse's own console was hard-halting the
|
||
* entire kernel as a direct result of this asymmetry. Recovers
|
||
* unconditionally now, matching every redirected identity's
|
||
* own session exactly -- no special case for `active == vm`. */
|
||
console_println("VM fault -- session recovered, resuming");
|
||
active->error = 0;
|
||
active->halted = 0;
|
||
active->abort_requested = 0;
|
||
#endif
|
||
} else {
|
||
console_puts(" ok\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
void sk_repl(VM *vm)
|
||
{
|
||
/* FABRIC-0.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 CLI");
|
||
console_println("FORTH-79 interpreter type BYE or power off to exit");
|
||
console_println("");
|
||
|
||
sk_repl_run(vm);
|
||
}
|