feat(v4.0.0): the node tells its kernel of its words; the boot seals the system

ENGINE.md 3b, the node's side of ruling A (a word's code is the node's, its
accounts the kernel's).

- dict.v4, system.v4: (WORD-DEFINED) ( xt -- ) is run when an entry is
  made, (WORD-FORGOTTEN) ( w -- ) when FORGET or COLD removes entries; with
  0 there no one is told, as on the hosted product
- test_host_quit.c: a kernel that keeps the list of words and is checked to
  hold exactly the node's dictionary after definitions, a vocabulary, an
  abandoned definition, FORGET, a refused FORGET and COLD; KERNEL-WORD
  called from the prompt and from a definition

Fixed, found while writing that test: since the capsules moved from build
time to boot time (294e6946), what COLD returns to and FORGET protects was
still the nucleus alone, so COLD lost U*, U/MOD and BYE and FORGET U* was
allowed.  The boot now seals the system when it has loaded it
(v4_image_seal), and hosted-check checks COLD, the capsule word after it,
the refused FORGET and BYE.

Verified: make -C v4 test passes at both widths (1283 checks in
test_host_quit.c); hosted-check passes on three ISAs; clean qemu with
STARFORTH_V4=1 on amd64, aarch64 and riscv64 passes POST, and COLD, U*
after it, FORGET U* (refused), an unserved kernel word and BYE typed at
each prompt are answered correctly (logs/20261005-193045, -193307, -193636).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-05 19:38:40 -04:00
co-authored by Claude Opus 5.5
parent e8e8ea13ea
commit 25fc5fd5e3
16 changed files with 609 additions and 15 deletions
+106
View File
@@ -83,6 +83,72 @@ 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 */
/* THE KERNEL. A node asks its kernel by writing to its port (node.h), and
* these tests are that too. Request 1 is "an entry has been made" ( xt -- )
* and request 2 "the entries from here up have gone" ( w -- ): the kernel's
* record of the words on the node (ENGINE.md 3b), kept here as the list of
* their xts. Any other request is noted and nothing is done. */
static v4_cell known[1024];
static unsigned known_n;
static v4_cell last_request;
static void kernel_serve(void)
{
last_request = n.request;
if (n.request == 1) {
v4_cell xt = v4_dstack_pop(&n.ds);
if (known_n < 1024) known[known_n++] = xt;
} else if (n.request == 2) {
v4_cell w = v4_dstack_pop(&n.ds);
unsigned i, kept = 0;
for (i = 0; i < known_n; i++) if (known[i] < w) known[kept++] = known[i];
known_n = kept;
}
}
/* Every entry on the node above the capsule's own, in every vocabulary:
* FORTH's list from LATEST, and each vocabulary's from its head. */
static unsigned node_words(v4_cell *out, unsigned max)
{
unsigned count = 0;
v4_cell xt, voc;
for (xt = n.mem[LATEST]; xt >= DICT_W; xt = n.mem[xt - 1]) if (count < max) out[count++] = xt;
for (voc = n.mem[VOC_LINK]; voc != 0; voc = n.mem[voc + 1])
for (xt = n.mem[voc]; xt >= DICT_W; xt = n.mem[xt - 1]) if (count < max) out[count++] = xt;
return count;
}
/* True if the kernel's record is exactly the words that are on the node. */
static int kernel_agrees(void)
{
static v4_cell there[1024];
unsigned count = node_words(there, 1024), i, k;
if (count != known_n) return 0;
for (i = 0; i < count; i++) {
for (k = 0; k < known_n && known[k] != there[i]; k++) { }
if (k == known_n) return 0;
}
return 1;
}
/* The name of the entry whose code is at xt, as the kernel reads it: the
* word before the link holds the word address of the counted name, its
* bytes four to a cell. */
static const char *node_name(v4_cell xt)
{
static char name[40];
v4_cell at = n.mem[xt - 2] * 4;
unsigned len, i;
len = (unsigned)(((v4_ucell)n.mem[at / 4] >> (8 * (at % 4))) & 0xFFu);
if (len > 31) len = 31;
for (i = 0; i < len; i++) {
v4_cell b = at + 1 + (v4_cell)i;
name[i] = (char)(((v4_ucell)n.mem[b / 4] >> (8 * (b % 4))) & 0xFFu);
}
name[len] = 0;
return name;
}
/* 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. */
@@ -94,6 +160,7 @@ static int run_line(long max)
if (v4_line_done(&n, &img)) { last_steps = steps; return 1; }
(void)v4_exec_step_word(&n, &es, &h);
steps++;
if (n.asking) { kernel_serve(); v4_node_port_served(&n); } /* blocked at its port: the kernel's turn */
if (n.input_pos == n.input_len && n.p >= w_key && n.p < w_key_end) idle++; else idle = 0;
}
last_steps = steps;
@@ -136,6 +203,11 @@ 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);
v4_node_port_attach(&n, PORT);
n.mem[WORD_DEFINED] = 0; /* no one is told of entries, until a test says so */
n.mem[WORD_FORGOTTEN] = 0;
known_n = 0;
last_request = 0;
n.mem[LINE_STATUS] = V4_LINE_COMPLETED;
n.p = img.idle; /* idle, waiting to be handed a line */
typed_len = 0;
@@ -1354,6 +1426,40 @@ int main(void)
CHECK(v4_node_guards_intact(&n), "guards intact");
/* ---- the kernel is told of every entry made and of every entry that goes (ENGINE.md 3b) ---- */
boot();
CHECK(is(say("5 KERNEL-WORD ASK5\n"), " ok\nok> "), "KERNEL-WORD makes a word");
CHECK(is(say("ASK5\n"), " ok\nok> ") && last_request == 5, "whose body writes its number to the port: %ld", (long)last_request);
CHECK(is(say(": TWICE ASK5 ASK5 ; TWICE\n"), " ok\nok> "), "and which a definition can call, again and again");
boot();
CHECK(is(say(": Q1 ;\n"), " ok\nok> ") && known_n == 0, "with nothing in (WORD-DEFINED) no one is told of a new entry");
CHECK(is(say("1 KERNEL-WORD KD 2 KERNEL-WORD KF\n"), " ok\nok> "), "the kernel's two words");
CHECK(is(say("' KD (WORD-DEFINED) ! ' KF (WORD-FORGOTTEN) !\n"), " ok\nok> "), "are put where the node will run them");
known_n = node_words(known, 1024); /* what was there before the kernel was listening */
n.mem[BOOT_CELLS] = n.mem[DP]; /* and all of it is the system, which COLD keeps */
n.mem[BOOT_CELLS + 1] = n.mem[LATEST];
n.mem[FENCE] = (n.mem[DP] + 3) / 4;
{
unsigned before = known_n;
CHECK(kernel_agrees() && before == 3, "the kernel starts knowing the words that are there: %u", before);
CHECK(is(say(": A1 ;\n"), " ok\nok> ") && known_n == before + 1 && kernel_agrees(), "a colon definition is told of");
CHECK(strcmp(node_name(known[known_n - 1]), "A1") == 0, "by its xt, from which its name is read: %s", node_name(known[known_n - 1]));
CHECK(is(say("VARIABLE V1 5 CONSTANT C1 CREATE D1\n"), " ok\nok> ") && known_n == before + 4 && kernel_agrees(),
"and a variable, a constant and a CREATEd word");
CHECK(is(say("VOCABULARY VOC1 VOC1 DEFINITIONS : IN1 ; FORTH DEFINITIONS : A2 ;\n"), " ok\nok> ")
&& known_n == before + 7 && kernel_agrees(), "a vocabulary, a word in it, and a word after it");
CHECK(strstr(say(": BAD NOSUCH ;\n"), "ERROR") != NULL && known_n == before + 8 && kernel_agrees(),
"a definition that is abandoned is still an entry, and is told of");
CHECK(is(say("FORGET C1\n"), " ok\nok> ") && known_n == before + 2 && kernel_agrees(),
"FORGET tells that every entry from that one up has gone: %u left of ours", known_n - before);
CHECK(is(say(": A3 ;\n"), " ok\nok> ") && known_n == before + 3 && kernel_agrees(), "and what is defined next is told of as before");
CHECK(strstr(say("FORGET KD\n"), "Protected word") != NULL && known_n == before + 3 && kernel_agrees(),
"a FORGET that is refused tells nothing");
(void)say("COLD\n");
CHECK(known_n == before && kernel_agrees(), "COLD tells that everything above the system has gone: %u", known_n);
CHECK(is(say(": A4 ; A4\n"), " ok\nok> ") && known_n == before + 1 && kernel_agrees(), "and the node goes on telling");
}
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}