feat(v4.0.0): a node asks its kernel by a blocking write to its port

ENGINE.md step 2, the carrier.  Ruled 2026-10-05 (V3-PARITY.md 1i), on
DECOMPOSITION.md section 6: a write to a port blocks until the neighbour
reads.

- node: v4_node_port_attach, v4_node_port_served; a store to the port keeps
  the value as the request and blocks the node
- exec: a blocked node executes nothing; served, it goes on from the opcode
  after the store, in the same instruction word; a fault meanwhile abandons
  the rest of the word
- compile.v4: n KERNEL-WORD name makes a word whose body writes n to the
  port; its arguments and results are on the data stack
- boot: the kernel's words are made by handing the node text, and requests
  are served between the node's opcodes; one no one serves is error 12
- BYE, the first kernel word: hosted it leaves the program, as hosted v3;
  on the lone node it is v3's cold restart
- ENGINE.md 3a: multiuser, multitasking, preemptive and cooperative, and
  what that asks of the engine

Verified: make -C v4 test passes at both widths, with tests/test_port.c;
hosted-check passes on three ISAs; clean qemu with STARFORTH_V4=1 on amd64,
aarch64 and riscv64 passes POST with the same hashes as hosted, and a
kernel word no one serves and BYE typed at each prompt are answered
(logs/20261005-185506, -185734, -190101; -185234 is an amd64 run in which
those two lines were not typed).

Not done: v3's own C functions serving a node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-05 19:03:03 -04:00
co-authored by Claude Opus 5.5
parent 5f1f60fc84
commit 8df6c16766
21 changed files with 908 additions and 21 deletions
+30 -5
View File
@@ -87,6 +87,10 @@ static int stack_faulted(v4_node *n, unsigned op)
void v4_exec_reset(v4_exec_state *es)
{
es->anticlock = 0;
es->resume = 0;
es->resume_iw = 0;
es->resume_slot = 0;
es->resume_faults = 0;
}
void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
@@ -244,11 +248,21 @@ unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
unsigned executed = 0;
unsigned slot = 0;
/* D-14: a node that faulted with no handler has stopped; and P itself
* may be what is outside memory. */
if (n->stopped || faulted(n, n->p)) return 0;
iw = fetch_iword(n, n->p);
n->p = add_wrap(n->p, 1);
/* 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 (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 */
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);
}
while (slot < V4_SLOT_COUNT) {
unsigned op = v4_iword_op(iw, slot);
@@ -264,6 +278,17 @@ unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
}
executed++;
if (n->asking) {
/* The store was to the port. The node is blocked here; what is
* left of the word waits until it has been served. */
if (slot + 1 < V4_SLOT_COUNT) {
es->resume = 1;
es->resume_iw = iw;
es->resume_slot = slot + 1;
es->resume_faults = n->faults;
}
break;
}
if (again) {
/* Same word, slot 0, no refetch and P untouched: an `@p` or `!p`
* inside a unext loop therefore walks the words that follow, as