feat(v4.0.0): the POST runner -- the kernel feeds a case and judges it from outside

v4/system/post.c: for each case it empties the node's data stack, sends
DECIMAL FORTH DEFINITIONS, sends the case's lines keeping what the node
prints, reads the stack, and judges: an error exactly if one is expected,
and otherwise the stack and every character printed.  A failing case is
named with what it printed and left.  Nothing of it is on the node.

Its tests are in test_host_quit.c, on that test's node: 29 checks of cases
that must pass and cases that must fail -- a wrong value, depth, order or
output, an error wanted or unwanted, a case of two lines, depth-only
cases, the starting state, more printing than is kept, and a line that
never comes back.  Both widths and the sanitizers.

The boot still runs the capsule; that is the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 10:36:36 -04:00
co-authored by Claude Opus 5.5
parent c2f4b9808a
commit 2bf958ce3a
4 changed files with 241 additions and 1 deletions
+105
View File
@@ -53,6 +53,7 @@ static int failures = 0, checks = 0;
#define CANARY ((v4_cell)0x0C0FFEE5)
#include "v4/blocks.h"
#include "v4/post.h"
#include "block_subsystem.h"
#include "host_map.h"
@@ -358,6 +359,53 @@ static const char *say(const char *input)
}
return out;
}
/* POST's RUNNER (v4/system/post.c), with these tests as its host: a line is
* sent to the node as the console sends one, and what the node prints is
* handed back and not shown. `post_lost` is the text of a line that is
* never to come back, and `post_state_fails` makes the line that sets the
* state end in an error; `post_said` is what the runner printed. */
static const char *post_lost;
static int post_state_fails;
static char post_said[4096];
static unsigned post_said_len;
static int post_line(void *self, const char *text, unsigned text_len, char *o, unsigned cap, unsigned *len)
{
int ended;
(void)self;
*len = 0;
if (post_lost && strlen(post_lost) == text_len && memcmp(post_lost, text, text_len) == 0) return -1;
if (post_state_fails && text_len == 25 && memcmp(text, "DECIMAL FORTH DEFINITIONS", 25) == 0) text = "NOSUCHWORD-FOR-STATE ";
if (!v4_message_text(&to_node, 0, 1, V4_MSG_TEXT, text, text_len)) return -1;
to_sent = 0; from_node.count = 0;
shown_len = 0; shown_dropped = 0;
ended = run_line(40000000);
if (ended != 1) return -1;
*len = shown_dropped ? (unsigned)sizeof shown + 1u : shown_len;
memcpy(o, shown, shown_len < cap ? shown_len : cap);
return text_ended;
}
static void post_say(void *self, const char *text, unsigned len)
{
(void)self;
if (post_said_len + len < sizeof post_said) { memcpy(post_said + post_said_len, text, len); post_said_len += len; post_said[post_said_len] = 0; }
}
static const v4_post_host post_host = { 0, &n, post_line, post_say };
/* Run one case; 1 if it passed. What the runner said is in post_said. */
static int post_one(const char *name, const char *l0, const char *l1, int expect, const long long *stack, unsigned depth, const char *output)
{
static const char *lines[2];
v4_post_case c;
v4_post_tally t;
int ok;
lines[0] = l0; lines[1] = l1;
c.name = name; c.lines = lines; c.line_count = l1 ? 2u : 1u; c.expect = expect;
c.stack = stack; c.depth = depth; c.output = output; c.output_len = (unsigned)strlen(output);
post_said_len = 0; post_said[0] = 0;
ok = v4_post_run(&post_host, &c, 1, &t);
if (t.tests != 1 || t.pass + t.fail != 1 || (int)t.pass != ok) return -1;
return ok;
}
/* Feed a file of FORTH source to the prompt, a line at a time, as if typed.
* True if every line was accepted: the node said " ok" and nothing else. */
static int load_source(const char *name)
@@ -1172,6 +1220,63 @@ int main(void)
CHECK(is(say("FORGET G3 G1 G2\n"), "AB ok\nok> ") && is(say("G3\n"), "UNKNOWN WORD: 'G3'\n ERROR\nok> "), "one above it can");
}
/* ---- POST's runner: the kernel feeds a case and judges it from outside (NUCLEUS.md 6.3) ---- */
{
static const long long three[] = { 3 }, four[] = { 4 }, two_vals[] = { 3, 3 }, seven[] = { 7 }, ab[] = { 1, 2 };
static char big[2100];
boot();
CHECK(post_one("add.right", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 1 && strstr(post_said, "POST FAIL") == NULL
&& strstr(post_said, "PARITY:V4_POST tests=1 pass=1 fail=0\n") != NULL, "a case that leaves what the table has passes, and the tally says so: \"%s\"", post_said);
CHECK(post_one("add.wrong", "1 2 +", 0, V4_POST_STACK, four, 1, "") == 0 && strstr(post_said, "POST FAIL: add.wrong ") != NULL && strstr(post_said, "stack<3 >") != NULL
&& strstr(post_said, "PARITY:V4_POST tests=1 pass=0 fail=1\n") != NULL, "one that leaves another value fails, by name, with the stack it left: \"%s\"", post_said);
CHECK(post_one("add.depth", "1 2 +", 0, V4_POST_STACK, two_vals, 2, "") == 0, "one that leaves fewer values than the table has fails");
CHECK(post_one("two.order", "1 2", 0, V4_POST_STACK, ab, 2, "") == 1 && post_one("two.swapped", "2 1", 0, V4_POST_STACK, ab, 2, "") == 0, "the stack is compared in order, the deepest first");
CHECK(post_one("dot.right", "5 DUP . . CR", 0, V4_POST_STACK, 0, 0, "5 5 \n") == 1, "what a case prints is compared");
CHECK(strstr(post_said, "5 5") == NULL, "and what a passing case prints does not reach the console: \"%s\"", post_said);
CHECK(post_one("emit.wrong", "65 EMIT", 0, V4_POST_STACK, 0, 0, "B") == 0 && strstr(post_said, "POST FAIL: emit.wrong out<A>") != NULL, "one that prints another character fails, with what it printed: \"%s\"", post_said);
CHECK(post_one("emit.short", "65 EMIT 66 EMIT", 0, V4_POST_STACK, 0, 0, "A") == 0 && post_one("emit.long", "65 EMIT", 0, V4_POST_STACK, 0, 0, "AB") == 0, "and one that prints more, or less");
CHECK(post_one("err.right", "NOSUCHWORD", 0, V4_POST_ERROR, 0, 0, "") == 1, "a case that must end in an error and does passes");
CHECK(strstr(post_said, "UNKNOWN") == NULL, "and the message of an expected error does not reach the console");
CHECK(post_one("err.none", "1 2 +", 0, V4_POST_ERROR, 0, 0, "") == 0, "one that must end in an error and does not fails");
CHECK(post_one("err.unwanted", "NOSUCHWORD", 0, V4_POST_STACK, 0, 0, "") == 0, "one that ends in an error and must not fails");
CHECK(post_one("err.first", "NOSUCHWORD", "1 DROP", V4_POST_ERROR, 0, 0, "") == 1, "a case has ended in an error if any of its lines did");
CHECK(post_one("two.lines", ": PQ 7 ;", "PQ", V4_POST_STACK, seven, 1, "") == 1, "a case of two lines is judged on what both leave");
CHECK(is(say("PQ .\n"), "7 ok\nok> "), "and what a case defines stays on the node");
CHECK(post_one("here.depth", "HERE", 0, V4_POST_DEPTH, 0, 1, "") == 1 && post_one("here.depth2", "HERE", 0, V4_POST_DEPTH, 0, 2, "") == 0, "a case may be checked for how many values it leaves and no more");
CHECK(post_one("here.out", "HERE 65 EMIT", 0, V4_POST_DEPTH_OUTPUT, 0, 1, "A") == 1 && post_one("here.out2", "HERE 65 EMIT", 0, V4_POST_DEPTH_OUTPUT, 0, 1, "B") == 0, "or for that and what it prints");
CHECK(post_one("depth.noout", "HERE 65 EMIT", 0, V4_POST_DEPTH, 0, 1, "") == 1, "a case checked for depth alone may print what it likes");
/* the state every case starts from */
CHECK(is(say("1 2 3 HEX\n"), " ok\nok> "), "the node is left in HEX with three values on its stack");
CHECK(post_one("state", "10 .", 0, V4_POST_STACK, 0, 0, "10 ") == 1, "a case still starts in decimal with nothing on the stack");
post_state_fails = 1;
CHECK(post_one("state.bad", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 0 && strstr(post_said, "POST FAIL: state.bad ") != NULL, "if the state cannot be set the case fails, by name");
post_state_fails = 0;
/* a case that prints more than is kept */
CHECK(is(say(": MANY 2000 0 DO 65 EMIT LOOP ;\n"), " ok\nok> "), "a word that prints 2,000 characters");
memset(big, 'A', 2000); big[2000] = 0;
CHECK(post_one("many", "MANY", 0, V4_POST_STACK, 0, 0, big) == 0 && strstr(post_said, "POST FAIL: many ") != NULL, "a case that prints more than the runner keeps fails, by name");
CHECK(post_one("after.many", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 1, "and the next case is judged as usual");
/* a line that never comes back */
post_lost = "LOST-LINE";
{
static const char *const la[] = { "1 2 +" }, *const lb[] = { "LOST-LINE" }, *const lc[] = { "65 EMIT" };
static const v4_post_case three_cases[] = {
{ "first", la, 1u, V4_POST_STACK, three, 1u, "", 0u },
{ "lost", lb, 1u, V4_POST_STACK, 0, 0u, "", 0u },
{ "last", lc, 1u, V4_POST_STACK, 0, 0u, "A", 1u },
};
v4_post_tally t;
post_said_len = 0; post_said[0] = 0;
CHECK(v4_post_run(&post_host, three_cases, 3, &t) == 0 && t.tests == 3 && t.pass == 2 && t.fail == 1
&& strstr(post_said, "POST FAIL: lost ") != NULL && strstr(post_said, "PARITY:V4_POST tests=3 pass=2 fail=1\n") != NULL,
"a line that does not come back fails its case; the others are still judged, and the run is not a pass: \"%s\"", post_said);
}
post_lost = 0;
}
/* ---- storage that refuses a write (MESH.md 8.2) ---- */
boot();
CHECK(is(say(REFUSING_FIRST " BLOCK DROP UPDATE SAVE-BUFFERS 65 EMIT\n"), "Storage refused\n ERROR\nok> "), "a write storage will not take says so, and the prompt returns");