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
+3 -1
View File
@@ -51,7 +51,7 @@
#define CFS_CELLS 32
#define WBUF_W (TOP - 170) /* WORD's buffer: 65 cells = 260 bytes */
#define WBUF_CELLS 65
#define TIB_W (TOP - 440) /* the text input buffer: 260 cells = 1040 bytes */
#define TIB_W (TOP - 440) /* the text input buffer: 260 cells = 1040 bytes; a line is at most 1024 */
#define PAD_W (TOP - 470) /* PAD: 21 cells = 84 bytes */
#define SBUF_W (TOP - 540) /* 64 cells for the tests' own strings */
#define HBUF_W (SVARS - 16) /* the hold buffer: 16 cells = 64 bytes */
@@ -67,6 +67,7 @@
#define ACL_HOOK (BUF0_W - 6) /* the xt of the access control recheck word, or 0 */
#define EMIT_HOOK (BUF0_W - 7) /* the xt of a word EMIT gives its character to, or 0: the console */
#define CATCH (BUF0_W - 8) /* non-zero: a line's error is recorded here, -1, and the line ends " ok" */
#define LINE_STATUS (BUF0_W - 9) /* how the last line ended: 0 QUIT, 1 completed, 2 an error */
#define LOG_LEVEL (BUF0_W - 5) /* the level at or below which a message is printed */
#define BOOT_CELLS (BVARS + 14) /* (BOOT): DP and LATEST as the loader left them, 2 cells */
#define BUF0_W (BVARS - 2 * 256) /* the two block buffers, 256 cells each */
@@ -142,6 +143,7 @@ static int host_load(v4_text *tx, v4_node *n, const char *const *files, unsigned
v4_text_constant(tx, "(SRC-HOOK)", SRC_HOOK);
v4_text_constant(tx, "(EMIT-HOOK)", EMIT_HOOK);
v4_text_constant(tx, "(CATCH)", CATCH);
v4_text_constant(tx, "(LINE-STATUS)", LINE_STATUS);
v4_text_constant(tx, "BLOCK-NUMBER", STORAGE_REG);
v4_text_constant(tx, "BLOCK-ADDRESS", STORAGE_REG + 1);
v4_text_constant(tx, "BLOCK-COMMAND", STORAGE_REG + 2);
+98 -26
View File
@@ -41,6 +41,7 @@
* 3 ROLL is ROT. v3's count from zero.
*/
#include "v4/text.h"
#include "v4/image.h"
#include "v4/testcode.h"
#include <stdint.h>
#include <stdio.h>
@@ -57,7 +58,8 @@ static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_text tx;
static v4_cell w_quit, w_key, w_key_end, w_fault, capsule_latest;
static v4_cell w_key, w_key_end, w_fault, capsule_latest;
static v4_image img; /* where a line is handed to the node: v4/include/v4/image.h */
static char out[V4_CONSOLE_CAP + 1];
/* The storage device's blocks: zeros at every switch-on. */
@@ -71,19 +73,32 @@ static void put_block(unsigned num, const char *text)
}
static long last_steps;
/* Run until the node has taken all its input and has been inside KEY, waiting,
* for 64 instruction words; or until `max` words. True if it is waiting. */
static int run_until_waiting(long max)
/* THE HOST. A node is handed a line and prints no prompt (ENGINE.md 3.1);
* these tests are its host, and do what tools/hosted.c does: take what is
* typed a line at a time, hand each line over, and say " ok" or " ERROR" and
* the next prompt. What is typed after a line is there for a word in it
* that reads the keyboard, and what such a word does not take is the next
* line. */
static char typed[8192]; /* typed and not yet taken */
static unsigned typed_len;
static int line_open; /* a line has been handed over and has not ended */
/* Run until the line ends, or until it has been inside KEY with nothing to
* read for 64 instruction words; or until `max` words. 1: ended, 2: waiting
* for a character, 0: neither. */
static int run_line(long max)
{
long steps = 0;
unsigned idle = 0;
while (steps < max && idle < 64) {
if (v4_line_done(&n, &img)) { last_steps = steps; return 1; }
(void)v4_exec_step_word(&n, &es, &h);
steps++;
if (n.input_pos == n.input_len && n.p >= w_key && n.p < w_key_end) idle++; else idle = 0;
}
last_steps = steps;
return idle >= 64;
if (v4_line_done(&n, &img)) return 1;
return idle >= 64 ? 2 : 0;
}
/* A node just switched on: an empty dictionary above the capsule's words,
@@ -121,8 +136,10 @@ static void boot_with(unsigned depth)
v4_heat_reset(&h);
for (i = 0; i < depth; i++) v4_dstack_push(&n.ds, (v4_cell)(0x5A000000 + i));
v4_dstack_push(&n.ds, CANARY);
n.p = w_quit;
(void)run_until_waiting(100000);
n.mem[LINE_STATUS] = V4_LINE_COMPLETED;
n.p = img.idle; /* idle, waiting to be handed a line */
typed_len = 0;
line_open = 0;
}
static void boot(void) { boot_with(0); }
/* The same with nothing at all on the data stack. */
@@ -141,16 +158,52 @@ static int canary_under_expect(void)
return 0;
}
/* Feed `input` and return what the node prints until it waits again. */
/* Type `input` and return what the console then shows: what each line
* printed, the host's " ok" or " ERROR" after it, and the next prompt. A
* line that has not been ended shows nothing; nor does one that is waiting
* for a character. */
static const char *say(const char *input)
{
unsigned len = (unsigned)strlen(input);
v4_node_console_attach(&n, CONSOLE_TX); /* empties the capture */
if (v4_node_console_feed(&n, input, len) != len) return "(input queue full)";
if (!run_until_waiting(40000000)) return "(still running)";
if (n.console_dropped) return "(too much output)";
memcpy(out, n.console, n.console_len);
out[n.console_len] = 0;
unsigned ilen = (unsigned)strlen(input), olen = 0;
if (typed_len + ilen > sizeof typed) return "(input queue full)";
memcpy(typed + typed_len, input, ilen);
typed_len += ilen;
out[0] = 0;
for (;;) {
const char *tail;
unsigned took;
int ended;
if (!line_open) {
char *nl = (char *)memchr(typed, '\n', typed_len);
unsigned len;
if (!nl) break;
len = (unsigned)(nl - typed);
if (!v4_line_begin(&n, &img, typed, len)) return "(line too long)";
memmove(typed, nl + 1, typed_len - len - 1);
typed_len -= len + 1;
line_open = 1;
}
v4_node_console_attach(&n, CONSOLE_TX); /* empties the capture */
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST); /* and the keyboard */
if (v4_node_console_feed(&n, typed, typed_len) != typed_len) return "(input queue full)";
ended = run_line(40000000);
took = n.input_pos; /* what a word in the line read */
memmove(typed, typed + took, typed_len - took);
typed_len -= took;
if (n.console_dropped || olen + n.console_len + 16 > sizeof out) return "(too much output)";
memcpy(out + olen, n.console, n.console_len);
olen += n.console_len;
out[olen] = 0;
if (ended == 0) return "(still running)";
if (ended == 2) break;
line_open = 0;
tail = v4_line_status(&n, &img) == V4_LINE_COMPLETED ? " ok\nok> "
: v4_line_status(&n, &img) == V4_LINE_ERROR ? " ERROR\nok> " : "ok> ";
memcpy(out + olen, tail, strlen(tail) + 1);
olen += (unsigned)strlen(tail);
}
return out;
}
/* Feed a file of FORTH source to the prompt, a line at a time, as if typed.
@@ -614,7 +667,12 @@ int main(void)
CHECK(v4_text_here(&tx) < DICT_W, "code stays below the dictionary space");
printf(" capsule: %ld words\n", (long)v4_text_here(&tx) - 16);
if (failures) { printf(" %d checks, %d failures\n", checks, failures); return 1; }
w_quit = v4_text_word(&tx, "QUIT");
img.line = v4_text_word(&tx, "(LINE)");
img.idle = v4_text_word(&tx, "(IDLE)");
img.line_status = LINE_STATUS;
img.tib = TIB;
img.tib_bytes = 1025u;
img.span = SPAN;
w_key = v4_text_word(&tx, "KEY");
w_key_end = v4_text_word(&tx, "CR"); /* the word after KEY in core.v4 */
w_fault = v4_text_word(&tx, "(FAULTS)");
@@ -623,10 +681,11 @@ int main(void)
/* ---- switch-on ---- */
boot();
CHECK(n.console_len == 5 && memcmp(n.console, "\nok> ", 5) == 0, "QUIT starts a new line and prompts");
CHECK(n.input_pos == n.input_len, "then waits for a line");
CHECK(run_until_waiting(5000) && last_steps == 64 && n.console_len == 5, "and goes on waiting, printing nothing");
CHECK(v4_line_done(&n, &img) && n.console_len == 0, "the node is idle and has printed nothing: the prompt is its host's");
CHECK(run_line(5000) == 1 && last_steps == 0, "and goes on waiting to be handed a line");
CHECK(canary_under_expect(), "with the data stack as it found it");
(void)v4_exec_step_word(&n, &es, &h);
CHECK(v4_line_done(&n, &img), "it stays idle however long it is run");
/* ---- the sessions ---- */
for (i = 0; i < NSCRIPT; i++) {
@@ -713,16 +772,15 @@ int main(void)
snprintf(want, sizeof want, "<><%.*s\n ok\nok> ", (int)i, "abcdefghijkl");
CHECK(is(say(line), want), "ABORT\" with %u characters, false then true", i);
}
/* the longest: 76 characters fit an 80-character line after ." and a space.
* EXPECT stops at the 80th character, so that line's new-line is still to
* come and is read as an empty line (FORTH-79's EXPECT; see input.v4). */
/* 76 characters after ." and a space make a line of 80. That was the most
* the node's own QUERY would take; a line the host hands over is whole. */
{
char line[128], want[128], text[80];
for (i = 0; i < 76; i++) text[i] = (char)('#' + i); /* no " in it */
text[76] = 0;
boot();
snprintf(line, sizeof line, ".\" %s\"\n", text);
snprintf(want, sizeof want, "%s ok\nok> ok\nok> ", text);
snprintf(want, sizeof want, "%s ok\nok> ", text);
CHECK(is(say(line), want), "a string that fills an 80-character line");
boot();
snprintf(line, sizeof line, ".\" %s\"\n", text + 1);
@@ -730,16 +788,30 @@ int main(void)
CHECK(is(say(line), want), "and one that fills a 79-character line");
}
/* ---- QUERY takes 80 characters; what is over is the next line ---- */
/* ---- a line is handed over whole, up to 1024 characters: a block, as v3 ---- */
boot();
{
char line[128];
static char line[1100];
memset(line, ' ', 100);
memcpy(line, "65 EMIT", 7);
memcpy(line + 70, "66 EMIT", 7);
memcpy(line + 80, "67 EMIT 68 EMIT", 15);
line[100] = '\n'; line[101] = 0;
CHECK(is(say(line), "AB ok\nok> CD ok\nok> "), "a line of 100 characters is read as 80 and 20");
CHECK(is(say(line), "ABCD ok\nok> "), "a line of 100 characters is one line");
boot();
memset(line, ' ', 1024);
memcpy(line, "65 EMIT", 7);
memcpy(line + 1017, "66 EMIT", 7);
line[1024] = '\n'; line[1025] = 0;
CHECK(is(say(line), "AB ok\nok> "), "a line of 1024 characters is interpreted to its end");
boot();
memset(line, ' ', 1025);
memcpy(line, "65 EMIT", 7);
line[1025] = '\n'; line[1026] = 0;
CHECK(is(say(line), "(line too long)"), "one of 1025 is not taken: the node's input buffer holds a block");
CHECK(v4_line_done(&n, &img) && n.console_len == 0, "and nothing of it was run");
}
/* ---- ABORT and QUIT from deep in a programme, again and again ---- */