Files
StarForth/src/starkernel/repl.c
T
Robert Allan JamesandClaude Sonnet 5 91742e02f4 FABRIC.md item 4.4x: split the REPL prompt into its own bottom strip
Scope expanded from pure CANVAS-rectangle arithmetic (as originally
scoped) to also splitting the REPL prompt/input line out of the
scrollback box into an independent single-line strip, per Captain Bob's
explicit fold-in after the gap was reported (§25.0 rule 3) rather than
silently expanded.

vt100.c: VT100_BOX_ORIGIN_X/Y are no longer hardcoded per-arch literals --
both are now derived from fb_width()/fb_height() at vt100_enable_ttf()
time. New vt100_strip_draw() renders the bottom strip (gray border lines,
bright-white text) directly via the existing ttf_draw_glyph_cell()
rasterizer, independent of the box's own grid/cursor state. Border lines
are drawn after the glyph loop so an oversized cell can only be clipped
by them, never erase them.

console.c/console.h: console_fb_strip_draw() thin wrapper, matching the
existing console_fb_enable_ttf()/console_fb_scroll_*() pattern.

repl.c: builds a plain-text "[VMName] ok> <input>" mirror in
g_strip_prompt/strip_refresh(), refreshed on every keystroke (including
backspace) from sk_readline() -- already wired for item 4.4v, since
keyboard-typed characters will flow through the same console_getc() path
once that lands. Also widened sk_repl_step()/sk_repl_run()'s local input
buffer from a second, smaller 256-byte buffer to INPUT_BUFFER_SIZE
(1025), per 4.4w's decision.

Verified: three-arch clean QEMU boot + logs, amd64 screendump showing
the box and strip as two visually distinct regions with no visible
glyph/border clipping.

Punch list §25 item 4.4x complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 14:59:07 -04:00

363 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
/**
* repl.c - Emergency FORTH REPL for LithosAnanke kernel
*
* Direct adaptation of src/repl.c for the freestanding kernel context.
* Replaces libc stdio (fgets/printf/fflush) with HAL serial I/O:
* - Input: console_getc() non-blocking poll with local echo and backspace
* - Output: console_puts() / console_putc()
*
* Idle spin: polls console_getc() and services the adaptive heartbeat.
* The timer ISR's top half (heartbeat_tick()) latches one
* sample per interrupt; the idle spin drains it every
* iteration via heartbeat_service() (item 0.8, FABRIC.md §26)
* and calls sk_repl_idle() once per SK_IDLE_BEAT_INTERVAL ticks
* for coarser subsystem dispatch. On QEMU TCG the ISR must fire
* for ticks to advance — check "Heartbeat: N ticks" in the
* serial log to confirm.
*
* Runs with interrupts enabled so the APIC heartbeat fires normally.
* Designed as the last thing kernel_main does before the idle loop.
*/
#include "starkernel/repl.h"
#include "console.h"
#include "vm.h"
#include "version.h"
#include "starkernel/timer.h"
#include "starkernel/arch.h"
#include <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;
/*===========================================================================
* FABRIC.md items 4.4u/4.4w/4.4x: the bottom REPL strip -- a plain-text
* (no ANSI/SGR) mirror of "[VMName] ok> <input so far>", refreshed into
* console_fb_strip_draw() on every keystroke. Built separately from
* SK_PROMPT_TEXT rather than derived from it, since the strip renders in
* one fixed color (4.4u) and has no SGR parser of its own -- the ANSI
* version keeps going to console_puts() exactly as before, unaffected,
* for serial and for the scrollback box's own history.
*===========================================================================*/
#define SK_STRIP_PROMPT_MAX 80u
static char g_strip_prompt[SK_STRIP_PROMPT_MAX] = "ok> ";
/* Builds "[VMName] ok> " or "[VMName] zuse)ok> " (no VM name -> just the
* suffix) into g_strip_prompt, bounded, no strcat -- mirrors emit_prefix()'s
* bracket format (console.c) so the strip and the box agree on what a
* prompt looks like, minus the ANSI color codes the strip doesn't use. */
static void set_strip_prompt(VM *vm)
{
const char *vn = console_get_vm_name();
char *p = g_strip_prompt;
size_t room = sizeof(g_strip_prompt) - 1u;
size_t n;
const char *suffix;
size_t used, left, sn;
if (vn && vn[0]) {
n = strlen(vn);
if (n > room) n = room;
*p++ = '[';
memcpy(p, vn, n);
p += n;
*p++ = ']';
*p++ = ' ';
}
suffix = (vm && vm->zuse_session) ? "zuse)ok> " : "ok> ";
used = (size_t)(p - g_strip_prompt);
left = (used < room) ? (room - used) : 0u;
sn = strlen(suffix);
if (sn > left) sn = left;
memcpy(p, suffix, sn);
p += sn;
*p = '\0';
}
/* Concatenates g_strip_prompt + input_so_far (bounded, no strcat) and
* hands the result to the strip renderer. Called once with an empty
* input_so_far right after set_strip_prompt(), then again on every
* keystroke from sk_readline() below. */
static void strip_refresh(const char *input_so_far)
{
char combined[INPUT_BUFFER_SIZE + SK_STRIP_PROMPT_MAX];
size_t plen = strlen(g_strip_prompt);
size_t ilen = strlen(input_so_far);
if (plen >= sizeof(combined)) plen = sizeof(combined) - 1u;
memcpy(combined, g_strip_prompt, plen);
if (plen + ilen >= sizeof(combined)) ilen = sizeof(combined) - 1u - plen;
memcpy(combined + plen, input_so_far, ilen);
combined[plen + ilen] = '\0';
console_fb_strip_draw(combined);
}
/*===========================================================================
* USE-word dispatch: which VM receives REPL input.
*
* NULL means "use the REPL's own vm parameter" (default — Mama).
* Set via sk_repl_set_active_vm(); read by sk_repl_run() each iteration.
*===========================================================================*/
static VM *g_repl_active_vm = (void *)0;
void sk_repl_set_active_vm(VM *vm) { g_repl_active_vm = vm; }
VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; }
/*===========================================================================
* Idle heartbeat service
*
* Called from sk_readline when heartbeat_ticks() has advanced by at least
* SK_IDLE_BEAT_INTERVAL since the last service call. Extend this function
* as higher-level subsystems (msg_fabric, capsule scheduler) come online.
*
* TODO: cadence policy and subsystem dispatch belong in Compudynamics once
* that layer governs cooperative VM execution.
*===========================================================================*/
#define SK_IDLE_BEAT_INTERVAL 100u /* ticks between idle service calls (1 s at 100 Hz) */
static uint64_t g_last_beat_tick; /* zero-initialized (BSS) */
static void sk_repl_idle(void)
{
/* Placeholder — extended by higher-level subsystems as they come online */
(void)0;
}
/*===========================================================================
* sk_readline - line read from serial console with echo
*
* Non-blocking poll of console_getc(). While no character is ready the idle
* spin services the adaptive heartbeat at SK_IDLE_BEAT_INTERVAL tick cadence.
* Supports backspace (0x7F and \b) and ignores other control characters.
* Returns the number of characters placed in buf (not counting '\0').
*===========================================================================*/
static int sk_readline(char *buf, int size)
{
int n = 0;
buf[0] = '\0';
strip_refresh(buf); /* FABRIC.md 4.4x: show the bare prompt before any input */
for (;;) {
int c = console_getc(); /* non-blocking poll */
if (c < 0) {
/* Service the heartbeat bottom half every idle iteration, not
* gated by SK_IDLE_BEAT_INTERVAL (item 0.8, FABRIC.md §26):
* heartbeat_service() drains at most one latched sample per
* call, so a coarse gate here would silently lose or merge
* samples between ISR-latched ticks. sk_repl_idle() below is
* a separate, deliberately coarser cadence for higher-level
* subsystem dispatch, unrelated to sample fidelity. */
heartbeat_service();
uint64_t now = heartbeat_ticks();
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle();
}
/*
* Do NOT use hlt here: QEMU single-threaded TCG can't process
* its APIC timer callbacks while the guest CPU is halted (the
* event loop and the TCG thread share the same OS thread).
* Interrupts are delivered at TB boundaries in a tight loop.
* On real hardware a wfi/hlt would be appropriate; add it here
* under an #ifdef REAL_HARDWARE guard when that path is needed.
*/
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
continue;
}
if (c == '\r' || c == '\n') {
console_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');
strip_refresh(buf);
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 */
strip_refresh(buf);
}
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);
set_strip_prompt(vm); /* FABRIC.md 4.4x: bottom strip mirrors the same prompt */
}
sk_readline(input, sizeof(input));
if (input[0] == '\0') {
console_puts(" ok\n");
return vm->halted ? 0 : 1;
}
vm_interpret(vm, input);
/* ABORT stops mid-line but leaves the flag set for the caller to
* consume -- this REPL step is that boundary. Clear it here so the
* next line isn't silently refused by vm_interpret's own check. */
vm->abort_requested = 0;
if (vm->error) {
console_puts(" ERROR\n");
vm->error = 0;
} else {
console_puts(" ok\n");
}
return vm->halted ? 0 : 1;
}
void sk_repl_run(VM *vm)
{
char input[INPUT_BUFFER_SIZE]; /* FABRIC.md 4.4w: matches the strip's input width */
VM *active;
vm->halted = 0;
while (!vm->halted) {
/* USE may redirect input to a different VM each iteration */
active = g_repl_active_vm ? g_repl_active_vm : vm;
/* Unified prompt (FABRIC.md 4.4a): console_putc()'s existing per-line
* "[VMName] " prefix (console.c, g_active_vm_name) already supplies the
* bracket -- print only "ok> " here, don't build a second one. The
* emergency_console bypass is a security decision, not a display one --
* it still applies only to Hera's bare prompt, per FABRIC.md 4.4. */
{
const char *vn = console_get_vm_name();
int is_hera = (!vn || (vn[0]=='H' && vn[1]=='e' && vn[2]=='r' && vn[3]=='a' && vn[4]=='\0'));
active->emergency_console = is_hera ? (active->zuse_session ? 0 : 1) : 0;
console_puts(SK_PROMPT_TEXT);
set_strip_prompt(active); /* FABRIC.md 4.4x: bottom strip mirrors the same prompt */
}
sk_readline(input, sizeof(input));
if (input[0] == '\0') {
console_puts(" ok\n");
continue;
}
vm_interpret(active, input);
/* ABORT stops mid-line but leaves the flag set for the caller to
* consume -- this REPL step is that boundary. Clear it here so the
* next line isn't silently refused by vm_interpret's own check. */
active->abort_requested = 0;
if (active->error) {
console_puts(" ERROR\n");
active->error = 0;
} else {
console_puts(" ok\n");
}
}
}
void sk_repl(VM *vm)
{
/* FABRIC.md item 4.4j: boot and POST (both already returned by the time
* sk_repl() is called) stay on font_8x16.c/VT100 by design; the
* interactive REPL -- this function -- is the boundary where TTF-TEXT
* takes over. One-shot: console_fb_enable_ttf() no-ops on any later
* call. */
console_fb_enable_ttf();
console_println(lithos_version);
console_puts("StarForth Version "); console_println(STARFORTH_VERSION);
console_println("");
console_println("StarForth Emergency CLI");
console_println("FORTH-79 interpreter — type BYE or power off to exit");
console_println("");
sk_repl_run(vm);
}