feat(v4.0.0): POST is the kernel's -- the boot feeds the cases, and nothing of the harness is on the node

The boot runs POST with the runner (v4/system/post.c) after the capsules:
it feeds the 538 cases to the node and judges them from outside.  A
case's printing no longer reaches the console.  What the cases define
stays in the dictionary, as in v3; the system is sealed after POST and
the boot prints PARITY:V4_SYSTEM word_count=N dict_hash=..., as v3 prints
its parity after POST.

Gone: capsules/v4/post79.4th and its blocks 7000 up; the harness words;
(CATCH) and (EMIT-HOOK), with what EMIT and the prompt loop did for them.

The generator runs v3 on the lines as the kernel sends them, without the
capsule's "T| ".  One expected result follows from that: >IN.initial
prints 6, not 9.

make -C v4 test, sanitize and hosted-check pass; amd64, aarch64 and
riscv64 boot, POST 538 of 538, word_count=411, dict_hash
0x6fb1d09418b189ee on all six: logs/20261007-105118, -105335, -105651.
T{ is unknown at the prompt; RS1 prints 42 42 before and after COLD.  A
scratch build with one expectation changed names the case and ends
PARITY:FAIL, POST: FAILED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 10:59:02 -04:00
co-authored by Claude Opus 5.5
parent 2bf958ce3a
commit 4bdb210a99
20 changed files with 659 additions and 1981 deletions
+51 -50
View File
@@ -4,6 +4,7 @@
* It is not part of the engine (v4/src): it needs the capsule directory,
* which only a whole system has.
*/
#include "v4/post.h"
#include "v4/blocks.h"
#include "v4/boot.h"
#include "v4/message.h"
@@ -13,18 +14,18 @@
#include "starkernel/capsule_blocks.h"
/* The capsules, in the order they are loaded. */
/* POST is part of the boot: v4:post79.4th is loaded after the vocabulary it
* tests and must end with a clean tally. -DV4_POST_AT_BOOT=0 leaves it
* out, for work on a vocabulary that does not pass yet. */
static const char *const boot_capsules[] = {
"v4:forth79.4th",
};
/* POST is part of the boot, and is the kernel's (v4/include/v4/post.h;
* docs/v4.0.0/NUCLEUS.md 6.3): when the capsules are in, the kernel feeds
* its cases to the node and judges them, and the boot passes only if none
* failed. -DV4_POST_AT_BOOT=0 leaves it out, for work on a vocabulary
* that does not pass yet. */
#ifndef V4_POST_AT_BOOT
#define V4_POST_AT_BOOT 1
#endif
static const char *const boot_capsules[] = {
"v4:forth79.4th",
#if V4_POST_AT_BOOT
"v4:post79.4th",
#endif
};
#define LINE_MAX 1025u /* a line is at most 1024 characters, a block */
#define STEP_LIMIT 4000000000ULL /* instruction words one line may take */
@@ -77,37 +78,6 @@ static uint32_t word_count(const v4_node *n, const v4_image *im)
/* ---- handing the node a line ---------------------------------------------- */
/* POST's verdict. The POST capsule ends by printing one line,
* PARITY:V4_POST tests=N pass=N fail=N
* and the boot passes only if it has seen that line with fail=0. That no
* line was refused is not enough: a POST that was broken half way would
* refuse nothing. */
static char post_line[96];
static unsigned post_len;
static int post_seen, post_clean;
static int text_at(const char *line, unsigned len, unsigned at, const char *want)
{
unsigned i;
for (i = 0; want[i]; i++)
if (at + i >= len || line[at + i] != want[i]) return 0;
return 1;
}
static void post_watch(char c)
{
static const char clean[] = " fail=0";
if (c != '\n') {
if (post_len < sizeof post_line) post_line[post_len++] = c;
return;
}
if (text_at(post_line, post_len, 0, "PARITY:V4_POST ")) {
post_seen = 1;
post_clean = post_len >= sizeof clean - 1 && text_at(post_line, post_len, post_len - (unsigned)(sizeof clean - 1), clean);
}
post_len = 0;
}
/* The boot is the node's neighbour on two of its ports. On port 0 it is
* the kernel: the node writes the number of a request there (ENGINE.md
* 3.3) -- for a block (blocks.h), or for one of the host's words. On port
@@ -120,7 +90,10 @@ static void post_watch(char c)
/* What the kernel keeps of the node's block window (blocks.h). */
static v4_blocks boot_blocks;
int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
/* Hand the node a line and run it to its end. What it prints goes to the
* console; or, if `keep` is given, into `keep`, at most `cap` characters,
* and none of it is shown: *printed is then how many it printed in all. */
static int run_line(const v4_boot *b, const char *text, unsigned len, char *keep, unsigned cap, unsigned *printed)
{
static v4_message out, in; /* one line at a time: the boot is not re-entered */
v4_node *n = b->n;
@@ -155,8 +128,8 @@ int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
if (v4_message_type(&in) == V4_MSG_OUTPUT) {
for (i = 0; i < chars; i++) {
char c = v4_message_char(&in, i);
post_watch(c);
b->out(&c, 1);
if (!keep) b->out(&c, 1);
else { if (*printed < cap) keep[*printed] = c; (*printed)++; }
}
} else if (v4_message_type(&in) == V4_MSG_DONE) {
return (int)in.word[V4_MSG_HEADER];
@@ -185,6 +158,27 @@ int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
}
}
int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
{
return run_line(b, text, len, 0, 0u, 0);
}
#if V4_POST_AT_BOOT
/* POST's host (post.h): a line is handed to the node as any line is, and
* what the node prints is kept for the runner and not shown. */
static int post_line(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len)
{
int how;
*len = 0;
how = run_line((const v4_boot *)self, text, text_len, out, cap, len);
return (how == V4_TEXT_QUIT || how == V4_TEXT_COMPLETED || how == V4_TEXT_ERROR) ? how : -1;
}
static void post_say(void *self, const char *text, unsigned len)
{
((const v4_boot *)self)->out(text, len);
}
#endif
/* ---- one capsule --------------------------------------------------------- */
static void say_where(const v4_boot *b, const char *name, uint32_t block, uint32_t line)
@@ -345,7 +339,6 @@ int v4_boot_run(const v4_boot *b)
{
unsigned i;
post_len = 0; post_seen = 0; post_clean = 0;
/* a node is born with nothing in it, and is sent the nucleus */
if (!v4_image_born(b->n, b->es, b->h, b->im)) {
say(b, "V4: the nucleus is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
@@ -375,16 +368,24 @@ int v4_boot_run(const v4_boot *b)
for (i = 0; i < sizeof boot_capsules / sizeof boot_capsules[0]; i++)
if (!load_capsule(b, boot_capsules[i])) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
#if V4_POST_AT_BOOT
if (!post_seen || !post_clean) {
say(b, post_seen ? "V4: POST reported failures\n" : "V4: POST did not report\n");
say(b, "PARITY:FAIL\nPOST: FAILED\n");
return 0;
{
v4_post_host host;
v4_post_tally tally;
host.self = (void *)b; host.n = b->n; host.line = post_line; host.say = post_say;
if (!v4_post_run(&host, v4_post_cases, v4_post_case_count, &tally)) {
say(b, "V4: POST reported failures\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
}
#endif
/* The kernel's words and the capsules are the system: COLD comes back
* to here, and FORGET will not go below it. */
/* The kernel's words, the capsules and what POST's cases defined are
* the system, as in v3: COLD comes back to here, and FORGET will not go
* below it. */
v4_image_seal(b->n, b->im);
say(b, "PARITY:V4_SYSTEM word_count="); say_dec(b, word_count(b->n, b->im));
say(b, " dict_hash="); say_hex(b, v4_boot_dict_hash(b->n, b->im));
say(b, "\n");
#if V4_POST_AT_BOOT
say(b, "PARITY:OK\nPOST: PASSED\n");
+1 -1
View File
@@ -877,7 +877,7 @@ const v4_post_case v4_post_cases[] = {
{ "-TRAILING.empty", l318, 1u, V4_POST_STACK, 0, 0u, "\012", 1u },
{ "CMOVE.basic", l319, 2u, V4_POST_STACK, 0, 0u, "65 66 \012", 7u },
{ "CMOVE.empty", l320, 1u, V4_POST_STACK, 0, 0u, "", 0u },
{ ">IN.initial", l321, 1u, V4_POST_STACK, 0, 0u, "9 \012", 3u },
{ ">IN.initial", l321, 1u, V4_POST_STACK, 0, 0u, "6 \012", 3u },
{ ">IN.advances", l322, 1u, V4_POST_STACK, 0, 0u, "0 \012", 3u },
{ "..positive", l323, 1u, V4_POST_STACK, 0, 0u, "42 \012", 4u },
{ "..negative", l324, 1u, V4_POST_STACK, 0, 0u, "-42 \012", 5u },