fix(v4.0.0): step 6e final review -- texts begun in order; a word no one will read lets its writer go; a cut wire wakes a wait for room; a full console wire is a refusal, not a fault

The final review of step 6e, by a fresh reviewer told to break it, and
what was ruled: fix them all, test first.  Each has a test that failed
before the mend.

- Two texts from one node could be begun in the wrong order when room
  for their answers came between the node's look at the first and at the
  second.  Texts whose answers go by the same port are begun in the order
  they came: (D-MASK), quit.v4.  102 of 700 timings were wrong before.
- A word written with PORT! to a node that speaks whole messages, or to
  a console's port, blocked its writer for ever and cut off every node
  beyond it.  It is error 18 now, whether that node is asleep, running
  or stuck: fabric.c, node.c.
- A node asleep for room on one wire is woken when another of its wires
  is cut.
- A line there was no room for on the lone node's wire was reported as a
  fault and ended the hosted program.  It is Message refused, and the
  prompt comes back: V4_BOOT_LINE_REFUSED.  hosted-check has the case;
  no bare-metal boot has typed it.
- Ruled: text behind held output for the same port is refused though it
  would fit.  MESH.md 7d.4 said otherwise and is corrected.
- The removal test also cuts and re-makes a wire, and has an exchange in
  which output is held at a full wire: 26,814 disturbances in all.
- MESH.md and v4/README.md no longer say that a stuck node holds up no
  other node, that only printing waits, or that no node is left waiting
  on one that is gone; 7d.6 lists the limits and what the products do
  that they did not before.

make -C v4 test and sanitize at both widths, hosted-check, lint.  Three
bare-metal boots: logs/20261013-194827 (amd64), -195131 (aarch64),
-195518 (riscv64).  POST 538 of 538, word_count=317,
dict_hash=0xe5a57f3eb72ff94a on all six; the three sessions are the same
character for character.  The boots were built with the working tree's
uncommitted change to kernel/Makefile that adds the ISA to the kernel's
version line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-13 20:02:33 -04:00
co-authored by Claude Opus 5.5
parent 06da5700bc
commit 66b26f823c
22 changed files with 1206 additions and 103 deletions
+149 -27
View File
@@ -262,35 +262,76 @@ static int answers(v4_cell node, unsigned id)
size_t len = strlen(got);
return len >= 2 && strcmp(got + len - 2, "3 ") == 0 && ended >= 1 && ended_from == node && at_rest(id);
}
/* The exchange that `text`, sent to `node`, begins is played from the mesh as kept, once for every step it
* takes, with the node at place `gone` removed at that step. Every node left must come to rest, blocked in
* nothing, and answer. With the middle node gone the far one is wired to the first, as whoever removed it
* would, so that it can be asked. How many steps the exchange takes; how many removals went wrong, and
* the first of them. */
/* AN EXCHANGE, DISTURBED AT EVERY STEP. The exchange that start() begins is played from the mesh as kept:
* once whole, to see how many steps it takes, and then once for every one of those steps with the mesh
* disturbed at that step --
* 1 node 11 removed, and node 12 then wired to node 10, as whoever removed it would, so that it can be asked;
* 2 node 12 removed;
* 3 the wire between nodes 11 and 12 cut and made again at once: what waited on it is let go.
* After each, every node left must come to rest, asleep and blocked at no port, and answer. With `moments`
* only the steps at which something the fabric keeps had changed are tried -- what is on a wire, or what a
* node asks of one: for an exchange too long to disturb at every step. A removal between two such steps
* finds the mesh as the one before it did.
* How many steps the exchange takes; how many disturbances were tried; how many went wrong, and the first. */
static v4_fabric_node now_pool[3];
static v4_place now_places[3];
static v4_wire_queue now_queues[QUEUES];
static v4_fabric now_f;
static unsigned removed_at_every_step(v4_cell node, const char *text, unsigned gone, unsigned *wrong, unsigned *first_wrong)
static v4_cell start_node;
static const char *start_text;
static int start_plain(void) { return put_text(start_node, start_text); }
static int hera_busy(void) { return pool[hera].n.mem[QUIET] == 0; }
/* Text for node 12 that prints, with node 10 made busy and the wire from node 11 to node 10 full but for 7
* cells: what node 12 prints, and how its text ended, are held by node 11, asleep for room (MESH.md 7d.4). */
static int start_held(void)
{
unsigned long before;
static v4_cell big[V4_WIRE_HEADER + V4_MSG_MAX_CHARS / 4u];
v4_wire_queue *q = places[mid].wire[2].tx;
unsigned k;
int ok = put_text(12, "A") && put_text(10, "SLOW") && run_until(hera_busy, 5000); /* node 10 has passed the one on and begun the other */
memset(big, 0, sizeof big);
big[0] = CONSOLE_ID; big[1] = 12; big[2] = V4_MSG_OUTPUT; big[3] = 16; big[6] = 1024;
for (k = 0; k < 3; k++) ok = ok && v4_wire_put(q, big, big + V4_WIRE_HEADER) == V4_WIRE_DONE;
big[6] = 884;
return ok && v4_wire_put(q, big, big + V4_WIRE_HEADER) == V4_WIRE_DONE && V4_WIRE_CELLS - q->used == 7;
}
/* what the fabric keeps, in a number: what every wire holds and what every node asks of one */
static unsigned long fabric_mark(void)
{
unsigned long h = 1469598103UL;
unsigned k;
for (k = 0; k < 3; k++) if (v4_fabric_node_at(&f, k)) h = (h ^ (unsigned long)(pool[k].n.doing * 16 + (int)pool[k].n.do_op)) * 16777619UL;
for (k = 0; k < QUEUES; k++) h = ((h ^ queues[k].used) * 16777619UL ^ queues[k].head) * 16777619UL;
return h;
}
static unsigned disturbed_at_every_step(int (*start)(void), unsigned how, int moments, unsigned *tried, unsigned *wrong, unsigned *first_wrong)
{
unsigned long before, mark, last = 0;
unsigned whole, s;
*wrong = 0; *first_wrong = 0;
*tried = 0; *wrong = 0; *first_wrong = 0;
put_back();
before = steps_run;
if (!put_text(node, text) || !run()) { *wrong = 1; return 0; }
saw_mid_hold = 0; watch = watch_mid_holds;
if (!start() || !run()) { watch = 0; *wrong = 1; return 0; }
watch = 0;
whole = (unsigned)(steps_run - before);
put_back();
if (!put_text(node, text)) { *wrong = 1; return 0; }
if (!start()) { *wrong = 1; return 0; }
for (s = 0; s <= whole; s++) {
int ok;
memcpy(now_pool, pool, sizeof pool); memcpy(now_places, places, sizeof places); memcpy(now_queues, queues, sizeof queues); now_f = f; /* the exchange, s steps on */
ok = v4_fabric_remove(&f, gone) == &pool[gone] && run();
ok = ok && at_rest(hera) && (gone == mid ? at_rest(far) : at_rest(mid));
if (ok && gone == mid) ok = v4_fabric_wire(&f, hera, 2, far, 2) && answers(12, far) && answers(10, hera);
else if (ok) ok = answers(11, mid) && answers(10, hera);
if (!ok) { if (!*wrong) *first_wrong = s; (*wrong)++; }
memcpy(pool, now_pool, sizeof pool); memcpy(places, now_places, sizeof places); memcpy(queues, now_queues, sizeof queues); f = now_f;
mark = fabric_mark();
if (!moments || s == 0 || mark != last) {
int ok;
memcpy(now_pool, pool, sizeof pool); memcpy(now_places, places, sizeof places); memcpy(now_queues, queues, sizeof queues); now_f = f; /* the exchange, s steps on */
if (how == 1) ok = v4_fabric_remove(&f, mid) == &pool[mid] && run() && at_rest(hera) && at_rest(far)
&& v4_fabric_wire(&f, hera, 2, far, 2) && answers(12, far) && answers(10, hera);
else if (how == 2) ok = v4_fabric_remove(&f, far) == &pool[far] && run() && at_rest(hera) && at_rest(mid) && answers(11, mid) && answers(10, hera);
else ok = v4_fabric_unwire(&f, mid, 3) && v4_fabric_wire(&f, mid, 3, far, 2) && run() && at_rest(hera) && at_rest(mid) && at_rest(far)
&& answers(12, far) && answers(11, mid) && answers(10, hera);
(*tried)++;
if (!ok) { if (!*wrong) *first_wrong = s; (*wrong)++; }
memcpy(pool, now_pool, sizeof pool); memcpy(places, now_places, sizeof places); memcpy(queues, now_queues, sizeof queues); f = now_f;
}
last = mark;
(void)v4_fabric_step(&f); (void)console_takes();
}
return whole;
@@ -995,32 +1036,113 @@ int main(void)
CHECK(between_data <= 4 && between_ret <= 7, "between texts: four cells of the data stack, and seven return entries");
/* ---- A NODE REMOVED AT EVERY STEP (MESH.md 7d.7, acceptance 6) ----
* Three exchanges, each played once whole and then once for every step it takes, with node 11 removed
* at that step, and again with node 12: the nodes left come to rest, blocked in nothing, and answer.
/* ---- A WORD WRITTEN TO A PORT, PORT!, HOLDS NO ONE (found by the final review of step 6e) ----
* A node's neighbours speak whole messages and are asleep at their wires; so is its console. A word
* written to one of them will never be read. The line that wrote it ends in an error and the node goes
* on; before, it was blocked for ever, and every node beyond it was cut off. */
CHECK(row(), "a row made afresh");
step_cap = 2000000;
CHECK(strstr(tell(10, "5 2 PORT! 65 EMIT"), "No one on that port") != NULL && strchr(printed, 'A') == NULL && ended == 1 && ended_how == V4_TEXT_ERROR && asleep(hera),
"node 10 writes a word to node 11's port: its line ends No one on that port: \"%s\"", printed);
CHECK(strcmp(tell(10, "1 2 + ."), "3 ") == 0 && strcmp(tell(11, "20 22 + ."), "42 ") == 0 && strcmp(tell(12, "7 8 * ."), "56 ") == 0, "and all three answer");
CHECK(strstr(tell(10, "5 1 PORT!"), "No one on that port") != NULL && ended_how == V4_TEXT_ERROR && strcmp(tell(10, "1 2 + ."), "3 ") == 0,
"the same for a word written to the console's port: \"%s\"", printed);
CHECK(strstr(tell(11, "5 3 PORT!"), "No one on that port") != NULL && ended_from == 11 && strcmp(tell(12, "7 8 * ."), "56 ") == 0 && strcmp(tell(11, "20 22 + ."), "42 ") == 0,
"and for node 11 writing to node 12's: node 12 is not cut off");
stuck = 1u << mid;
(void)tell(11, ": C BEGIN 0 UNTIL ; C");
CHECK(ended == 0 && !asleep(mid), "node 11 is stuck in a loop: it does not sleep");
CHECK(strstr(tell(10, "5 2 PORT!"), "No one on that port") != NULL && ended_how == V4_TEXT_ERROR && strcmp(tell(10, "1 2 + ."), "3 ") == 0,
"a word node 10 writes to it is refused all the same, and node 10 can be typed to: \"%s\"", printed);
stuck = 0;
step_cap = 20000000;
/* ---- TWO TEXTS FROM ONE NODE ARE BEGUN IN THE ORDER THEY CAME (acceptance 8; found by the final review) ----
* Node 12 has two texts from node 10 on its wire, and the wire their answers go by has 7 cells free:
* neither can be begun. Node 11, which was not taking, then takes a message off that wire -- at every
* step in turn, so that the room comes before node 12 looks at the first text, between its look at the
* first and at the second, and after both. Whenever it comes, the first is begun first. */
{
static v4_cell big[V4_WIRE_HEADER + V4_MSG_MAX_CHARS / 4u], hd[V4_WIRE_HEADER];
static v4_message m;
unsigned k, wrong = 0, first_wrong = 0, undone = 0;
v4_wire_queue *out, *in;
CHECK(row() && told(12, ": X1 65 EMIT ; : X2 66 EMIT ;"), "a row made afresh; node 12 has two words that print");
v4_fabric_sleep(&f, mid); /* node 11 takes nothing for now */
out = places[far].wire[2].tx; in = places[far].wire[2].rx;
memset(big, 0, sizeof big);
big[0] = 1; big[1] = 12; big[2] = V4_MSG_OUTPUT; big[3] = 16;
big[6] = 1024; for (i = 0; i < 3; i++) (void)v4_wire_put(out, big, big + V4_WIRE_HEADER);
big[6] = 884; (void)v4_wire_put(out, big, big + V4_WIRE_HEADER);
CHECK(V4_WIRE_CELLS - out->used == 7, "the wire from node 12 to node 11 has 7 cells free: %u", V4_WIRE_CELLS - out->used);
CHECK(v4_message_text(&m, 12, 10, V4_MSG_TEXT, "X1", 2) && (m.word[3] = 16, m.word[5] = 1, v4_wire_put(in, m.word, m.word + V4_MSG_HEADER) == V4_WIRE_DONE)
&& v4_message_text(&m, 12, 10, V4_MSG_TEXT, "X2", 2) && (m.word[3] = 16, m.word[5] = 2, v4_wire_put(in, m.word, m.word + V4_MSG_HEADER) == V4_WIRE_DONE),
"two texts from node 10 are on the wire to node 12, numbered 1 and 2");
keep();
for (k = 0; k < 700; k++) {
unsigned began = 0, seq[2] = { 0, 0 };
put_back();
for (i = 0; i < 100000 && began < 2; i++) {
const v4_node *n = &pool[far].n;
int kept = n->mem[KEEP] >= 0;
if (i == k) { v4_wire_first(out); (void)v4_wire_look(out, hd); (void)v4_wire_drop(out); }
(void)v4_fabric_step(&f); (void)console_takes();
if (!kept && n->mem[KEEP] >= 0) seq[began++] = (unsigned)n->mem[MSG + 5];
}
if (began < 2) undone++;
else if (seq[0] != 1 || seq[1] != 2) { if (!wrong) first_wrong = k; wrong++; }
}
CHECK(undone == 0 && wrong == 0, "whenever the room came, both texts were done, the first first: %u not done, %u in the wrong order, the first when it came at step %u", undone, wrong, first_wrong);
}
/* ---- A NODE REMOVED, OR A WIRE CUT, AT EVERY STEP (MESH.md 7d.7, acceptance 6) ----
* Three exchanges, each played once whole and then once for every step it takes with node 11 removed at
* that step, again with node 12 removed, and again with the wire between them cut and made again: the
* nodes left come to rest, blocked at nothing, and answer.
* What is typed is a word defined beforehand, and so is what a node left is asked (Q, which is
* `1 2 + .`): a line of several words is mostly the dictionary searched, thousands of steps in which the
* mesh does nothing, and every one of them would be a removal played to the end. */
* mesh does nothing, and every one of them would be a removal played to the end.
* WHAT "ANSWERS" MEANS HERE. A node that was waiting for an answer from a node now removed, or for one
* that was on a wire now cut, is still waiting when the mesh comes to rest: removing a node tells no one
* (Hera's KILL does: test_host_unit.c). Its wait is ended by the line it is then typed, "Interrupted",
* and it answers. */
{
static const struct { const char *what, *set10, *set11, *set12; v4_cell node; const char *text; } ex[3] = {
{ "text from the console for node 12, what it prints and how it ended", "1 DROP", "1 DROP", ": A 7 8 * . ;", 12, "A" },
{ "node 11 SENDs node 12 text and waits for its answer", "1 DROP", ": T S\" Q\" 12 SEND 12 AWAIT . ;", "1 DROP", 11, "T" },
{ "a message node 12 has no way for, and its NACK to node 10", "55 2 ROUTE : N S\" 1 DROP\" 55 SEND ;", "55 3 ROUTE", "NO-ROUTES 1 2 ROUTE 10 2 ROUTE 11 2 ROUTE", 10, "N" }
};
unsigned e, g, whole, wrong, first;
static const char *const how_name[4] = { "", "node 11 removed", "node 12 removed", "the wire between nodes 11 and 12 cut and made again" };
unsigned e, g, whole, tried, wrong, first, all = 0;
step_cap = 2000000;
for (e = 0; e < 3; e++) {
CHECK(row() && told(10, ex[e].set10) && told(11, ex[e].set11) && told(12, ex[e].set12) && told(10, ASK) && told(11, ASK) && told(12, ASK), "a row made afresh for: %s", ex[e].what);
step_cap = 2000000;
keep();
for (g = 1; g <= 2; g++) {
whole = removed_at_every_step(ex[e].node, ex[e].text, g == 1 ? mid : far, &wrong, &first);
CHECK(whole > 0 && wrong == 0, "%s, in %u steps: with node %u removed at each of them the rest come to rest and answer: %u did not, the first at step %u",
ex[e].what, whole, 10u + g, wrong, first);
start_node = ex[e].node; start_text = ex[e].text;
for (g = 1; g <= 3; g++) {
whole = disturbed_at_every_step(start_plain, g, 0, &tried, &wrong, &first);
all += tried;
CHECK(whole > 0 && wrong == 0, "%s, in %u steps: with %s at each of them the rest come to rest and answer: %u did not, the first at step %u",
ex[e].what, whole, how_name[g], wrong, first);
}
}
put_back();
CHECK(is_empty(tell(10, "N")) && pool[hera].n.mem[REFUSED] == 1 && pool[far].n.mem[LOST] == 1, "the last of them, whole: node 12 let the message go and node 10 was told it was refused");
/* and with what node 12 prints held at node 11, which sleeps for room: at every step at which
* anything on a wire, or what a node asks of one, has changed */
CHECK(row() && told(12, ": A 7 8 * . ;") && told(10, ": SLOW 3000 0 DO LOOP ;") && told(10, ASK) && told(11, ASK) && told(12, ASK), "a row made afresh for output held at a full wire");
step_cap = 2000000;
keep();
for (g = 1; g <= 3; g++) {
whole = disturbed_at_every_step(start_held, g, 1, &tried, &wrong, &first);
all += tried;
CHECK(whole > 0 && saw_mid_hold > 0, "text for node 12 that prints, node 10 busy and the wire to it full: node 11 held what was printed, asleep for room, for %u of %u steps", saw_mid_hold, whole);
CHECK(tried > 20 && wrong == 0, "with %s at each of the %u steps at which a wire or a node's asking changed, the rest come to rest and answer: %u did not, the first at step %u",
how_name[g], tried, wrong, first);
}
printf(" a node removed or a wire cut: %u times in all\n", all);
step_cap = 20000000;
}