fix(v4.0.0): a node blocked at a port with no one on it is given an error, not left there

A node writing to a node that was stuck, and was then killed, stayed
blocked for ever: killing a stuck node could cost its neighbours.  And on
the hosted and bare-metal products a write to an empty port, 5 7 PORT!,
ended the program with "the node stopped".

Now a node that can take an error, blocked writing to or reading from one
port that nothing is wired to -- nothing ever was, or what was there has
been killed or the wire cut -- has error 18, No one on that port, raised
on it and goes on.  A bare node waits as on the fabric; so does a node
whose neighbour is asleep, and one reading "any port".

test_host_unit.c: Hera kills node 14 while node 12 is blocked writing to
it.  It failed first: 12 stayed blocked.  test_fabric.c: a bare node still
waits.  hosted-check types 5 7 PORT! and goes on; it failed first too.

make -C v4 test, sanitize and hosted-check pass; amd64, aarch64 and
riscv64 boot, POST 538 of 538, dict_hash 0x5f0a949a6fc8ef2b on all six:
logs/20261007-121743, -122008, -122337.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 12:25:35 -04:00
co-authored by Claude Opus 5.5
parent 18adf59091
commit bc6deab3f5
17 changed files with 609 additions and 4 deletions
+2
View File
@@ -198,6 +198,8 @@ hosted-check: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/boot-$(i).txt) $(BINDIR)/star
@printf 'RS1\nT{\nWORDS\n' | $(BINDIR)/starforth4-amd64 > $(BINDIR)/after-post-amd64.txt
@grep -q "^ok> UNKNOWN WORD: 'RS1'$$" $(BINDIR)/after-post-amd64.txt || { echo "hosted-check: a word a POST case defined is still there"; exit 1; }
@grep -q "^ok> UNKNOWN WORD: 'T{'$$" $(BINDIR)/after-post-amd64.txt || { echo "hosted-check: POST's harness is on the node"; exit 1; }
@printf '5 7 PORT!\n1 2 + .\n' | $(BINDIR)/starforth4-amd64 > $(BINDIR)/noone-amd64.txt 2>&1; true
@grep -q '^ok> No one on that port$$' $(BINDIR)/noone-amd64.txt && grep -q '^ok> 3 ok$$' $(BINDIR)/noone-amd64.txt || { echo "hosted-check: a write to a port with no one on it was not an error the node came back from"; cat $(BINDIR)/noone-amd64.txt | tail -4; exit 1; }
@$(BINDIR)/starforth4-postfail < /dev/null > $(BINDIR)/postfail.txt; test $$? -ne 0 || { echo "hosted-check: a boot with a failing case did not fail"; exit 1; }
@grep -q '^POST FAIL: fail.wrong out<5 5 $$' $(BINDIR)/postfail.txt || { echo "hosted-check: the failing case was not named"; cat $(BINDIR)/postfail.txt; exit 1; }
@grep -q '^POST FAIL: fail.many out<QQQQ' $(BINDIR)/postfail.txt || { echo "hosted-check: the case that prints too much was not named"; exit 1; }
+1 -1
View File
@@ -20,7 +20,7 @@
\ 11 Division by zero (Q./) 12 Argument out of range
\ 13 Block out of range 14 No block is being loaded
\ 15 Deferred word not set 16 Not a word
\ 17 Storage refused
\ 17 Storage refused 18 No one on that port
\ -1 the word has printed its own message
macro SWAP over push push drop pop pop endmacro
+2 -1
View File
@@ -284,7 +284,7 @@ header ABORT
POS: -1 + if M1 -1 + if M2 -1 + if M3 -1 + if M4
-1 + if M5 -1 + if M6 -1 + if M7 -1 + if M8 -1 + if M9
-1 + if M10 -1 + if M11 -1 + if M12 -1 + if M13 -1 + if M14
-1 + if M15 -1 + if M16 -1 + if M17
-1 + if M15 -1 + if M16 -1 + if M17 -1 + if M18
drop 2 jump (DONE)
M1: drop $6167654E (EMIT4) $65766974 (EMIT4) $756F6320 (EMIT4) $746E (EMIT4) CR 2 jump (DONE)
M2: drop $20746F4E (EMIT4) $756E2061 (EMIT4) $7265626D (EMIT4) CR 2 jump (DONE)
@@ -303,6 +303,7 @@ header ABORT
M15: drop $65666544 (EMIT4) $64657272 (EMIT4) $726F7720 (EMIT4) $6F6E2064 (EMIT4) $65732074 (EMIT4) $74 (EMIT4) CR 2 jump (DONE)
M16: drop $20746F4E (EMIT4) $6F772061 (EMIT4) $6472 (EMIT4) CR 2 jump (DONE)
M17: drop $726F7453 (EMIT4) $20656761 (EMIT4) $75666572 (EMIT4) $646573 (EMIT4) CR 2 jump (DONE)
M18: drop $6F206F4E (EMIT4) $6F20656E (EMIT4) $6874206E (EMIT4) $70207461 (EMIT4) $74726F (EMIT4) CR 2 jump (DONE)
\ The table the loader gives the node: six words, one for each kind of
\ fault in the node's order, each a jump. A jump fills its word, so they
+12
View File
@@ -54,11 +54,23 @@ typedef struct {
typedef struct {
v4_place *place;
unsigned capacity;
v4_cell gone_error; /* see v4_fabric_gone_error; 0: none */
} v4_fabric;
/* An empty fabric with room for `capacity` nodes, in `places`. */
void v4_fabric_init(v4_fabric *f, v4_place *places, unsigned capacity);
/* A PORT WITH NOTHING ON IT. On the fabric a node that writes to such a
* port, or reads from it, waits for ever (docs/v4.0.0/MESH.md 4.1). That
* is so here for a bare node. But a node that can take an error is not
* left there: when it is blocked writing to, or reading from, one port that
* nothing is wired to -- because nothing ever was, or because what was
* there has been removed or the wire cut -- the error `code` is raised on
* it and it goes on (node.h, v4_node_port_gone). A node whose neighbour is
* asleep waits, and so does one reading "any port". With `code` 0, which
* is how a fabric begins, nothing is raised. */
void v4_fabric_gone_error(v4_fabric *f, v4_cell code);
/* More room: `places`, of `capacity` no less than before, takes over; every
* node keeps its number and its wiring. */
void v4_fabric_grow(v4_fabric *f, v4_place *places, unsigned capacity);
+5
View File
@@ -60,6 +60,11 @@ typedef struct {
v4_cell block_window;
} v4_image;
/* The nucleus's error for a node blocked at a port with no one on it
* (v4/capsule/core.v4's list; node.h, v4_node_port_gone): "No one on that
* port". */
#define V4_ERROR_NO_ONE 18
/* The description of the nucleus built from v4/capsule (the generated file
* defines it). */
extern const v4_image v4_capsule_image;
+10
View File
@@ -214,6 +214,16 @@ void v4_node_port_attach(v4_node *n, v4_cell base);
/* The neighbour has taken what the node wrote. */
void v4_node_port_served(v4_node *n);
/* THERE IS NO ONE ON THE PORT the node is blocked at -- writing to it, or
* reading from that one port -- and there will not be. A node that can
* take an error (it has an error register and a fault table: node.h,
* v4_node_error_attach, v4_node_fault_attach) is let go and the error
* `code` is raised on it, as if it had stored the code to its error
* register: what it was doing ends there. Returns 1. A node that cannot
* stays blocked, as on the fabric, and 0 is returned; so is 0 for a node
* that is not blocked at a port, or is reading "any port". */
int v4_node_port_gone(v4_node *n, v4_cell code);
/* The neighbour on `port` writes `value`: the node, blocked reading that
* port or "any", is unblocked and will fetch it. */
void v4_node_port_give(v4_node *n, unsigned port, v4_cell value);
+17
View File
@@ -17,9 +17,15 @@ void v4_fabric_init(v4_fabric *f, v4_place *places, unsigned capacity)
unsigned i;
f->place = places;
f->capacity = capacity;
f->gone_error = 0;
for (i = 0; i < capacity; i++) clear_place(&places[i]);
}
void v4_fabric_gone_error(v4_fabric *f, v4_cell code)
{
f->gone_error = code;
}
void v4_fabric_grow(v4_fabric *f, v4_place *places, unsigned capacity)
{
unsigned i;
@@ -196,5 +202,16 @@ unsigned v4_fabric_step(v4_fabric *f)
for (i = 0; i < f->capacity; i++)
if (awake(f, i) && f->place[i].node->n.reading && !f->place[i].node->n.given) done += hand_over_read(f, i);
/* a node blocked at one port that nothing is wired to */
if (f->gone_error != 0)
for (i = 0; i < f->capacity; i++) {
v4_node *n;
if (!awake(f, i)) continue;
n = &f->place[i].node->n;
if ((n->asking && f->place[i].wire[n->ask_port].kind == V4_WIRE_NONE)
|| (n->reading && !n->given && n->read_port != V4_PORT_ANY && f->place[i].wire[n->read_port].kind == V4_WIRE_NONE))
done += (unsigned)v4_node_port_gone(n, f->gone_error);
}
return done;
}
+10
View File
@@ -222,3 +222,13 @@ void v4_node_port_served(v4_node *n)
{
n->asking = 0;
}
int v4_node_port_gone(v4_node *n, v4_cell code)
{
if (code == 0 || n->error_reg < 0 || n->fault_vector < 0 || n->stopped) return 0;
if (n->asking) n->asking = 0;
else if (n->reading && !n->given && n->read_port != V4_PORT_ANY) n->reading = 0;
else return 0;
v4_node_store(n, n->error_reg, code); /* raises it: P is the node's handler now */
return 1;
}
+5 -1
View File
@@ -139,13 +139,17 @@ static int run_line(const v4_boot *b, const char *text, unsigned len, char *keep
}
continue;
}
if (n->asking) return V4_BOOT_LINE_STOPPED; /* a port with nothing on it */
if (n->asking) { /* a port with no one on it: an error on the node */
if (!v4_node_port_gone(n, V4_ERROR_NO_ONE)) return V4_BOOT_LINE_STOPPED;
continue;
}
if (n->reading && !n->given) {
if (sent < out.count && (n->read_port == CONSOLE_PORT || n->read_port == V4_PORT_ANY)) {
v4_node_port_give(n, CONSOLE_PORT, out.word[sent++]); /* a word of the message to the node */
continue;
}
if (n->read_port != CONSOLE_PORT && n->read_port != KERNEL_PORT && v4_node_port_gone(n, V4_ERROR_NO_ONE)) continue; /* reading one port, with no one on it */
return V4_BOOT_LINE_STOPPED; /* waiting for what will not come */
}
if (v4_image_waiting(n, b->im)) { /* the text is reading the keyboard */
+8
View File
@@ -273,6 +273,14 @@ int main(void)
CHECK(ND(c)->n.mem[OUT] == 2, "who gets its next: %ld", (long)ND(c)->n.mem[OUT]);
CHECK(ND(a)->n.asking && ND(a)->n.request == 3, "and the writer waits with its third");
CHECK(v4_fabric_unwire(&f, a, 0) && settle(100) == 0 && ND(a)->n.asking, "with the wire cut it goes on waiting");
v4_fabric_gone_error(&f, 18);
CHECK(settle(100) == 0 && ND(a)->n.asking && ND(a)->n.faults == 0, "and still does when the fabric has an error for it: a bare node cannot take one");
v4_node_error_attach(&ND(a)->n, OUT + 20);
v4_node_fault_attach(&ND(a)->n, 900);
CHECK(v4_fabric_step(&f) == 1 && !ND(a)->n.asking && ND(a)->n.fault_kind == V4_FAULT_RAISED && ND(a)->n.fault_addr == 18
&& ND(a)->n.p == 900 + (v4_cell)V4_FAULT_RAISED && ND(a)->n.mem[OUT + 20] == 18,
"a node that can take an error is let go, and the error is raised on it: its P is its handler");
CHECK(!v4_node_port_gone(&ND(c)->n, 18), "a node that is not blocked at a port is not touched");
/* ---- asleep, awake, and removed, while looping; and a node born while others run ---- */
v4_fabric_init(&f, places, 4);
+17
View File
@@ -380,6 +380,7 @@ int main(void)
/* ---- Hera is born empty, and takes in the nucleus ---- */
v4_fabric_init(&f, places, PLACES);
v4_fabric_gone_error(&f, V4_ERROR_NO_ONE);
place = host_born(0);
CHECK(place == 0, "a node is born");
hera_place = (unsigned)place;
@@ -498,6 +499,22 @@ int main(void)
CHECK(strcmp(tell(10, "(LOST) @ ."), "0 ") == 0 && strcmp(tell(12, "(LOST) @ ."), "0 ") == 0, "and no message was let go");
CHECK(waiting(10) && waiting(11) && waiting(12) && waiting(13) && waiting(14), "all five are waiting at their ports again");
/* ---- a node is killed while its neighbour is writing to it ---- */
step_limit = 3000000ul;
CHECK(strcmp(tell(14, ": SPIN BEGIN 0 UNTIL ; SPIN"), "(still running)") == 0, "node 14 is stuck in a loop that never ends");
CHECK(strcmp(tell(12, ": TO14 S\" 65 EMIT\" 14 SEND ; TO14"), "(still running)") == 0 && node_numbered(12)->asking,
"node 12, beside it, sends it text: 14 never reads, and 12 is blocked writing to it");
{
unsigned born = born_count;
const char *r = tell(10, "(P4) @ NODE-KILL 65 EMIT");
CHECK(strstr(printed, "A") != NULL && node_numbered(14) == NULL && born_count == born, "Hera kills node 14: \"%s\"", r);
}
CHECK(!node_numbered(12)->asking && !node_numbered(12)->stopped && waiting(12), "node 12 is no longer blocked: it is waiting at its ports");
CHECK(strstr(printed, "No one on that port") != NULL, "what it was doing ended in an error, which said so: \"%s\"", printed);
step_limit = 30000000ul;
CHECK(strcmp(tell(12, "12 100 * ."), "1200 ") == 0 && strcmp(tell(13, "13 100 * ."), "1300 ") == 0 && strcmp(tell(10, "1 2 + ."), "3 ") == 0, "and everything else keeps running: \"%s\"", printed);
CHECK(strstr(tell(12, ": AGAIN14 S\" 65 EMIT\" 14 SEND ; AGAIN14"), "No one on that port") != NULL && waiting(12), "a later write to where node 14 was is an error at once, not a wait: \"%s\"", printed);
printf(" %d checks, %d failures\n", checks, failures);
return failures != 0;
}