feat(v4.0.0): nodes that talk -- ports, a node born empty, and the fabric

MESH.md step 1, in the engine, which knows nothing of StarForth or of any
kernel.

- node: V4_PORTS ports (8), a build parameter; "any port" and the port the
  last such read came from; a read blocks until the neighbour writes, as a
  write blocks until the neighbour reads; v4_node_born: empty, P at "any
  port"
- exec: a fetch from a port -- @ @b @+ @p, or of an instruction word when P
  is a port -- waits for a word; a node executes what arrives at a port
  without advancing P; a blocked node goes on from the slot it stopped at
- fabric: the nodes there are and the table of how their ports are wired,
  both changed while the nodes run; devices on a port; asleep and awake; a
  step is every unblocked node executing one instruction word, then every
  write with a reader waiting being handed over
- DECOMPOSITION.md section 6: four named ports withdrawn for V4_PORTS
  numbered ones and wiring as data, as ruled

Verified: tests/test_fabric.c, 53 checks at both widths: two nodes exchange
words; an empty node is filled through its port by a device, and by another
node, and runs what it was sent; a word is passed on by a node in between;
a waiting node executes nothing; the wiring is changed while they run; a
node is put to sleep, woken and removed while looping; a node is born while
others run; the fabric is given more room.  make -C v4 test and make -C v4
sanitize pass.  The single-node products are unchanged: hosted-check on
three ISAs, and clean qemu with STARFORTH_V4=1 on amd64, aarch64 and
riscv64 with lines typed at each prompt (logs/20261006-074907, -075150,
-075532).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-06 07:58:00 -04:00
co-authored by Claude Opus 5.5
parent 2c5427d9c1
commit 9af442f793
17 changed files with 1170 additions and 39 deletions
+31 -4
View File
@@ -143,20 +143,30 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
case V4_OP_FETCH_P:
if (faulted(n, n->p)) return;
if (v4_node_port_index(n, n->p) >= 0) {
/* P is a port: the literal is the next word its neighbour
* writes, and P stays where it is */
if (!v4_node_read_ready(n, n->p)) return;
v4_dstack_push(ds, v4_node_fetch(n, n->p));
break;
}
v4_dstack_push(ds, v4_node_load(n, n->p));
n->p = add_wrap(n->p, 1);
break;
case V4_OP_FETCH_INC:
if (faulted(n, n->a)) return;
if (!v4_node_read_ready(n, n->a)) return; /* a port with nothing written to it yet */
v4_dstack_push(ds, v4_node_fetch(n, n->a));
n->a = add_wrap(n->a, 1);
break;
case V4_OP_FETCH_B:
if (faulted(n, n->b)) return;
if (!v4_node_read_ready(n, n->b)) return; /* a port with nothing written to it yet */
v4_dstack_push(ds, v4_node_fetch(n, n->b));
break;
case V4_OP_FETCH_A:
if (faulted(n, n->a)) return;
if (!v4_node_read_ready(n, n->a)) return; /* a port with nothing written to it yet */
v4_dstack_push(ds, v4_node_fetch(n, n->a));
break;
case V4_OP_STORE_P:
@@ -250,18 +260,25 @@ unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
/* D-14: a node that faulted with no handler has stopped. A node that
* has written to its port is blocked until it has been served. */
if (n->stopped || n->asking) return 0;
if (n->stopped || n->asking || n->reading) return 0;
if (es->resume && es->resume_faults != n->faults) es->resume = 0; /* it faulted while blocked: P is the handler */
if (es->resume) {
/* served: the rest of the word the store was in */
/* unblocked: the rest of the word it was in */
es->resume = 0;
iw = es->resume_iw;
slot = es->resume_slot;
} else {
/* P itself may be what is outside memory */
if (faulted(n, n->p)) return 0;
iw = fetch_iword(n, n->p);
n->p = add_wrap(n->p, 1);
if (v4_node_port_index(n, n->p) >= 0) {
/* P is a port: the node executes what its neighbour writes
* there, and P stays where it is */
if (!v4_node_read_ready(n, n->p)) return 0;
iw = (v4_iword)((v4_ucell)v4_node_fetch(n, n->p) & 0xFFFFFFFFu);
} else {
iw = fetch_iword(n, n->p);
n->p = add_wrap(n->p, 1);
}
}
while (slot < V4_SLOT_COUNT) {
@@ -271,6 +288,16 @@ unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
unsigned faults = n->faults;
v4_exec_op(n, es, h, op, iw, slot);
if (n->reading) {
/* The fetch was from a port with nothing written to it yet. The
* opcode has not executed: the node waits here, and this slot
* is where it goes on from. */
es->resume = 1;
es->resume_iw = iw;
es->resume_slot = slot;
es->resume_faults = n->faults;
break;
}
if (n->faults != faults) { /* P is the handler now, or the node has stopped */
/* a store that raised an error (D-18) did execute; any other fault means the opcode did not */
if (n->fault_kind == V4_FAULT_RAISED) executed++;