From 2bf958ce3aa396a9a1e167dcb454ee0a4a2f62b3 Mon Sep 17 00:00:00 2001 From: rajames Date: Wed, 7 Oct 2026 10:36:36 -0400 Subject: [PATCH] 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 --- v4/Makefile | 4 +- v4/include/v4/post.h | 29 +++++++++++ v4/system/post.c | 104 +++++++++++++++++++++++++++++++++++++ v4/tests/test_host_quit.c | 105 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 v4/system/post.c diff --git a/v4/Makefile b/v4/Makefile index 4200c747..14f4582a 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -72,7 +72,9 @@ $(BINDIR)/san-v3blocks.o: $(V3_BLOCK_SRCS) $(wildcard $(ROOT_DIR)/v3/include/*.h $(LD) -r $(BINDIR)/san-v3o/*.o -o $@ # blocks.c is compiled with each test that uses it, under that test's own # flags: its warnings, its sanitizers, its cell width and node size. -blocks_src = $(if $(call uses_blocks,$(1)),$(BLOCKS_SRC)) +# So are POST's runner and its cases (system/post.c, system/post_cases.c; +# docs/v4.0.0/NUCLEUS.md 6.3), which are the kernel's too. +blocks_src = $(if $(call uses_blocks,$(1)),$(BLOCKS_SRC) $(HERE)/system/post.c $(HERE)/system/post_cases.c) blocks_objs = $(if $(call uses_blocks,$(1)),$(BINDIR)/v3blocks.o) san_blocks_objs = $(if $(call uses_blocks,$(1)),$(BINDIR)/san-v3blocks.o) diff --git a/v4/include/v4/post.h b/v4/include/v4/post.h index bc9e5f62..225855b2 100644 --- a/v4/include/v4/post.h +++ b/v4/include/v4/post.h @@ -29,4 +29,33 @@ typedef struct { extern const v4_post_case v4_post_cases[]; extern const unsigned v4_post_case_count; +/* THE RUNNER (v4/system/post.c). Whoever runs POST on a node supplies how a + * line is sent to it and where the console is. For each case the runner + * empties the node's data stack from outside; sends DECIMAL FORTH + * DEFINITIONS, the state every expected result was taken from; sends the + * case's lines one at a time, keeping what the node prints and showing none + * of it; and judges. A case passes when it ended in an error exactly if + * one was expected and, if none was, its stack and its printing are what + * the table has. It has ended in an error if any of its lines did. What + * the cases define stays in the node's dictionary, as in v3. */ +#define V4_POST_OUT 1024u /* the most of a case's printing that is kept */ + +typedef struct { + void *self; + v4_node *n; /* the node: its data stack is read and emptied */ + /* Send the node a line and run it to its end. What it prints is put at + * `out`, at most `cap` characters, and *len is how many it printed in + * all. Returns V4_TEXT_QUIT, V4_TEXT_COMPLETED or V4_TEXT_ERROR + * (message.h); or a negative number if the line did not come back. */ + int (*line)(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len); + void (*say)(void *self, const char *text, unsigned len); /* the console */ +} v4_post_host; + +typedef struct { unsigned tests, pass, fail; } v4_post_tally; + +/* Run the cases. Prints "POST FAIL: name out<...> stack<...>" for each + * failing case and then "PARITY:V4_POST tests=N pass=N fail=N". Returns 1 + * if none failed. */ +int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally); + #endif /* V4_POST_H */ diff --git a/v4/system/post.c b/v4/system/post.c new file mode 100644 index 00000000..8b0ba0d9 --- /dev/null +++ b/v4/system/post.c @@ -0,0 +1,104 @@ +/* post.c -- POST's runner: the kernel feeds a case to the node and judges it + * from outside. See post.h; docs/v4.0.0/NUCLEUS.md section 6.3. + * + * Nothing here uses the C library: the bare-metal kernel links this file. + */ +#include "v4/post.h" +#include "v4/message.h" + +#define SHOWN_OUT 255u /* how much of a failing case's printing is shown */ +#define SHOWN_STACK 32u /* and how many of the values it left */ + +static const char state_line[] = "DECIMAL FORTH DEFINITIONS"; + +static unsigned length(const char *s) +{ + unsigned n = 0; + while (s[n]) n++; + return n; +} + +static void say(const v4_post_host *h, const char *s) +{ + h->say(h->self, s, length(s)); +} + +static void say_number(const v4_post_host *h, long long v) +{ + char text[24]; + unsigned at = sizeof text; + unsigned long long u = v < 0 ? 0ull - (unsigned long long)v : (unsigned long long)v; + + do { text[--at] = (char)('0' + u % 10u); u /= 10u; } while (u); + if (v < 0) text[--at] = '-'; + h->say(h->self, text + at, (unsigned)sizeof text - at); +} + +int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally) +{ + static char out[V4_POST_OUT]; /* what the case being run has printed */ + v4_cell left[SHOWN_STACK]; /* what it left on the stack, the top first */ + unsigned i, k; + + tally->tests = 0; tally->pass = 0; tally->fail = 0; + for (i = 0; i < count; i++) { + const v4_post_case *c = &cases[i]; + unsigned kept = 0, total = 0, len = 0, depth; + int errored = 0, came_back = 1, ok, how; + + /* every case starts from the same state */ + v4_dstack_reset(&h->n->ds); + how = h->line(h->self, state_line, (unsigned)sizeof state_line - 1u, out, 0u, &len); + if (how != V4_TEXT_COMPLETED) came_back = 0; + v4_dstack_reset(&h->n->ds); + + for (k = 0; came_back && k < c->line_count; k++) { + len = 0; + how = h->line(h->self, c->lines[k], length(c->lines[k]), out + kept, V4_POST_OUT - kept, &len); + if (how < 0) { came_back = 0; break; } + if (how == V4_TEXT_ERROR) errored = 1; + total += len; + kept = total < V4_POST_OUT ? total : V4_POST_OUT; + } + + /* what it left, read before anything is judged */ + depth = h->n->ds.depth; + for (k = 0; k < depth; k++) { + v4_cell v = v4_dstack_pop(&h->n->ds); + if (k < SHOWN_STACK) left[k] = v; + } + v4_dstack_reset(&h->n->ds); + + if (!came_back) { + ok = 0; + } else if (c->expect == V4_POST_ERROR) { + ok = errored; + } else if (errored) { + ok = 0; + } else { + ok = depth == c->depth; + if (ok && c->expect == V4_POST_STACK) { + if (depth > SHOWN_STACK) ok = 0; + for (k = 0; ok && k < depth; k++) ok = left[k] == (v4_cell)c->stack[depth - 1u - k]; + } + if (ok && c->expect != V4_POST_DEPTH) { + ok = total == c->output_len && total <= V4_POST_OUT; + for (k = 0; ok && k < total; k++) ok = out[k] == c->output[k]; + } + } + + tally->tests++; + if (ok) { tally->pass++; continue; } + tally->fail++; + say(h, "POST FAIL: "); say(h, c->name); + say(h, " out<"); h->say(h->self, out, kept < SHOWN_OUT ? kept : SHOWN_OUT); + say(h, "> stack<"); + for (k = depth < SHOWN_STACK ? depth : SHOWN_STACK; k-- > 0; ) { say_number(h, (long long)left[k]); say(h, " "); } + say(h, ">\n"); + } + say(h, "PARITY:V4_POST tests="); say_number(h, (long long)tally->tests); + say(h, " pass="); say_number(h, (long long)tally->pass); + say(h, " fail="); say_number(h, (long long)tally->fail); + say(h, "\n"); + return tally->fail == 0; +} diff --git a/v4/tests/test_host_quit.c b/v4/tests/test_host_quit.c index cb090ecd..37ba0c42 100644 --- a/v4/tests/test_host_quit.c +++ b/v4/tests/test_host_quit.c @@ -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") != 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");