feat(v4.0.0): a node is handed a line -- ENGINE.md step 1

A v4 node no longer reads its own command line or prints a prompt.  Its
host puts a line of text in the node's input buffer and starts it at
(LINE); the node interprets it and stops at (IDLE), leaving in
(LINE-STATUS) how it ended: completed, an error, or QUIT.  The host says
" ok" or " ERROR" and prompts, as the kernel's REPL does for a v3 VM.  A
line may be 1024 characters, a block, as v3's.  Ruled 2026-10-05
(V3-PARITY.md 1b); design ENGINE.md 3.1.

- quit.v4: (REPL), the node's prompt loop, is gone; (LINE) (IDLE) (DONE)
- image.h/.c: v4_line_begin, v4_line_done, v4_line_status; the node is
  idle at switch-on
- boot.c: v4_boot_line, the one loop the hosted binary, the kernel and the
  capsule loader hand a line with; the code that took " ok" and the prompt
  back out of the node's output is gone
- hosted.c, sk_v4.c: the prompt and the line editing are the host's
- test_host_quit.c: the tests are the node's host; two tests of the old
  80-character prompt line now test a whole line, 1024 and 1025 characters

Verified: make -C v4 test passes at both widths; hosted-check passes on
three ISAs; clean qemu with STARFORTH_V4=1 on amd64, aarch64 and riscv64
passes POST (550 of 550) with the same hashes as hosted, and three lines
typed at each bare-metal prompt through the serial port are answered
correctly (logs/20261005-180922, -181152, -181541).

Still the lone node: kernel_main.c starts it before the fleet tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-05 18:17:37 -04:00
co-authored by Claude Opus 5.5
parent ab7a9bf06f
commit 01c447f9ab
17 changed files with 771 additions and 253 deletions
+30 -17
View File
@@ -8,7 +8,7 @@
*/
#include "v4/boot.h"
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BLOCKS 64u
@@ -22,27 +22,40 @@ static void console_out(const char *text, unsigned len)
(void)fwrite(text, 1, len, stdout);
}
/* a character for a line that reads the keyboard */
static int console_key(void)
{
int c;
fflush(stdout);
c = getchar();
return c == EOF ? V4_BOOT_KEY_END : c;
}
/* The prompt is the host's, as it is the kernel's REPL's for a v3 VM: read a
* line, hand it to the node, say how it ended. */
int main(void)
{
const v4_image *im = &v4_capsule_image;
unsigned char buf[256];
static char line[1026];
v4_boot boot;
boot.n = &n; boot.es = &es; boot.h = &h; boot.im = im; boot.out = console_out;
boot.n = &n; boot.es = &es; boot.h = &h; boot.im = &v4_capsule_image;
boot.out = console_out; boot.key = console_key;
if (!v4_boot_run(&boot, disk, BLOCKS)) { fflush(stdout); return 1; }
for (;;) {
(void)v4_exec_step_word(&n, &es, &h);
if (n.console_len) {
(void)fwrite(n.console, 1, n.console_len, stdout);
n.console_len = 0;
}
if (n.stopped) { fflush(stdout); fprintf(stderr, "starforth4: the node stopped on a fault\n"); return 1; }
if (v4_image_waiting(&n, im)) {
ssize_t got;
fflush(stdout);
got = read(0, buf, sizeof buf);
if (got <= 0) { putchar('\n'); return 0; }
(void)v4_node_console_feed(&n, buf, (unsigned)got);
}
size_t len;
int how;
fputs("ok> ", stdout);
fflush(stdout);
if (!fgets(line, sizeof line, stdin)) { putchar('\n'); return 0; }
len = strlen(line);
if (len && line[len - 1] == '\n') len--;
how = v4_boot_line(&boot, line, (unsigned)len);
if (how == V4_LINE_COMPLETED) fputs(" ok\n", stdout);
else if (how == V4_LINE_ERROR) fputs(" ERROR\n", stdout);
else if (how == V4_BOOT_LINE_TOO_LONG) fputs("Line too long\n ERROR\n", stdout);
else if (how == V4_BOOT_LINE_NO_INPUT) { putchar('\n'); return 0; }
else if (how == V4_BOOT_LINE_STOPPED) { fflush(stdout); fprintf(stderr, "starforth4: the node stopped on a fault\n"); return 1; }
}
}
+6 -30
View File
@@ -4,7 +4,7 @@
*
* Runs on the build machine. It assembles capsule/ *.v4 -- the nucleus,
* docs/v4.0.0/NUCLEUS.md -- onto a host node with the memory map of
* tests/host_map.h and starts the node at its prompt. Then it writes every
* tests/host_map.h. It runs nothing: it writes every
* word of the node's memory that is not zero, and the addresses a loader
* needs, as a C file. No FORTH source is compiled here: what is not in the
* nucleus is loaded from capsules when the system boots (v4/system/boot.c).
@@ -22,11 +22,8 @@
#include "../tests/host_map.h"
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_text tx;
static v4_cell w_key, w_key_end;
static unsigned char disk[4 * V4_BLOCK_BYTES];
static void die(const char *what, const char *detail)
{
@@ -34,19 +31,6 @@ static void die(const char *what, const char *detail)
exit(1);
}
/* run until the node has taken all its input and is waiting in KEY */
static void run_until_waiting(void)
{
long steps = 0;
unsigned idle = 0;
while (idle < 64) {
if (++steps > 200000000L) die("the node did not come back to its prompt", NULL);
if (n.stopped) die("the node stopped on a fault", NULL);
(void)v4_exec_step_word(&n, &es, &h);
if (n.input_pos == n.input_len && n.p >= w_key && n.p < w_key_end) idle++; else idle = 0;
}
}
/* every entry from xt back: no access control fields set */
static void clear_acl(v4_cell xt)
{
@@ -58,7 +42,7 @@ int main(int argc, char **argv)
static const char *const files[] = { "core.v4", "input.v4", "dict.v4", "codegen.v4", "compile.v4", "quit.v4", "forth.v4",
"numout.v4", "words.v4", "system.v4", "qmath.v4", "blocks.v4", "log.v4", "acl.v4" };
FILE *out;
v4_cell k, entry, faults, voc;
v4_cell k, line, idle, faults, voc;
unsigned count = 0;
if (argc != 2) die("usage: mkimage OUTPUT.c", NULL);
@@ -66,7 +50,8 @@ int main(int argc, char **argv)
if (!host_load(&tx, &n, files, (unsigned)(sizeof files / sizeof files[0]))) die("the capsule does not assemble", NULL);
if (!v4_text_finish(&tx)) die("not everything is defined", v4_text_error(&tx));
if (v4_text_here(&tx) >= DICT_W) die("the capsule's code runs into the dictionary space", NULL);
entry = v4_text_word(&tx, "QUIT");
line = v4_text_word(&tx, "(LINE)");
idle = v4_text_word(&tx, "(IDLE)");
faults = v4_text_word(&tx, "(FAULTS)");
w_key = v4_text_word(&tx, "KEY");
w_key_end = v4_text_word(&tx, "CR");
@@ -83,16 +68,6 @@ int main(int argc, char **argv)
n.mem[CURRENT] = LATEST;
n.mem[SRC] = TIB;
v4_exec_reset(&es);
v4_heat_reset(&h);
v4_node_console_attach(&n, CONSOLE_TX);
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
v4_node_error_attach(&n, NODE_ERROR);
v4_node_fault_attach(&n, faults);
v4_node_storage_attach(&n, STORAGE_REG, disk, 4);
n.p = entry;
run_until_waiting();
/* The nucleus is the system as COLD finds it: COLD comes back to here,
* and FORGET will not go below it. What the capsules add at boot
* (v4/system/boot.c) is above it. */
@@ -125,7 +100,8 @@ int main(int argc, char **argv)
}
fprintf(out, "};\n\nconst v4_image v4_capsule_image = {\n cells, %uu,\n %uu, %uu, %uu, %uu,\n", count,
(unsigned)V4_CELL_BITS, (unsigned)V4_NODE_WORDS, (unsigned)V4_DATA_RING, (unsigned)V4_RET_RING);
fprintf(out, " %ld, %ld,\n %ld, %ld,\n", (long)entry, (long)faults, (long)w_key, (long)w_key_end);
fprintf(out, " %ld, %ld, %ld,\n %ld, %uu, %ld,\n %ld,\n %ld, %ld,\n", (long)line, (long)idle, (long)LINE_STATUS,
(long)TIB, 1025u, (long)SPAN, (long)faults, (long)w_key, (long)w_key_end);
fprintf(out, " %ld, %ld, %ld,\n %ld, %ld,\n %ld,\n %ld,\n %ld, %ld\n};\n", (long)CONSOLE_TX, (long)CONSOLE_RX, (long)CONSOLE_ST,
(long)DSTACK_REG, (long)RSTACK_REG, (long)NODE_ERROR, (long)STORAGE_REG, (long)DP, (long)LATEST);
if (fclose(out) != 0) die("cannot write", argv[1]);