fix(v4.0.0): step 6c mended after its review -- what a stuck or gone node can and cannot hold up

The review of step 6c found it did not keep its rule.  Ruled: fix them
all; 16 hops.

- Paying a NACK no longer waits on a lower-numbered neighbour; a node
  that owes one does not sleep, and goes on taking in what it is sent.
- AWAIT looks first through the messages already waiting.
- A node waiting to write to a removed neighbour gets error 18: the
  word that says who is reading also says which ports have anything on
  them.
- A console line lets Hera go when she is blocked on the first word of
  a message (v4_fabric_interrupt_error, a device's `pending`).
- A refused answer is told to the node that waits, not the one that
  answered; one owed to the node itself goes to its own queue.
- A message is passed on by at most 16 nodes, then refused.
- A GONE is believed only when it says it is from the node's centre.
- Tests: the check that tested nothing is replaced; no room for a
  waiting sender; the limit that remains is tested as a limit.
- MESH.md 7b's rule reworded to what holds, 7b.7 says where it falls
  short; README likewise.

make -C v4 test, sanitize, hosted-check; three bare-metal boots.  POST
538 of 538, word_count=317, dict_hash=0xc0769523a47b7dc3 on all six.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 22:18:06 -04:00
co-authored by Claude Opus 5.5
parent d343001dea
commit 59727f762a
19 changed files with 1058 additions and 66 deletions
+101 -9
View File
@@ -10,6 +10,7 @@
* tells it. The console is a device on a port and speaks messages
* (v4/include/v4/message.h).
*/
#include "v4/image.h"
#include "v4/fabric.h"
#include "v4/message.h"
#include "v4/text.h"
@@ -70,10 +71,12 @@ static int console_take(void *self, v4_cell value)
}
return 1;
}
static const v4_device console = { console_take, console_give, 0 };
static int console_pending(void *self) { (void)self; return going_at < going.count; }
static const v4_device console = { console_take, console_give, 0, console_pending };
static int is_empty(const char *s) { return s[0] == 0; }
static unsigned step_cap = 20000000; /* how long a line may take before it is given up on */
/* Send text to a node and let everything that follows from it happen. */
static const char *tell(v4_cell node, const char *text)
{
@@ -82,8 +85,16 @@ static const char *tell(v4_cell node, const char *text)
ended = 0; ended_from = -1; ended_how = -1;
if (!v4_message_text(&going, node, CONSOLE_ID, V4_MSG_TEXT, text, (unsigned)strlen(text))) return "(too long)";
going_at = 0;
while (steps < 20000000 && v4_fabric_step(&f) != 0) steps++;
if (steps >= 20000000) return "(still running)";
while (steps < step_cap && v4_fabric_step(&f) != 0) steps++;
if (steps >= step_cap) {
if (getenv("V4_MESH_STUCK")) {
unsigned k;
for (k = 0; k < 3; k++) if (v4_fabric_node_at(&f, k)) { const v4_node *n = &pool[k].n;
printf(" stuck after \"%s\": node %ld p=%ld asking=%d port=%u reading=%d rport=%u mq=%ld owed=%ld lost=%ld fault=%u@%ld\n", text, (long)n->mem[ME], (long)n->p, n->asking, n->ask_port, n->reading, n->read_port,
(long)n->mem[MQ_COUNT], (long)n->mem[OWED_COUNT], (long)n->mem[LOST], n->fault_kind, (long)n->fault_addr); }
}
return "(still running)";
}
return printed;
}
@@ -298,8 +309,6 @@ int main(void)
CHECK(pool[1].n.mem[OWED_COUNT] == 8, "it owes a refusal for each of the rest, as many as it can owe: %ld", (long)pool[1].n.mem[OWED_COUNT]);
CHECK(pool[0].n.mem[REFUSED] == refused, "and has sent none yet: it is still waiting");
/* a GONE that does not come from its centre is not acted on */
CHECK(is_empty(tell(12, "12 11 GONE")) && pool[1].n.mem[AWAIT_FROM] == 12, "node 12 tells it that 12 is gone: that is not its centre speaking, and it goes on waiting");
/* one from its centre ends the wait */
CHECK(is_empty(tell(10, "12 NO-ROUTE")), "node 10 forgets the way to 12");
CHECK(strstr(tell(10, "12 11 GONE"), "Node gone") != NULL && printed_from == 11 && ended_from != -1, "node 10, its centre, tells it 12 is gone: its wait ends with an error that says so: \"%s\"", printed);
@@ -308,10 +317,8 @@ int main(void)
not_taken = 49 - got;
CHECK(got >= 30 && not_taken > 8, "it then did every message it had kept: %ld of the 49", (long)got);
CHECK(pool[0].n.mem[REFUSED] == refused + 8, "and sent the eight refusals it owed: node 10 was told of %ld", (long)(pool[0].n.mem[REFUSED] - refused));
/* two more than node 10's: node 12's word of how "12 11 GONE" ended came this way for the console while
* node 11 was full, and was not taken; nor could the refusal for it be owed */
CHECK(pool[1].n.mem[LOST] == lost + (not_taken + 1) + (not_taken + 1 - 8), "it counted each message it did not take, and each refusal it could not owe: %ld for %ld not taken",
(long)(pool[1].n.mem[LOST] - lost), (long)(not_taken + 1));
CHECK(pool[1].n.mem[LOST] == lost + not_taken + (not_taken - 8), "it counted each message it did not take, and each refusal it could not owe: %ld for %ld not taken",
(long)(pool[1].n.mem[LOST] - lost), (long)not_taken);
/* the way to 12 is forgotten: what is sent there now is refused */
refused = pool[1].n.mem[REFUSED];
CHECK(is_empty(tell(11, ": P12 S\" 1 DROP\" 12 SEND ; P12")) && pool[1].n.mem[REFUSED] == refused + 1, "having forgotten the way to 12, what node 11 sends there goes toward its centre, which has no way either, and is refused");
@@ -330,6 +337,91 @@ int main(void)
CHECK(strstr(tell(10, "5 NO-ROUTE"), "ERROR") == NULL && strcmp(tell(11, "20 22 + ."), "42 ") == 0, "forgetting the way to a node there was no way to changes nothing");
/* ---- what the review of step 6c found (MESH.md step 6c) ------------------- */
step_cap = 3000000;
v4_cell ways11 = 0;
/* a GONE is believed only from the node's centre: not by another port, and not from another node by the centre's port */
CHECK(is_empty(tell(10, "12 NO-ROUTE 12 3 ROUTE 12 3 NEIGHBOUR")) && is_empty(tell(12, "NO-ROUTES 3 DEFAULT-ROUTE 11 2 ROUTE 10 3 NEIGHBOUR")),
"node 12 is reached straight from node 10 now, and has its own wire to 11");
CHECK(is_empty(tell(11, "12 AWAIT .")) && ended == 0 && pool[1].n.mem[AWAIT_FROM] == 12, "node 11 waits for node 12's answer");
ways11 = pool[1].n.mem[ROUTE_COUNT];
CHECK(is_empty(tell(12, "12 11 GONE")) && ended == 1 && ended_from == 12 && pool[1].n.mem[AWAIT_FROM] == 12 && pool[1].n.mem[ROUTE_COUNT] == ways11,
"node 12 tells it, by the wire between them, that 12 is gone: that is not by its centre's port, and it goes on waiting");
CHECK(is_empty(tell(12, "11 NO-ROUTE 12 11 GONE")) && ended == 1 && ended_from == 12 && pool[1].n.mem[AWAIT_FROM] == 12 && pool[1].n.mem[ROUTE_COUNT] == ways11,
"node 12 says it again by way of node 10: it comes by the centre's port but is not from the centre, and node 11 goes on waiting, its ways as they were");
CHECK(strstr(tell(10, "12 11 GONE"), "Node gone") != NULL && pool[1].n.mem[AWAIT_FROM] == 0, "node 10, its centre, says it: the wait ends: \"%s\"", printed);
CHECK(is_empty(tell(11, "12 3 ROUTE")) && is_empty(tell(12, "11 2 ROUTE")), "the ways are told again");
/* an answer that came before the wait began is in the messages waiting, and is found there */
CHECK(strstr(tell(12, ": QF S\" 1 DROP\" 77 SEND 3000 0 DO LOOP S\" 1 DROP\" 10 SEND 77 AWAIT . ; QF"), "Message refused") != NULL && ended_how == V4_TEXT_ERROR
&& pool[2].n.mem[AWAIT_FROM] == 0 && waiting(far),
"node 12 sends to 77, is busy a while, sends another message -- taking in the refusal as it does -- and then waits for 77's answer: the wait ends at once: \"%s\"", printed);
CHECK(strcmp(tell(12, "7 8 * ."), "56 ") == 0, "and it goes on");
/* a waiting sender whose message found no room */
{
CHECK(is_empty(tell(11, "0 GOT !")) && is_empty(tell(11, "12 AWAIT .")) && pool[1].n.mem[AWAIT_FROM] == 12, "node 11 waits on 12 again");
CHECK(is_empty(tell(10, ": F40 40 0 DO S\" HIT\" 11 SEND LOOP ; F40")) && pool[1].n.mem[OWED_COUNT] == 0, "node 10 sends it as many messages as it has room for");
(void)tell(10, ": ONE S\" HIT\" 11 SEND 12 NO-ROUTE 12 11 GONE 11 AWAIT . ; ONE");
CHECK(strstr(printed, "Message refused") != NULL && pool[0].n.mem[AWAIT_FROM] == 0,
"it sends one more, for which there is no room, ends node 11's wait, and waits for node 11's answer: its own wait ends, the message refused: \"%s\"", printed);
CHECK(strtol(tell(11, "GOT @ ."), NULL, 10) == 40 && is_empty(tell(10, "12 3 ROUTE")) && is_empty(tell(11, "12 3 ROUTE")), "node 11 did the forty it had kept; the ways are told again");
}
/* an answer that is refused on its way back is told to the node that waits for it, not to the node that answered */
{
v4_cell r12 = pool[2].n.mem[REFUSED];
CHECK(is_empty(tell(12, "10 2 ROUTE")), "node 12 is told the way to node 10 is by node 11");
CHECK(is_empty(tell(11, "0 GOT !")) && is_empty(tell(11, "55 AWAIT .")) && pool[1].n.mem[AWAIT_FROM] == 55, "node 11 waits on a node there is not");
CHECK(is_empty(tell(10, "F40")) && pool[1].n.mem[OWED_COUNT] == 0, "node 10 sends it as many messages as it has room for");
(void)tell(10, ": RA S\" 1 DROP\" 12 SEND 3000 0 DO LOOP 55 11 GONE 12 AWAIT . ; RA");
CHECK(strstr(printed, "Message refused") != NULL && pool[0].n.mem[AWAIT_FROM] == 0,
"node 10 sends node 12 text; its answer comes back by node 11, which has no room for it; node 10 then ends node 11's wait and waits for the answer: it is told the message was refused: \"%s\"", printed);
CHECK(pool[2].n.mem[REFUSED] == r12, "node 12, which answered, is told nothing");
CHECK(strtol(tell(11, "GOT @ ."), NULL, 10) == 40 && is_empty(tell(12, "10 3 ROUTE")) && strcmp(tell(12, "7 8 * ."), "56 ") == 0, "node 11 did the forty it had kept; the way is told again");
}
/* a message that would go round for ever is refused after 16 nodes have passed it on */
{
v4_cell refused = pool[0].n.mem[REFUSED], l10 = pool[0].n.mem[LOST], l11 = pool[1].n.mem[LOST];
CHECK(is_empty(tell(10, "66 2 ROUTE")), "node 10 is told the way to 66 is by node 11, whose way for everything it has not been told of is back by node 10");
CHECK(is_empty(tell(10, ": H66 S\" 1 DROP\" 66 SEND ; H66")) && ended == 1 && ended_how == V4_TEXT_COMPLETED, "node 10 sends 66 a message: it comes to rest");
CHECK(pool[0].n.mem[LOST] + pool[1].n.mem[LOST] == l10 + l11 + 1 && pool[0].n.mem[REFUSED] == refused + 1,
"one of the two let it go when it had been passed on 16 times, and node 10 was told it was refused");
CHECK(is_empty(tell(10, "66 NO-ROUTE")) && waiting(hera) && waiting(mid) && waiting(far), "the way is forgotten, and all three are at rest");
}
/* a node that owes a refusal to a node that is stuck is not stuck itself */
CHECK(is_empty(tell(12, "NO-ROUTES 1 3 ROUTE 10 3 ROUTE 11 2 ROUTE")) && is_empty(tell(11, "55 3 ROUTE")), "node 12 has no way for 55; node 11 is told the way to 55 is by node 12");
CHECK(strcmp(tell(11, ": C1 S\" 1 DROP\" 55 SEND BEGIN 0 UNTIL ; C1"), "(still running)") == 0, "node 11 sends 55 a message and is then stuck in a loop");
CHECK(pool[2].n.mem[OWED_COUNT] == 1, "node 12 owes node 11 a refusal, and cannot give it: node 11 is not reading");
(void)tell(12, "7 8 * .");
CHECK(strcmp(printed, "56 ") == 0 && printed_from == 12, "node 12 goes on doing what it is sent: \"%s\"", printed);
/* a node that is waiting to write to a node finds out when that node is removed */
(void)tell(12, ": TO11 S\" 1 DROP\" 11 SEND 65 EMIT ; TO11");
CHECK(ended == 0, "node 12 sends node 11 a message: node 11 is not reading, and node 12 waits to write to it");
CHECK(v4_fabric_remove(&f, mid) == &pool[1], "node 11 is removed");
(void)tell(10, "1 DROP");
CHECK(strstr(printed, "No one on that port") != NULL && strstr(printed, "A") == NULL, "node 12's line ends in an error that says no one is there: \"%s\"", printed);
step_cap = 20000000;
CHECK(strcmp(tell(12, "7 8 * ."), "56 ") == 0 && pool[2].n.mem[OWED_COUNT] == 0 && waiting(far) && waiting(hera),
"it goes on, the refusal it owed node 11 let go: it owes nothing and is at rest");
/* THE LIMIT (MESH.md 7b.7): a refusal owed to a neighbour whose number is the higher is written
* without looking, as any message to it is, so a node that is stuck holds up a neighbour that owes
* it one -- until it is removed */
step_cap = 3000000;
CHECK(is_empty(tell(12, "55 3 ROUTE")), "node 12 is told the way to 55 is by node 10, which has none");
CHECK(strcmp(tell(12, ": C2 S\" 1 DROP\" 55 SEND BEGIN 0 UNTIL ; C2"), "(still running)") == 0 && pool[0].n.asking && pool[0].n.mem[OWED_COUNT] == 1,
"node 12 sends 55 a message and is then stuck in a loop: node 10 owes it a refusal, writes it, and is blocked");
v4_fabric_gone_error(&f, V4_ERROR_NO_ONE);
CHECK(v4_fabric_remove(&f, far) == &pool[2], "node 12 is removed");
step_cap = 20000000;
CHECK(strcmp(tell(10, "1 2 + ."), "3 ") == 0 && pool[0].n.mem[OWED_COUNT] == 0 && waiting(hera), "node 10 is let go, lets the refusal go, and goes on: \"%s\"", printed);
printf(" %d checks, %d failures\n", checks, failures);
return failures != 0;
}