capsule_runcap_birth() (new capsule_runcap.h/.c): builds a heap-only, single-entry CapsuleDirHeader + CapsuleDesc + CapsuleNameEntry + arena from a home-blocks drive's identity_src region (skipping the first devblock, reserved for MINT's user_identity_seed_t record) and hands it to the existing, unmodified capsule_birth_baby() -- no new birth mechanism, matching FABRIC-3.md §F.6's own trace. Found and closed a real gap in that trace along the way: capsule_birth_baby()'s signature check calls capsule_get_signatures(), which unconditionally returns the compile-time-baked global array -- meaningless for a heap-built directory, where index 0 would compare RUNCAP's own content against whatever real capsule happens to occupy that slot in the baked array (guaranteed-wrong, not a security check). Added an explicit skip_pki_sig flag (0 for all 4 existing call sites, 1 for RUNCAP): that content's trust comes from CERTVERIFY, a separate root, not the capsule-PKI chain. Also found live: capsule_birth_baby() never sets the registry entry's own .name (every existing caller does this itself afterward via capsule_vm_registry_set_name() -- RUNCAP now does too), and capsule_exec_payload() requires a "Block NNNN" header per chunk of content or it's silently skipped, never executed -- not a bug, but necessary context for whoever authors MINT's default personality content next. Added a small accessor pair (repl.h/.c) exposing the currently attached home-blocks device/sig -- the same gap F.9's own BINDSTEP scoping had already flagged, needed by both. Verified end-to-end live in QEMU: synthetic identity-source content written directly to a thumbdrive image's raw devblocks (no capsule build, no mkcapsule) was read, compiled, and executed by a genuinely new VM via a diagnostic RUNCAP-TEST word -- confirmed via VM-EXEC invoking a word defined only in that source. Clean 3-architecture regression boot (no RUNCAP drive attached) confirms no side effects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
571 lines
25 KiB
C
571 lines
25 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.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 "starkernel/homeblocks_sig.h"
|
||
#include "starkernel/capsule_birth.h"
|
||
#include "starkernel/capsule_run.h"
|
||
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.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; }
|
||
|
||
/*===========================================================================
|
||
* Currently attached home-blocks device: mirrors g_repl_active_vm's own
|
||
* shape (FABRIC-3.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;
|
||
}
|
||
|
||
/*===========================================================================
|
||
* 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) {
|
||
/* FABRIC-3.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-3.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:
|
||
console_println("xhci: USB drive recognized as a home-blocks drive");
|
||
g_homeblocks_dev = &usb_blk_dev;
|
||
g_homeblocks_sig = sig;
|
||
g_homeblocks_sig_valid = 1;
|
||
break;
|
||
case HOMEBLOCKS_SIG_BLANK:
|
||
console_println("xhci: USB drive not recognized (blank or foreign media) -- read-only general use only");
|
||
break;
|
||
case HOMEBLOCKS_SIG_BAD_VERSION:
|
||
console_println("xhci: USB drive has a home-blocks header of an unrecognized version -- read-only general use only");
|
||
break;
|
||
case HOMEBLOCKS_SIG_BAD_CRC:
|
||
console_println("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:
|
||
console_println("xhci: USB drive signature check failed to read the device -- read-only general use only");
|
||
break;
|
||
}
|
||
}
|
||
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);
|
||
if (g_homeblocks_dev == &usb_blk_dev) {
|
||
g_homeblocks_dev = (void *)0;
|
||
g_homeblocks_sig_valid = 0;
|
||
}
|
||
}
|
||
|
||
/* 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-3.md Phase C (2026-08-28): distributed messaging pump. Every
|
||
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
|
||
* MSG-TICK word (see capsules/common/messaging.4th) instead of only
|
||
* Hermes having one -- "fully distributed, Hera pumps each VM's
|
||
* drain" was the confirmed design. Hera is excluded: she never loads
|
||
* common:messaging.4th (kernel_main.c's own comment at the Hermes-
|
||
* birth call site explains why -- the STADIUM-* primitives it needs
|
||
* are deliberately never registered in her own dictionary, to keep
|
||
* her dict_hash off item 4.1's baseline), so MSG-TICK is genuinely
|
||
* absent there, not just untried. She is the only VM with a
|
||
* persistent idle tick, so she walks the registry once per idle beat
|
||
* and VM-EXECs MSG-TICK into every OTHER live VM's own dictionary. */
|
||
{
|
||
VM *mama = (VM *)sk_get_mama_vm();
|
||
uint32_t count = capsule_vm_registry_count();
|
||
uint32_t i;
|
||
for (i = 0; i < count; i++) {
|
||
VMRegistryEntry ent;
|
||
if (capsule_vm_registry_get_by_index(i, &ent) != 0) continue;
|
||
if (ent.state != VM_STATE_LIVE) continue;
|
||
if (ent.vm_ptr == (void *)mama) continue;
|
||
|
||
static const char PFX[] = "S\" MSG-TICK\" S\" ";
|
||
static const char SFX[] = "\" VM-EXEC";
|
||
char cmd[128];
|
||
size_t p = 0;
|
||
size_t name_len = strlen(ent.name);
|
||
if (name_len > VM_NAME_MAX - 1u) name_len = VM_NAME_MAX - 1u;
|
||
|
||
memcpy(cmd + p, PFX, sizeof(PFX) - 1u); p += sizeof(PFX) - 1u;
|
||
memcpy(cmd + p, ent.name, name_len); p += name_len;
|
||
memcpy(cmd + p, SFX, sizeof(SFX) - 1u); p += sizeof(SFX) - 1u;
|
||
cmd[p] = '\0';
|
||
|
||
vm_interpret(mama, cmd);
|
||
}
|
||
}
|
||
}
|
||
|
||
/*===========================================================================
|
||
* 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);
|
||
}
|