feat(v4.0.0): a node looks before it writes; two neighbours no longer stop each other

MESH.md step 4, the fault found there, as ruled (section 7a): a node
writes only to a neighbour that is reading, keeps what it takes in
meanwhile, and loses and counts what it has no room for.

Engine: two more addresses after a node's ports -- which ports have a
neighbour waiting to write to it, which to read from it. Nucleus: (GATE)
before every message; the messages waiting, a ring of 400 cells, dealt
with when the node is idle. Of two neighbours the lower number may wait
to write (ruled after it was built); NEIGHBOUR tells a node who is on
each port.

test_host_mesh.c, 44 checks: the case that stopped the nodes passes; six
messages from each node to each at once all arrive; 800 at once, the
nodes come to rest and every message arrived or was counted (287 arrived,
714 of all kinds let go). test_fabric.c: the two looks. Both widths,
ASan+UBSan.

POST: twelve cases handed HERE, a cell address on v4, to words that take
a byte address, and so wrote into or read from the nucleus's code at cell
HERE/4. Ruled: left out, marked OPEN, until HERE and the byte words are
made to agree as its own step. POST is 538 cases.

hosted-check on three ISAs, 538/538. Bare metal: logs/20261006-134817
(amd64), -135044 (aarch64), -135440 (riscv64).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-06 13:56:57 -04:00
co-authored by Claude Opus 5.5
parent 630d03fa4e
commit f9c034cadd
21 changed files with 1015 additions and 308 deletions
+83 -46
View File
@@ -213,38 +213,60 @@ outer nodes, and for each other unit the port toward that unit's centre.
A different geometry is a different way of filling in the wiring table
and these tables. Nothing else changes.
## 7a. Two neighbours writing to each other (open; awaits a ruling)
## 7a. Two neighbours writing to each other (ruled 2026-10-06)
The fault is in section 10, step 4. Three ways out, none built:
The fault is in section 10, step 4. **Ruled: a node writes only to a
neighbour that is already reading; a node keeps the messages it takes in
meanwhile, and one there is no room for is lost and counted.**
**A. A node writes only to a neighbour that is already reading.** The
engine gives a node two more things to fetch, as the F18's `io` register
does: which ports have a neighbour writing, and which have a neighbour
reading. A node with a message to send looks first. If the neighbour is
reading, it writes, and the write does not wait. If the neighbour is
writing to it instead, it takes that message in first, and deals with it,
and looks again. A node therefore holds messages waiting to go, in its own
memory, and what it does when that room is full has to be decided (the
message is lost and counted, as one with nowhere to go is now). Writes
still block, as ruled; a node simply does not start one it cannot finish.
This is how it is done on the chip. It is all in the nucleus but for the
two fetches.
**As built.**
**B. Passing on is not the node's programme's work.** `DECOMPOSITION.md`
6.1 says "the router in each node forwards packets". Read as a thing apart
from the programme -- logic beside the F18 on the FPGA, a part of the
fabric in the model -- it takes in and sends on whatever the programme is
doing, and holds what is for the programme until it reads. The programme's
own writes then never wait on a neighbour's programme. This is more
hardware per node and takes passing-on out of F18 code, and out of what a
capsule can change.
- *The engine.* Two more addresses follow a node's ports, as the F18's
`io` register would give them: which ports have a neighbour waiting to
write to this node, and which have one waiting to read from it, a bit to
a port. Fetching them waits for nothing and changes nothing
(`v4/src/node.c`; the fabric keeps them, `v4/src/fabric.c`). A device
that takes what is written to it shows as waiting to read.
- *Looking before writing* (`(GATE)`, `v4/capsule/core.v4`). Before a node
begins any message it takes in every message a neighbour is waiting to
write to it; then it writes if the neighbour it means to write to is
waiting to read; and if not, it looks again. Once a message is begun it
is written to its end: the neighbour that took its first word reads the
rest.
- *The messages waiting* (`(MQ)`). What a node takes in is kept in a ring
of 400 cells of its own memory -- for each message its seven words, the
port it came on, and its text -- and dealt with, oldest first, when the
node has nothing else to do. A node with none waiting is blocked reading
its ports, as before. One there is no room for is read to its end, let
go, and counted in `(LOST)`.
**C. No answer to a `SEND`.** It removes the case found and leaves the
fault: any two messages crossing still stop both nodes. Not offered as a
way out; listed so that it is seen to have been weighed.
**What had to be added to the ruling, and why.** As put to Captain Bob the
rule was "write only to a neighbour that is reading, and look again if it
is not". That is not enough: two neighbours each with a message for the
other would each look, see the other not reading, and look again, for
ever. No rule that treats both ends of a wire alike can get out of that.
So one end of every wire may write without waiting for a reader, and one
only: **the node with the lower number.** A node that waits to write is
then always waiting on a higher number, so no ring of nodes can all be
waiting on each other, and the highest of any that wait is not waiting to
write: it is looking, and takes in what is being written to it.
Recommended: A. It keeps every ruling as it stands, keeps a node an F18
and nothing more, and what it adds to the engine the chip already has.
For this a node must know the number of the node on each of its ports.
Whoever wires it tells it, as it is told the ways: `NEIGHBOUR ( node port
-- )`, `v4/capsule/quit.v4`. A port it has not been told of -- a device's
-- is written to only when what is there is waiting to read. **Two nodes
wired together and not told of each other can still stop each other**,
each looking for ever; they execute, but nothing passes. Telling them is
part of wiring them. Put to Captain Bob after it was built, and ruled:
"1 is fine."
**What it costs.** A node that is flooded loses messages, of every kind:
text for it, answers to text it sent, and messages it was only passing
on. In the test three nodes send each other 800 messages at once; 287
arrive and 714 messages are let go (the count includes answers and text
that was to start a node sending). Six from each to each, at once, all
arrive. Nothing here makes a sender slow down or send again; that is
kernel-Hermes's work in v3 and is not decided for the mesh.
## 8. Storage
@@ -344,8 +366,7 @@ Each is tested, committed and pushed before the next.
input registers, not a message. A message not for this node, or not
text, is let go: passing it on is step 4.
4. **Finding the way (section 7).**
**Built 2026-10-06; NOT done: one fault stands, below, and it needs a
ruling.** Each node has a table of up to 16 destinations and the port
**Built 2026-10-06.** Each node has a table of up to 16 destinations and the port
toward each, and one port for everything else (`ROUTE ( node port -- )`,
`DEFAULT-ROUTE ( port -- )`, `NO-ROUTES`; `(PORT-FOR)` in
`v4/capsule/core.v4`). A message not for this node is written, whole,
@@ -365,23 +386,39 @@ Each is tested, committed and pushed before the next.
unchanged: `hosted-check` on three ISAs; bare metal
`logs/20261006-115225` (amd64), `-115501` (aarch64), `-115849`
(riscv64).
**THE FAULT: two neighbours that write to each other at once wait for
ever.** A write blocks until the neighbour reads (ruling, section 4.1),
and a node that is blocked writing is not reading. Passing a message on
is done here by the node's own programme, so a node cannot pass
anything on, or take anything in, while it waits to write. Found so: the
far node `SEND`s text to the middle one and then goes to send word of
how its own text ended, which goes by the middle one; the middle one has
done the text and goes to answer the far one. Each is writing the first
word of a message to the other. It is not peculiar to `SEND`: any two
messages going opposite ways on one wire at the same moment do it, and
so would a ring of nodes each writing to the next. The last check in
`test_host_mesh.c` shows it and is named KNOWN FAULT; it is there so the
fault is on record and is seen to go.
It is not a slip in the code. It is what blocking writes and a
programme that does one thing at a time give. Section 7 said nothing
about it and should have. What is to be done about it is not decided:
the choices are in section 7a.
**The fault found here, and put right 2026-10-06 (section 7a).** Two
neighbours that wrote to each other at once waited for ever: a write
blocks until the neighbour reads, and a node that is blocked writing is
not reading. Found when the far node `SEND`s text to the middle one and
then sends word of how its own text ended, which goes by the middle
one, while the middle one answers the far one. Ruled: a node writes only
to a neighbour that is reading, keeps what it takes in meanwhile, and
loses and counts what it has no room for. Built so, with one thing
added that the ruling needs (section 7a): of two neighbours the lower
number may wait to write, and `NEIGHBOUR` tells a node who is on each
port. `test_host_mesh.c`, 44 checks: the case that stopped the nodes
now passes; every node sending every other six messages at once, all
arrive; 800 at once, the nodes come to rest and every message either
arrived or was counted. `test_fabric.c`: a node sees who is waiting to
write to it and who to read from it, and looking disturbs neither.
The prompt and `EMIT` take no more of the data stack than they did.
All v4 tests at both widths and under ASan and UBSan; `hosted-check` on
three ISAs; bare metal, with lines typed at each prompt,
`logs/20261006-134817` (amd64), `-135044` (aarch64), `-135440`
(riscv64).
**Found on the way: POST was writing into the nucleus's own code.** On
v4 `HERE` is a cell address and `C!`, `FILL` and `CMOVE` take byte
addresses (D-1), so cases such as `65 HERE C!` wrote their bytes into
the nucleus at cell `HERE/4`, read them back from there, and passed.
`65 HERE C!` was watched changing cell 2223 during POST; the other
cases of the same form were not watched, and the boots committed
before this were not gone back to. It showed only when this step moved
other code onto that cell and POST stopped. Ruled: the twelve cases
are left out, marked OPEN (`v4/tools/post79_rules.py`,
`docs/v4.0.0/POST79.md` section 4), and how `HERE` and the byte words
are to agree is its own step. POST is 538 cases, not 550. `C!` and
`FILL` now have fewer cases; nothing stops any other text doing what
those cases did.
5. **Birth and Hera's requests (section 9); the unit of five.**
6. **Storage (section 8).**
7. **A second unit; scaling while running; sleep, wake and kill by
+17 -5
View File
@@ -3,7 +3,7 @@
Written by `v4/tools/mkpost.py` from `v4/tools/post79_rules.py`; do not edit.
Design: `NUCLEUS.md` section 6. The capsule is `capsules/v4/post79.4th`.
POST runs 550 cases. 443 are v3's, with what the hosted v3 binary did as the
POST runs 538 cases. 431 are v3's, with what the hosted v3 binary did as the
expected result. This file lists every other case, and why.
## 1. Rewritten for FORTH-79 (59)
@@ -60,11 +60,11 @@ standard's and not v3's.
| `CMOVE.basic` | `HERE S" Test" DUP >R HERE 10 + SWAP CMOVE CR` | stack `325`, printed `\n` | `65 PAD C! 66 PAD 1+ C! PAD PAD 10 + 2 CMOVE PAD 10 + C@ . PAD 11 + C@ . CR` | stack `empty`, prints `65 66 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and the v3 case leaves a value on the return stack |
| `TYPE.basic_string` | `HERE S" Hello" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `400`, printed `Hello\n` | `72 PAD C! 105 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `Hi\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `TYPE.numbers` | `HERE S" 12345" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `406`, printed `12345\n` | `49 PAD C! 50 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `12\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>IN.initial` | `>IN @ . CR` | stack `empty`, printed `852597488 \n` | unchanged | stack `empty`, prints `9 \n` | FORTH-79: >IN is the offset into the input, 9 here; v3 prints an address |
| `>IN.initial` | `>IN @ . CR` | stack `empty`, printed `1009040112 \n` | unchanged | stack `empty`, prints `9 \n` | FORTH-79: >IN is the offset into the input, 9 here; v3 prints an address |
| `UPDATE.no_block` | `0 SCR ! UPDATE` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for |
| `SCR.after_load` | `1 LOAD SCR @ . CR` | stack `empty`, printed `` | unchanged | stack `empty`, prints `1 \n` | v3 prints nothing after 1 LOAD; SCR is still 1 from the LIST before |
| `FIND.existing` | `FIND DUP . CR` | stack `empty`, printed `852596848 \n` | unchanged | 0 value(s) left; output not compared | FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed |
| `FIND.user_word` | `: test5 44 ; FIND test5 . CR` | stack `empty`, printed `852923888 \n` | unchanged | 0 value(s) left; output not compared | the address FIND leaves is printed |
| `FIND.existing` | `FIND DUP . CR` | stack `empty`, printed `1009039472 \n` | unchanged | 0 value(s) left; output not compared | FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed |
| `FIND.user_word` | `: test5 44 ; FIND test5 . CR` | stack `empty`, printed `1009366512 \n` | unchanged | 0 value(s) left; output not compared | the address FIND leaves is printed |
| `FIND.empty` | `FIND` | error | unchanged | stack `0`, prints nothing | FORTH-79: FIND leaves 0 when there is no word; v3 raises an error |
| `VOCABULARY.cross_vocab_access` | `VOCABULARY V1 V1 DEFINITIONS : V1WORD 11 ; FORTH V1WORD . CR` | stack `empty`, printed `11 \n` | unchanged | an error | FORTH-79: a word defined in another vocabulary is not found from FORTH; v3 finds it |
| `VOCABULARY.duplicate` | `VOCABULARY TESTVOC VOCABULARY TESTVOC` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for: defining a name again |
@@ -139,7 +139,7 @@ standard's; no v3 run stands behind them.
| `WORD.count` | `: W3 BL WORD C@ . ; W3 ABC` | stack `empty`, prints `3 ` |
| `WORD.skips_leading` | `: W4 BL WORD COUNT TYPE ; W4 X` | stack `empty`, prints `X` |
## 4. v3 cases left out (27)
## 4. v3 cases left out (39)
| Case | Why |
|---|---|
@@ -155,7 +155,19 @@ standard's; no v3 run stands behind them.
| `].basic` | v3 did not get through it in one session |
| `ELSE.double` | v3 did not get through it in one session |
| `THEN.extra` | v3 did not get through it in one session |
| `C!.basic` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.zero` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.high_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.truncation` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C@.after_cstore` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C@.zero_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `FILL.basic` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `FILL.zero_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `COUNT.string_bounds` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `CMOVE.overlap` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `CMOVE.bounds` | moves 1000 bytes over the system; v3 refuses by a bounds rule FORTH-79 has not got |
| `TYPE.single_char` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `COUNT.zero_addr_plus_one` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `WORD.empty_input` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.space_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.newline_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |