feat(v4.0.0): a refused message is told to its sender -- NACK and GONE

A node that has no room for a message, or no way to pass it on, lets it
go and counts it as before, and now notes that it owes its sender a NACK
(type 4: the node the message was for).  It sends what it owes when it
next has nothing else to do.  36 cells of the messages waiting are kept
for NACK and GONE, and nothing is owed for either.

A node sent a NACK counts it in (REFUSED); one waiting for an answer from
that node (AWAIT) stops, with error 19, Message refused.  A GONE (type 5)
from a node's centre makes it forget the way to the node named (NO-ROUTE,
new), and ends a wait on it with error 20, Node gone.  From anyone else
it is ignored.  GONE ( n node -- ) sends one.

test_host_mesh.c, three nodes: text for a node nobody has a way to comes
back to the console, and to a node two away, as a NACK; a waiting sender's
line ends Message refused; a node that is waiting keeps what it has room
for, owes eight refusals, counts the rest, and sends them when its wait is
ended by a GONE from its centre.  70 checks, both widths, sanitizers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 19:58:27 -04:00
co-authored by Claude Opus 5.5
parent 6e1ac157c6
commit 9efe8aad3a
5 changed files with 184 additions and 14 deletions
+7
View File
@@ -64,6 +64,9 @@
#define BLK (BVARS + 7)
#define SRC (BVARS + 8)
#define SRC_HOOK (BVARS + 9)
#define REFUSED (BUF0_W - 7) /* how many of this node's messages it has been told were refused */
#define OWED_COUNT (BUF0_W - 8) /* how many refusals this node owes and has not sent yet, at most 8 */
#define OWED (BUF0_W - 112) /* those: for each, the node it is owed to and the node the refused message was for */
#define ACL_HOOK (BUF0_W - 6) /* the xt of the access control recheck word, or 0 */
#define LINE_STATUS (BUF0_W - 9) /* how the last line ended: 0 QUIT, 1 completed, 2 an error */
#define PORT (BUF0_W - 32) /* the node's ports (node.h): V4_PORTS of them, then "any port" and "which port". Port 0 is where its requests go. */
@@ -192,6 +195,10 @@ static int host_load(v4_text *tx, v4_node *n, const char *const *files, unsigned
v4_text_constant(tx, "(AWAIT-FROM)", AWAIT_FROM);
v4_text_constant(tx, "(MQ)", MQ_W);
v4_text_constant(tx, "(MQ-END)", MQ_W + MQ_CELLS);
v4_text_constant(tx, "-MQ-ORDINARY", -(v4_cell)(MQ_CELLS - 7 - 36)); /* the same for a message that is not a refusal or a GONE: 36 cells are kept for those */
v4_text_constant(tx, "(REFUSED)", REFUSED);
v4_text_constant(tx, "(OWED#)", OWED_COUNT);
v4_text_constant(tx, "(OWED)", OWED);
v4_text_constant(tx, "-MQ-ROOM", -(v4_cell)(MQ_CELLS - 7)); /* cells taken + a text's words + this is below zero while the message fits */
v4_text_constant(tx, "(MQ-HEAD)", MQ_HEAD);
v4_text_constant(tx, "(MQ-TAIL)", MQ_TAIL);
+61 -2
View File
@@ -38,6 +38,8 @@ static unsigned printed_len;
static v4_cell printed_from; /* which node the last of it came from */
static int ended; /* how many messages of how text ended have come, */
static v4_cell ended_from, ended_how; /* and the last of them */
static int nacks; /* how many NACKs have come to the console, */
static v4_cell nack_from, nack_about; /* and the last of them: who refused, and the node the refused message was for */
static int console_give(void *self, v4_cell *value)
{
@@ -59,6 +61,10 @@ static int console_take(void *self, v4_cell value)
ended++;
ended_from = v4_message_from(&coming);
ended_how = coming.word[V4_MSG_HEADER];
} else if (v4_message_type(&coming) == V4_MSG_NACK) {
nacks++;
nack_from = v4_message_from(&coming);
nack_about = coming.word[V4_MSG_HEADER];
}
coming.count = 0;
}
@@ -177,8 +183,11 @@ int main(void)
"SEND to a node there is no way to is an error");
/* ---- no way ---- */
CHECK(is_empty(tell(99, "1 2 + .")) && ended == 0, "text for a node there is no way to: nothing comes back");
CHECK(strcmp(tell(10, "(LOST) @ ."), "1 ") == 0, "and the node that could not pass it on has counted it");
nacks = 0;
CHECK(is_empty(tell(99, "1 2 + .")) && ended == 0, "text for a node there is no way to is not done by anyone");
CHECK(nacks == 1 && nack_from == 10 && nack_about == 99, "the console, which sent it, is told it was refused, by the node that could not pass it on: %d from %ld about %ld",
nacks, (long)nack_from, (long)nack_about);
CHECK(strcmp(tell(10, "(LOST) @ ."), "1 ") == 0, "and that node has counted it");
CHECK(strcmp(tell(11, "(LOST) @ ."), "0 ") == 0 && strcmp(tell(12, "(LOST) @ ."), "0 ") == 0, "the others have lost nothing");
/* ---- waiting ---- */
@@ -258,8 +267,58 @@ int main(void)
CHECK(lost > 0, "there was not room for all of them");
CHECK(got <= sent && got + lost >= sent, "every message sent either arrived or was counted: %ld sent, %ld arrived, %ld let go", sent, got, lost);
CHECK(strcmp(tell(12, "7 8 * ."), "56 ") == 0 && printed_from == 12, "and the nodes go on as before");
CHECK(pool[0].n.mem[OWED_COUNT] == 0 && pool[1].n.mem[OWED_COUNT] == 0 && pool[2].n.mem[OWED_COUNT] == 0 && waiting(hera) && waiting(mid) && waiting(far),
"every refusal a node owed has been sent, and all three are at rest");
CHECK(pool[0].n.mem[REFUSED] + pool[1].n.mem[REFUSED] + pool[2].n.mem[REFUSED] > 0, "senders were told of what was refused: %ld %ld %ld",
(long)pool[0].n.mem[REFUSED], (long)pool[1].n.mem[REFUSED], (long)pool[2].n.mem[REFUSED]);
}
/* ---- refusals and waits (docs/v4.0.0/MESH.md 7b) ---------------------------
* No message is lost without its sender being told, and no node waits
* for ever. */
/* no way: the refusal travels back to the node that sent the message */
{
v4_cell refused = pool[2].n.mem[REFUSED], lost = pool[0].n.mem[LOST];
CHECK(is_empty(tell(12, ": N77 S\" 1 DROP\" 77 SEND ; N77")) && ended_how == V4_TEXT_COMPLETED, "node 12 sends text to node 77: it has a way for everything, toward 11");
CHECK(pool[0].n.mem[LOST] == lost + 1, "node 10, two away, has no way to 77 and counts the message");
CHECK(pool[2].n.mem[REFUSED] == refused + 1, "and node 12 is told: its count of refusals is one more");
}
/* a sender that is waiting for the answer stops waiting */
CHECK(strstr(tell(12, ": W77 S\" 1 DROP\" 77 SEND 77 AWAIT . ; W77"), "Message refused") != NULL && ended == 1 && ended_how == V4_TEXT_ERROR && ended_from == 12,
"a node that sends to 77 and waits for its answer: the wait ends, with an error that says the message was refused: \"%s\"", printed);
CHECK(strcmp(tell(12, "7 8 * ."), "56 ") == 0 && pool[2].n.mem[AWAIT_FROM] == 0, "and it goes on to its next line, waiting for no one");
/* no room: a node that is waiting keeps what it is sent, and has room for only so much */
{
v4_cell lost = pool[1].n.mem[LOST], refused = pool[0].n.mem[REFUSED], got, not_taken;
CHECK(is_empty(tell(11, "0 GOT !")) && is_empty(tell(11, "12 AWAIT .")) && ended == 0 && pool[1].n.mem[AWAIT_FROM] == 12, "node 11 waits for an answer from 12, which is not going to send one");
CHECK(is_empty(tell(10, ": F 49 0 DO S\" HIT\" 11 SEND LOOP ; F")) && ended == 1 && ended_from == 10, "node 10 sends it 49 messages meanwhile");
CHECK(pool[1].n.mem[MQ_COUNT] > 300 && pool[1].n.mem[MQ_COUNT] <= MQ_CELLS - 36, "node 11 has kept what it has room for, and left the room that is kept for refusals: %ld cells", (long)pool[1].n.mem[MQ_COUNT]);
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);
CHECK(pool[1].n.mem[AWAIT_FROM] == 0 && waiting(mid), "it is waiting for no one, and is back at its ports");
got = strtol(tell(11, "GOT @ ."), NULL, 10);
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));
/* 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");
CHECK(is_empty(tell(10, "12 2 ROUTE")) && is_empty(tell(11, "12 3 ROUTE")) && strcmp(tell(12, "7 8 * ."), "56 ") == 0, "told the way again, they reach 12 as before");
}
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");
printf(" %d checks, %d failures\n", checks, failures);
return failures != 0;
}