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>
363 lines
14 KiB
C
363 lines
14 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 <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);
|
||
}
|