diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md
index d36fc6c4..4e491847 100644
--- a/capsules/BLOCK_MAP.md
+++ b/capsules/BLOCK_MAP.md
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
-
+
@@ -30,6 +30,7 @@
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | n/a |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | n/a |
| `v4:forth79.4th` | 6000, 6001, 6002 | `0x4055641ee17d176b` | n/a |
+| `v4:hera.4th` | 8000, 8001, 8002, 8003, 8004 | `0x93a3160c447d7641` | n/a |
| `v4:post79.4th` | 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 7036, 7037, 7038, 7039, 7040, 7041, 7042, 7043, 7044, 7045, 7046, 7047, 7048, 7049, 7050, 7051, 7052, 7053, 7054, 7055, 7056, 7057, 7058, 7059, 7060, 7061, 7062, 7063 | `0xb26d1b974884179d` | n/a |
| `workload-0.4th` | 2200, 2201 | `0x93f86f60aeba8feb` | n/a |
| `workload-1-lite.4th` | 5058, 5059 | `0x44a7a7e3176dcc8d` | n/a |
@@ -435,10 +436,15 @@
| 7061 | `v4:post79.4th` | `0xb26d1b974884179d` | ok |
| 7062 | `v4:post79.4th` | `0xb26d1b974884179d` | ok |
| 7063 | `v4:post79.4th` | `0xb26d1b974884179d` | ok |
+| 8000 | `v4:hera.4th` | `0x93a3160c447d7641` | ok |
+| 8001 | `v4:hera.4th` | `0x93a3160c447d7641` | ok |
+| 8002 | `v4:hera.4th` | `0x93a3160c447d7641` | ok |
+| 8003 | `v4:hera.4th` | `0x93a3160c447d7641` | ok |
+| 8004 | `v4:hera.4th` | `0x93a3160c447d7641` | ok |
## Conflicts
None.
---
-*38 capsule(s) scanned. Re-run `mkcapsule --manifest
` to refresh.*
+*39 capsule(s) scanned. Re-run `mkcapsule --manifest ` to refresh.*
diff --git a/capsules/v4/hera.4th b/capsules/v4/hera.4th
new file mode 100644
index 00000000..adc3a8d8
--- /dev/null
+++ b/capsules/v4/hera.4th
@@ -0,0 +1,80 @@
+Block 8000
+( hera.4th -- Hera: the node that has the others born and )
+( manages them. docs/v4.0.0/MESH.md sections 9 and 10. )
+( She asks whoever holds the fabric for a node, wires it, )
+( and sends it its capsules through the port that joins )
+( them. The unit rule is here and nowhere else: four outer )
+( nodes round a centre, an outer node wired only inside its )
+( unit. )
+( Text for another node is built in PAD. )
+VARIABLE (T#)
+: (T0) ( -- ) 0 (T#) ! ;
+: (TC) ( c -- ) PAD (T#) @ + C! 1 (T#) +! ;
+: (TS) ( baddr u -- )
+ DUP IF 0 DO DUP I + C@ (TC) LOOP DROP ELSE DROP DROP THEN ;
+: (TN) ( n -- ) 0 <# #S #> (TS) 32 (TC) ;
+: (T) ( -- baddr u ) PAD (T#) @ ;
+Block 8001
+( The node being born: its place in the fabric, the number )
+( it is to have, and the port of this node that leads to it. )
+VARIABLE (KID) VARIABLE (KID#) VARIABLE (KIDP)
+: (FAIL) ( -- )
+ ." BIRTH: node " (KID#) @ . ." did not do as it was told" CR
+ -1 NODE-ERROR ! ;
+: (TOLD) ( how -- ) 1 - IF (FAIL) THEN ;
+: (TELL) ( baddr u -- ) (KID#) @ SEND (KID#) @ AWAIT (TOLD) ;
+( the nucleus, a capsule of F18 code, cell by cell to its port )
+: (NUCLEUS) ( -- )
+ S" v4:nucleus-64.f18" CAPSULE-OPEN 0= IF (FAIL) THEN
+ BEGIN CAPSULE-CELL WHILE (KIDP) @ PORT! REPEAT DROP ;
+( a capsule of FORTH blocks, a line at a time )
+: (LINES) ( baddr u -- ) CAPSULE-OPEN 0= IF (FAIL) THEN
+ BEGIN PAD CAPSULE-LINE DUP 0< 0= WHILE PAD SWAP (TELL) REPEAT
+ DROP ;
+Block 8002
+( who it is, where its printing goes, and that everything )
+( not told of goes by its port 2, where this node is )
+: (WHO) ( -- )
+ (T0) (KID#) @ (TN) S" (ME) ! " (TS)
+ (CONSOLE) @ (TN) S" (CONSOLE) ! 2 DEFAULT-ROUTE " (TS)
+ (ME) @ (TN) S" 2 NEIGHBOUR" (TS)
+ (T) 0 (KIDP) @ SEND-ON (KID#) @ AWAIT (TOLD) ;
+( number port -- place A NODE IS BORN on that port of )
+( this one: empty, then the nucleus, then FORTH-79 and POST. )
+: BIRTH ( number port -- place )
+ (KIDP) ! (KID#) !
+ NODE-BORN DUP 0< IF (FAIL) THEN (KID) !
+ NODE-ME (KIDP) @ (KID) @ 2 NODE-WIRE 0= IF (FAIL) THEN
+ (NUCLEUS)
+ (KID#) @ (KIDP) @ ROUTE (KID#) @ (KIDP) @ NEIGHBOUR (WHO)
+ S" v4:forth79.4th" (LINES) S" v4:post79.4th" (LINES)
+Block 8003
+ S" (SEAL)" (TELL)
+ (KID) @ (KID#) @ NODE-PARITY
+ (KID) @ ;
+( Two outer nodes are joined: port 3 of X to port 4 of Y, )
+( and each is told the other is there. )
+VARIABLE (XP) VARIABLE (XN) VARIABLE (YP) VARIABLE (YN)
+: (X) ( place number -- ) (XN) ! (XP) ! ;
+: (Y) ( place number -- ) (YN) ! (YP) ! ;
+: (SAY) ( them port me -- ) (KID#) !
+ (T0) OVER (TN) DUP (TN) S" ROUTE " (TS)
+ SWAP (TN) (TN) S" NEIGHBOUR" (TS) (T) (TELL) ;
+: (JOIN) ( -- )
+ (XP) @ 3 (YP) @ 4 NODE-WIRE 0= IF (FAIL) THEN
+ (YN) @ 3 (XN) @ (SAY) (XN) @ 4 (YN) @ (SAY) ;
+Block 8004
+( n -- A UNIT: four outer nodes, n+1 to n+4, on ports 2 )
+( to 5 of this one, and joined in a square: 1-2, 2-4, 4-3, )
+( 3-1. The corners across from each other are not wired: )
+( what one sends the other goes by this node. )
+VARIABLE (U)
+VARIABLE (P1) VARIABLE (P2) VARIABLE (P3) VARIABLE (P4)
+: (N) ( k -- number ) (U) @ + ;
+: UNIT ( n -- ) (U) !
+ 1 (N) 2 BIRTH (P1) ! 2 (N) 3 BIRTH (P2) !
+ 3 (N) 4 BIRTH (P3) ! 4 (N) 5 BIRTH (P4) !
+ (P1) @ 1 (N) (X) (P2) @ 2 (N) (Y) (JOIN)
+ (P2) @ 2 (N) (X) (P4) @ 4 (N) (Y) (JOIN)
+ (P4) @ 4 (N) (X) (P3) @ 3 (N) (Y) (JOIN)
+ (P3) @ 3 (N) (X) (P1) @ 1 (N) (Y) (JOIN) ;
diff --git a/capsules/v4/nucleus-64.f18 b/capsules/v4/nucleus-64.f18
index 84d109da..3034f2bf 100644
Binary files a/capsules/v4/nucleus-64.f18 and b/capsules/v4/nucleus-64.f18 differ
diff --git a/docs/v4.0.0/MESH.md b/docs/v4.0.0/MESH.md
index ec36bd20..f38ec5be 100644
--- a/docs/v4.0.0/MESH.md
+++ b/docs/v4.0.0/MESH.md
@@ -289,11 +289,11 @@ checked in this step.
## 9. Birth, and Hera
-**Proposal.** Hera asks, through the port where her requests go
-(`ENGINE.md` 3.3), for a node to be added and wired; she then sends the
-newborn its capsules through the port that joins them. Putting a node to
-sleep, waking it, and removing it are requests of the same kind. Only
-Hera's requests are honoured.
+**Proposal, built as step 5 (section 10).** Hera asks, through the port
+where her requests go (`ENGINE.md` 3.3), for a node to be added and wired;
+she then sends the newborn its capsules through the port that joins them.
+Putting a node to sleep, waking it, and removing it are requests of the
+same kind. Only Hera's requests are honoured.
The unit rule (ruling 5) is Hera's, in capsule code: it is what she does
with those requests. The engine and the host do not know it.
@@ -420,6 +420,62 @@ Each is tested, committed and pushed before the next.
`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.**
+ **Done 2026-10-06, in the fabric under test; the products are still one
+ node each until steps 8 and 9.** Section 9's proposal, as built:
+ - *What Hera asks* (`v4/include/v4/manage.h`, `v4/src/manage.c`). Eleven
+ requests, each a word on her made with `KERNEL-WORD`, written to the
+ port her requests go to: `NODE-ME`, `NODE-BORN`, `NODE-WIRE`,
+ `NODE-UNWIRE`, `NODE-SLEEP`, `NODE-WAKE`, `NODE-KILL`, `NODE-PARITY`;
+ and `CAPSULE-OPEN`, `CAPSULE-CELL`, `CAPSULE-LINE`, by which she reads
+ a capsule the host has found and checked. A node is named to the host
+ by its place in the fabric; its number, which messages go by, is the
+ nodes' own. Only the node the host has wired to this is answered.
+ - *What a node needs to do it* (`v4/capsule/quit.v4`): `PORT! ( w port
+ -- )`, a cell written to a port as it is; `SEND-ON ( baddr u node port
+ -- )`, text by a port named, for a neighbour with no number yet;
+ `AWAIT ( node -- how )`, blocked until that node's word of how text
+ ended comes, keeping every other message for later; `(SEAL)`, what is
+ in the dictionary now is the system.
+ - *Hera's capsule* (`capsules/v4/hera.4th`, blocks 8000 to 8004), FORTH.
+ `BIRTH ( number port -- place )`: a node is asked for and wired to
+ that port; the nucleus is sent it cell by cell; it is told its number,
+ its console and that Hera is on its port 2; FORTH-79 and POST are sent
+ it a line at a time, each waited for; it seals; its parity is
+ recorded. `UNIT ( n -- )`: four births, on ports 2 to 5, and the four
+ joined in a square, each told of the two beside it. The unit rule is
+ there and nowhere else.
+ `v4/tests/test_host_unit.c`, 34 checks, 64-bit: Hera is born empty and
+ takes the nucleus through her port, then FORTH-79, POST (538 of 538) and
+ her capsule from the console; `10 UNIT`; four nodes are born, each
+ passes POST, the host records four parities with one dictionary hash;
+ all five wait and execute nothing; text for each outer node is done
+ there and what it prints comes back; `COLD` on one comes back to the
+ nucleus and FORTH-79; one outer node sends text to the one beside it,
+ and to the one across from it by way of Hera. Acceptance 1 and 2, in
+ the fabric. All v4 tests at both widths and under ASan and UBSan. The
+ single-node products are unchanged but for the nucleus's new words:
+ `hosted-check` on three ISAs; bare metal `logs/20261006-143934`
+ (amd64), `-144204` (aarch64), `-144601` (riscv64).
+ **What stands in, in the test only:** the capsules are read from the
+ files the build bakes in, and the nucleus capsule is written from the
+ nucleus as the test assembles it, not found in the baked directory with
+ its hash and signature checked; that is the host's part and comes with
+ step 8. Each node has a disk of its own attached at birth, as the boot's
+ one node has; storage through the ports is step 6.
+ **Not done, and open:**
+ - The test is not run on 32-bit cells: the POST capsule's expected
+ results are 64-bit v3's and the nucleus capsule is `nucleus-64`. POST
+ has never been run at 32 bits.
+ - A node that never answers leaves Hera waiting for ever in `AWAIT`;
+ she cannot then kill it. What she does about a birth that does not
+ finish is not decided.
+ - What an outer node prints while Hera is giving birth waits in Hera's
+ 400 cells until she is idle, and is lost if there is more than they
+ hold. POST prints one line.
+ - An outer node has no port to the kernel, so no kernel words: no
+ `BYE`, and nothing it asks is honoured, as ruled.
+ - The suite now takes about four minutes, eight under the sanitizers:
+ five POSTs.
6. **Storage (section 8).**
7. **A second unit; scaling while running; sleep, wake and kill by
command.**
diff --git a/logs/20261006-143934/amd64/qemu-amd64-20261006-143934.log b/logs/20261006-143934/amd64/qemu-amd64-20261006-143934.log
new file mode 100644
index 00000000..0ef01733
--- /dev/null
+++ b/logs/20261006-143934/amd64/qemu-amd64-20261006-143934.log
@@ -0,0 +1,130 @@
+[2J[001;001H[=3h[2J[001;001H[2J[001;001H[8;042;160t[2J[001;001H[2J[001;001H[8;056;240t[2J[001;001HBdsDxe: loading Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
+BdsDxe: starting Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
+[2J[001;001H[=3h[2J[001;001HStarKernel UEFI Loader
+Loading kernel from ESP...
+[CKPT 001] Entered efi_main - ConOut live
+RAW SERIAL UP
+[CKPT 002] Serial (COM1) initialized
+Monolithic build - kernel linked directly
+Collecting boot information...
+CmdLine: parsed OK
+[CKPT 004] Command line parsed
+[CKPT 005] Kernel stack allocation decided
+[CKPT 006] Boot info collected (ACPI table located)
+GOP: linear framebuffer found
+[CKPT 007] GOP: linear framebuffer found
+[CKPT 008] About to enter ExitBootServices retry loop
+EBS...
+EBS OK
+Calling kernel_main (monolithic)...
+
+
+ _____ _ _ __ _
+ / ____| | | |/ / | |
+ | (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
+ \___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
+ ____) | || (_| | | | . \ __/ | | | | | __/ |
+ |_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
+
+LithosAnanke v2.1.0
+Architecture: amd64
+Build: Oct 6 2026 14:39:07
+
+UEFI BootServices: EXITED
+
+=== StarKernel Boot Information ===
+Memory map entries: 131
+Total memory: 1023 MB
+Usable memory: 968 MB
+===================================
+
+PMM initialized.
+PMM statistics:
+ Total pages: 249251
+ Free pages : 247728
+ Used pages : 1523
+ Total MB : 973
+ Free MB : 967
+ Used MB : 5
+
+VMM initialized (mapped RAM, CR3 switched)
+VMM initialized (mapped RAM, CR3 switched)
+VMM self-test: mapped OK at 0xffff800000000000
+VMM self-test complete.
+
+IDT installed.
+
+APIC: init...
+APIC: IA32_APIC_BASE MSR=0x00000000fee00900
+APIC: stale-ISR drain: 0 EOI(s) issued
+APIC: initialized (xAPIC MMIO, TPR=0, SIVR=0x1FF, EOI-clear)
+APIC: init done
+
+I/O APIC: init...
+I/O APIC: base=0xfec00000, gsi_base=0, overrides=5
+ override: source=00 gsi=2 flags=0x0000
+ override: source=05 gsi=5 flags=0x000d
+ override: source=09 gsi=9 flags=0x000d
+ override: source=0a gsi=10 flags=0x000d
+ override: source=0b gsi=11 flags=0x000d
+i8042: keyboard ACKed enable-scanning
+i8042: IRQ1 enabled in controller config byte
+I/O APIC: keyboard IRQ1 routed (masked)
+
+Timer: init...
+Timer: init start
+Timer: PM_TMR_BLK discovered from FADT at port 1544
+Timer: VM mode detected (hypervisor present).
+Timer: HPET calibration disabled (VM-exit MMIO would poison timing).
+Timer: WARNING: invariant TSC not present under hypervisor.
+Timer: continuing in RELATIVE mode (no determinism guarantees).
+Timer: RDTSCP not present; using RDTSC (less serialized).
+Timer: CPUID frequency unavailable; trying PM Timer...
+Timer: trust=1 (0=NONE,1=REL,2=ABS), TSC=2100263096 Hz
+Timer: init done
+
+Kernel heap initialized.
+Heap statistics:
+ Total bytes: 536870872
+ Free bytes: 536870872
+ Used bytes: 0
+ Peak bytes: 0
+ Heap base addr: 24641536
+ Heap end addr: 561512448
+
+Heartbeat: init...
+APIC Timer: calibrating...
+APIC Timer: apic_hz=1007167846, tick_hz=100, initial_count=10071678
+APIC Timer: configured (masked, ready to start)
+Heartbeat: init done
+Kernel initialization complete.
+Boot successful!
+
+StarForth v4: one host node, the F18-derived engine
+V4: capsule v4:nucleus-64.f18 signature: missing (unsigned)
+PARITY:V4_NUCLEUS name=v4:nucleus-64.f18 capsule_id=0xdac6d69e81818b71 capsule_hash=0xdac6d69e81818b71 words=310
+V4: capsule v4:forth79.4th signature: missing (unsigned)
+PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x4abf568309282a76
+V4: capsule v4:post79.4th signature: missing (unsigned)
+PARITY:V4_POST tests=538 pass=538 fail=0
+PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb26d1b974884179d capsule_hash=0xb26d1b974884179d dict_hash=0xd110b71c6c09dd8e
+PARITY:OK
+POST: PASSED
+ok> : SQ DUP * ;
+ ok
+ok> 7 SQ . 3 4 U* . .
+49 0 12 ok
+ok> COLD
+FORTH-79 Cold Start
+System initialized.
+ ok
+ok> 3 4 U* . .
+0 12 ok
+ok> FORGET U*
+Protected word
+ ERROR
+ok> 9 KERNEL-WORD ASK9 ASK9
+Argument out of range
+ ERROR
+ok> BYE
+BYE: cold restart
diff --git a/logs/20261006-144204/aarch64/qemu-aarch64-20261006-144204.log b/logs/20261006-144204/aarch64/qemu-aarch64-20261006-144204.log
new file mode 100644
index 00000000..9acbad0e
--- /dev/null
+++ b/logs/20261006-144204/aarch64/qemu-aarch64-20261006-144204.log
@@ -0,0 +1,104 @@
+UEFI firmware (version 2025.11-3ubuntu7.3 built at 15:40:26 on Sep 23 2026)
+[2J[001;001H[=3h[2J[001;001H[2J[001;001H[8;031;100t[2J[001;001HBdsDxe: failed to load Boot0002 "UEFI Misc Device" from PciRoot(0x0)/Pci(0x2,0x0): Not Found
+BdsDxe: failed to load Boot0003 "UEFI QEMU QEMU USB HARDDRIVE 1-0000:00:04.0-1" from PciRoot(0x0)/Pci(0x4,0x0)/USB(0x0,0x0): Not Found
+BdsDxe: loading Boot0004 "UEFI Misc Device 2" from PciRoot(0x0)/Pci(0x6,0x0)
+BdsDxe: starting Boot0004 "UEFI Misc Device 2" from PciRoot(0x0)/Pci(0x6,0x0)
+[2J[001;001H[=3h[2J[001;001HStarKernel UEFI Loader
+Loading kernel from ESP...
+[CKPT 001] Entered efi_main - ConOut live
+Collecting boot information...
+[CKPT 004] Command line parsed
+[CKPT 005] Kernel stack allocation decided
+[CKPT 006] Boot info collected (ACPI table located)
+[CKPT 007] GOP: linear framebuffer found
+[CKPT 008] About to enter ExitBootServices retry loop
+
+
+ _____ _ _ __ _
+ / ____| | | |/ / | |
+ | (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
+ \___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
+ ____) | || (_| | | | . \ __/ | | | | | __/ |
+ |_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
+
+LithosAnanke v2.1.0
+Architecture: aarch64
+Build: Oct 6 2026 14:41:37
+
+UEFI BootServices: EXITED
+
+=== StarKernel Boot Information ===
+Memory map entries: 108
+Total memory: 4093 MB
+Usable memory: 4056 MB
+===================================
+
+PMM initialized.
+PMM statistics:
+ Total pages: 1039866
+ Free pages : 1038455
+ Used pages : 1411
+ Total MB : 4061
+ Free MB : 4056
+ Used MB : 5
+
+VMM initialized (mapped RAM, CR3 switched)
+VMM initialized (mapped RAM, CR3 switched)
+VMM self-test: mapped OK at 0xffff800000000000
+VMM self-test complete.
+
+AArch64: running at EL1
+IDT installed.
+
+APIC: init...
+PSCI: no DTB -- using HVC (QEMU virt-machine default)
+GICv2: no DTB GIC node -- using QEMU virt-machine defaults
+GICv2: distributor+CPU interface enabled, PPI 30
+APIC: init done
+
+Timer: init...
+Timer: AArch64 generic timer initialised.
+Timer: init done
+
+Kernel heap initialized.
+Heap statistics:
+ Total bytes: 2147483608
+ Free bytes: 2147483608
+ Used bytes: 0
+ Peak bytes: 0
+ Heap base addr: 1207959552
+ Heap end addr: 3355443200
+
+Heartbeat: init...
+Heartbeat: init done
+Kernel initialization complete.
+Boot successful!
+
+StarForth v4: one host node, the F18-derived engine
+V4: capsule v4:nucleus-64.f18 signature: missing (unsigned)
+PARITY:V4_NUCLEUS name=v4:nucleus-64.f18 capsule_id=0xdac6d69e81818b71 capsule_hash=0xdac6d69e81818b71 words=310
+V4: capsule v4:forth79.4th signature: missing (unsigned)
+PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x4abf568309282a76
+V4: capsule v4:post79.4th signature: missing (unsigned)
+PARITY:V4_POST tests=538 pass=538 fail=0
+PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb26d1b974884179d capsule_hash=0xb26d1b974884179d dict_hash=0xd110b71c6c09dd8e
+PARITY:OK
+POST: PASSED
+ok> : SQ DUP * ;
+ ok
+ok> 7 SQ . 3 4 U* . .
+49 0 12 ok
+ok> COLD
+FORTH-79 Cold Start
+System initialized.
+ ok
+ok> 3 4 U* . .
+0 12 ok
+ok> FORGET U*
+Protected word
+ ERROR
+ok> 9 KERNEL-WORD ASK9 ASK9
+Argument out of range
+ ERROR
+ok> BYE
+BYE: cold restart
diff --git a/logs/20261006-144601/riscv64/qemu-riscv64-20261006-144601.log b/logs/20261006-144601/riscv64/qemu-riscv64-20261006-144601.log
new file mode 100644
index 00000000..14e7db9d
--- /dev/null
+++ b/logs/20261006-144601/riscv64/qemu-riscv64-20261006-144601.log
@@ -0,0 +1,174 @@
+
+OpenSBI v1.8
+ ____ _____ ____ _____
+ / __ \ / ____| _ \_ _|
+ | | | |_ __ ___ _ __ | (___ | |_) || |
+ | | | | '_ \ / _ \ '_ \ \___ \| _ < | |
+ | |__| | |_) | __/ | | |____) | |_) || |_
+ \____/| .__/ \___|_| |_|_____/|____/_____|
+ | |
+ |_|
+
+Platform Name : riscv-virtio,qemu
+Platform Features : medeleg
+Platform HART Count : 1
+Platform HART Protection : pmp
+Platform IPI Device : aclint-mswi
+Platform Timer Device : aclint-mtimer @ 10000000Hz
+Platform Console Device : uart8250
+Platform HSM Device : ---
+Platform PMU Device : ---
+Platform Reboot Device : syscon-reboot
+Platform Shutdown Device : syscon-poweroff
+Platform Suspend Device : ---
+Platform CPPC Device : ---
+Firmware Base : 0x80000000
+Firmware Size : 321 KB
+Firmware RW Offset : 0x40000
+Firmware RW Size : 65 KB
+Firmware Heap Offset : 0x47000
+Firmware Heap Size : 37 KB (total), 0 KB (reserved), 12 KB (used), 23 KB (free)
+Firmware Scratch Size : 4096 B (total), 1464 B (used), 2632 B (free)
+Runtime SBI Version : 3.0
+Standard SBI Extensions : ipi,pmu,srst,sse,hsm,rfnc,fwft,time,base,legacy,dbcn,dbtr
+Experimental SBI Extensions : none
+
+Domain0 Name : root
+Domain0 Boot HART : 0
+Domain0 HARTs : 0*
+Domain0 Region00 : 0x0000000080040000-0x000000008005ffff M: (F,R,W) S/U: ()
+Domain0 Region01 : 0x0000000080000000-0x000000008003ffff M: (F,R,X) S/U: ()
+Domain0 Region02 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W)
+Domain0 Region03 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W)
+Domain0 Region04 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: ()
+Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W)
+Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W)
+Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X)
+Domain0 Next Address : 0x0000000020000000
+Domain0 Next Arg1 : 0x00000000bfe00000
+Domain0 Next Mode : S-mode
+Domain0 SysReset : yes
+Domain0 SysSuspend : yes
+
+Boot HART ID : 0
+Boot HART Domain : root
+Boot HART Priv Version : v1.12
+Boot HART Base ISA : rv64imafdch
+Boot HART ISA Extensions : sstc,zicntr,zihpm,zicboz,zicbom,sdtrig,svadu
+Boot HART PMP Count : 16
+Boot HART PMP Granularity : 2 bits
+Boot HART PMP Address Bits : 54
+Boot HART MHPM Info : 16 (0x0007fff8)
+Boot HART Debug Triggers : 2 triggers
+Boot HART MIDELEG : 0x0000000000001666
+Boot HART MEDELEG : 0x0000000000f4b509
+[2J[004D[=3h[2J[009D[2J[004D[8;031;100t[2J[016DRISC-V EDK2 firmware version 2025.11-3ubuntu7.3
+Press ESCAPE within 5 seconds for boot options ERROR: C40000002:V03051002 I0 6D33944A-EC75-4855-A54D-809C75241F6C 83FFF850
+BdsDxe: failed to load Boot0001 "UEFI Misc Device" fr
+om PciRoot(0x0)/Pci(0x1,0x0): Not Found
+[2J[001;001H[=3h[2J[009DStarKernel UEFI Loader
+Loading kernel from ESP...
+[CKPT 001] Entered efi_main - ConOut live
+Monolithic build - kernel linked directly
+Collecting boot information...
+CmdLine: parsed OK
+[CKPT 004] Command line parsed
+[CKPT 005] Kernel stack allocation decided
+[CKPT 006] Boot info collected (ACPI table located)
+GOP: linear framebuffer found
+[CKPT 007] GOP: linear framebuffer found
+[CKPT 008] About to enter ExitBootServices retry loop
+Calling kernel_main (monolithic)...
+riscv64 item 4.3.5a: satp state at kernel entry (before switch)
+ satp.MODE = 0x000000000000000a
+ satp.PPN = 0x00000000000bf868
+ __kernel_start = 0x00000000bdea38f8
+riscv64: satp cleared -- Bare mode, explicit (item 4.3.5a)
+
+
+ _____ _ _ __ _
+ / ____| | | |/ / | |
+ | (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
+ \___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
+ ____) | || (_| | | | . \ __/ | | | | | __/ |
+ |_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
+
+LithosAnanke v2.1.0
+Architecture: riscv64
+Build: Oct 6 2026 14:45:22
+
+UEFI BootServices: EXITED
+
+=== StarKernel Boot Information ===
+Memory map entries: 100
+Total memory: 1020 MB
+Usable memory: 977 MB
+===================================
+
+PMM initialized.
+PMM statistics:
+ Total pages: 250780
+ Free pages : 250128
+ Used pages : 652
+ Total MB : 979
+ Free MB : 977
+ Used MB : 2
+
+VMM initialized (mapped RAM, CR3 switched)
+VMM initialized (mapped RAM, CR3 switched)
+VMM self-test: mapped OK at 0xffff800000000000
+VMM self-test complete.
+
+IDT installed.
+
+APIC: init...
+PLIC: no DTB PLIC node -- using QEMU virt-machine default (base=0x0c000000)
+PLIC: init (S-mode context 1, threshold=0)
+APIC: init done
+
+Timer: init...
+Timer: RISC-V time CSR @ 10000000 Hz (FALLBACK, no devicetree)
+Timer: init done
+
+Kernel heap initialized.
+Heap statistics:
+ Total bytes: 536870872
+ Free bytes: 536870872
+ Used bytes: 0
+ Peak bytes: 0
+ Heap base addr: 2214588416
+ Heap end addr: 2751459328
+
+Heartbeat: init...
+Heartbeat: init done
+Kernel initialization complete.
+Boot successful!
+
+StarForth v4: one host node, the F18-derived engine
+V4: capsule v4:nucleus-64.f18 signature: missing (unsigned)
+PARITY:V4_NUCLEUS name=v4:nucleus-64.f18 capsule_id=0xdac6d69e81818b71 capsule_hash=0xdac6d69e81818b71 words=310
+V4: capsule v4:forth79.4th signature: missing (unsigned)
+PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x4abf568309282a76
+V4: capsule v4:post79.4th signature: missing (unsigned)
+PARITY:V4_POST tests=538 pass=538 fail=0
+PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb26d1b974884179d capsule_hash=0xb26d1b974884179d dict_hash=0xd110b71c6c09dd8e
+PARITY:OK
+POST: PASSED
+ok> : SQ DUP * ;
+ ok
+ok> 7 SQ . 3 4 U* . .
+49 0 12 ok
+ok> COLD
+FORTH-79 Cold Start
+System initialized.
+ ok
+ok> 3 4 U* . .
+0 12 ok
+ok> FORGET U*
+Protected word
+ ERROR
+ok> 9 KERNEL-WORD ASK9 ASK9
+Argument out of range
+ ERROR
+ok> BYE
+BYE: cold restart
diff --git a/v4/README.md b/v4/README.md
index 23e16d5c..2e0a58e7 100644
--- a/v4/README.md
+++ b/v4/README.md
@@ -123,6 +123,15 @@ read -- or, of two neighbours, the one with the lower number may wait.
`NEIGHBOUR ( node port -- )` tells a node who is on each port. A node with
no room for one more message lets it go and counts it in `(LOST)`.
+**Five nodes from nothing** (`MESH.md` step 5) is built and proven in the
+fabric under test: Hera asks whoever holds the fabric for a node
+(`v4/src/manage.c`: `NODE-BORN`, `NODE-WIRE` and the rest), sends it the
+nucleus through the port that joins them, then FORTH-79 and POST a line at
+a time, and joins four such nodes in a square round her
+(`capsules/v4/hera.4th`: `BIRTH`, `UNIT`). `v4/tests/test_host_unit.c`.
+The two products are not yet this: they become the five nodes at steps 8
+and 9.
+
**This is still the lone node.** On bare metal `kernel_main.c` starts it
before the fleet tables, beside the kernel's own system and not in the VM's
place. `ENGINE.md` sets out the steps from here; step 1 is done and step 2 is
diff --git a/v4/capsule/core.v4 b/v4/capsule/core.v4
index 48ca494e..de1a18bd 100644
--- a/v4/capsule/core.v4
+++ b/v4/capsule/core.v4
@@ -181,13 +181,18 @@ header CMOVE
SET: (MQ-HEAD) a! !
(MQ#) a! @ -1 + ! ;
-\ ( port to -- ) take in a message. B is at the port, by its address,
-\ and `to` is the message's first word, already read from it. A length
+\ ( port to -- ) the seven words of a message being taken in, and the
+\ port, go into (MQ-HDR). B is at the port, by its address, and `to` is
+\ the message's first word, already read from it.
+: (TAKE-HDR)
+ (MQ-HDR) a! !+ 5 FOR @b !+ UNEXT ! ;
+
+\ ( -- ) the message whose seven words are in (MQ-HDR) is kept with the
+\ messages waiting: its text is read from the port B is at. A length
\ below zero, or above what a message carries, is kept as -1 with no text:
\ a longer one is read to its end first, so that what follows it is not
\ taken for a message.
-: (TAKE)
- (MQ-HDR) a! !+ 5 FOR @b !+ UNEXT ! \ its seven words, and the port
+: (TAKE-KEEP)
(MQ-HDR)+6 a! @ -if SIZED
drop -1 (MQ-HDR)+6 a! ! 0 jump COUNTED
SIZED: dup -1025 + -if LONG drop 3 + 2/ 2/ jump COUNTED
@@ -203,6 +208,9 @@ header CMOVE
GONE: drop
LOSE: (LOST) a! @ 1 + ! ;
+\ ( port to -- ) take in a message and keep it
+: (TAKE) (TAKE-HDR) jump (TAKE-KEEP)
+
\ ( port -- ) wait until a message may be begun on the port, by its
\ address, taking in whatever is being written to this node meanwhile; and
\ leave B at the port. The port is kept in (GATE-PORT), not on the stack:
diff --git a/v4/capsule/quit.v4 b/v4/capsule/quit.v4
index dc8510ee..43a31a38 100644
--- a/v4/capsule/quit.v4
+++ b/v4/capsule/quit.v4
@@ -175,13 +175,11 @@ header DEFAULT-ROUTE
header NO-ROUTES
: NO-ROUTES 0 (ROUTE#) a! ! 0 (ROUTE-DEFAULT) a! ! ;
-\ ( baddr u node -- ) send the node u characters of text to interpret. It
-\ is on its way when SEND returns; what it prints there goes to that node's
-\ console. With no way to the node it is error 12. The write waits for the
-\ neighbour to read, and is not begun until it may be (core.v4, (GATE)).
-header SEND
-: SEND
- dup (PORT-FOR) if NOWAY
+\ ( baddr u node port -- ) send the node u characters of text to
+\ interpret, by the port given, by its address. It is on its way when this
+\ returns. The write waits for the neighbour to read, and is not begun
+\ until it may be (core.v4, (GATE)).
+: (SEND-ON)
(GATE) !b \ to
1 (HDR)
dup !b \ length
@@ -189,8 +187,59 @@ header SEND
FOR dup C@ over 1 + C@ 8* + over 2 + C@ 8* 8* + over 3 + C@ 8* 8* 8* + !b 4 + NEXT
drop ;
NONE: drop drop ;
+
+\ ( baddr u node -- ) send the node u characters of text to interpret,
+\ by the way this node knows to it. What it prints there goes to that
+\ node's console. With no way to the node it is error 12.
+header SEND
+: SEND
+ dup (PORT-FOR) if NOWAY jump (SEND-ON)
NOWAY: drop drop drop drop NODE-ERROR b! 12 !b ;
+\ ( baddr u node port -- ) the same by a port this node names, by its
+\ number, whatever ways it knows: for a neighbour that has no number of
+\ its own yet, to which text is sent as to node 0.
+header SEND-ON
+: SEND-ON
+ dup -if POS jump BAD
+ POS: -PORTS + -if BAD drop (PORT) + jump (SEND-ON)
+ BAD: drop drop drop drop drop NODE-ERROR b! 12 !b ;
+
+\ ( node -- how ) WAIT FOR THE NODE'S WORD OF HOW TEXT ENDED: 0 QUIT,
+\ 1 completed, 2 an error. This node is blocked reading its ports until a
+\ message of type 3 comes for it from that node; every other message that
+\ comes meanwhile is kept with the messages waiting. It waits for ever
+\ for a node that never answers.
+header AWAIT
+: AWAIT
+ (AWAIT-FROM) a! !
+ L: (PORT)+8 b! @b (PORT)+9 b! @b (PORT) + dup b! SWAP (TAKE-HDR)
+ (MQ-HDR) a! @ (ME) a! @ xor if A1 drop jump KEEP
+ A1: drop (MQ-HDR)+2 a! @ -3 + if A2 drop jump KEEP
+ A2: drop (MQ-HDR)+1 a! @ (AWAIT-FROM) a! @ xor if A3 drop jump KEEP
+ A3: drop (MQ-HDR)+6 a! @ -4 + if A4 drop jump KEEP
+ A4: drop @b ; \ its one word
+ KEEP: (TAKE-KEEP) jump L
+
+\ ( w port -- ) write a cell to the port, by its number. It waits until
+\ the neighbour has taken it. This is how a node is sent a capsule of F18
+\ code (docs/v4.0.0/MESH.md section 5): the cells go as they are, not as a
+\ message, to a node that is executing from its ports.
+header PORT!
+: PORT!
+ dup -if POS jump BAD
+ POS: -PORTS + -if BAD drop (PORT) + b! !b ;
+ BAD: drop drop drop NODE-ERROR b! 12 !b ;
+
+\ ( -- ) WHAT IS IN THE DICTIONARY NOW IS THE SYSTEM: COLD comes back to
+\ it and FORGET will not go below it. Whoever has loaded a node's capsules
+\ has it do this (system.v4, (BOOT) and FENCE).
+header (SEAL)
+: (SEAL)
+ DP a! @ (BOOT) a! !
+ (LATEST) a! @ (BOOT)+1 a! !
+ DP a! @ 3 + 2/ 2/ FENCE a! ! ;
+
\ FORTH-79: clear the return stack, set execution mode, return control to
\ the terminal; no message is given. The data stack is left as it is.
\ NODE-ERROR, by name: how a definition written in FORTH raises an error.
diff --git a/v4/include/v4/image.h b/v4/include/v4/image.h
index 7c145c8e..0038a663 100644
--- a/v4/include/v4/image.h
+++ b/v4/include/v4/image.h
@@ -65,6 +65,12 @@ extern const v4_image v4_capsule_image;
* back to, and FORGET will not go below it. */
void v4_image_seal(v4_node *n, const v4_image *im);
+/* The dictionary hash of a node started from `im`: FNV-1a over its memory
+ * below HERE, a cell at a time, low byte first, and then LATEST. It does
+ * not depend on the machine: every build of one commit, with one cell
+ * width, gives the same hash for the same dictionary. */
+uint64_t v4_image_dict_hash(const v4_node *n, const v4_image *im);
+
/* A node newly born (node.h), with the registers the nucleus expects of the
* node it runs on -- and `disk`, `blocks` blocks of 1024 bytes, as its block
* storage, or none if `disk` is 0. Its memory is empty and it is blocked
diff --git a/v4/include/v4/manage.h b/v4/include/v4/manage.h
new file mode 100644
index 00000000..93ff4110
--- /dev/null
+++ b/v4/include/v4/manage.h
@@ -0,0 +1,81 @@
+/* manage.h -- what Hera asks of whoever holds the fabric.
+ * docs/v4.0.0/MESH.md section 9.
+ *
+ * Hera decides which nodes exist and are awake (MESH.md ruling 7). She is
+ * a node like any other and cannot make one: she asks, by a write to the
+ * port where her requests go (ENGINE.md 3.3), and the write blocks her until
+ * she is answered. Each request is a word on her, made with KERNEL-WORD
+ * (v4/capsule/compile.v4); its arguments are what it finds on her data
+ * stack and its results are what is left there.
+ *
+ * These are the requests, in order. The n-th is request `first + n`, where
+ * `first` is whatever number the host gives the first of them:
+ *
+ * NODE-ME ( -- place ) the place of the node that asks
+ * NODE-BORN ( -- place | -1 ) a node is born, empty, listening at
+ * its ports; -1: there is no room
+ * NODE-WIRE ( a pa b pb -- flag ) port pa of the node at a is wired
+ * to port pb of the node at b
+ * NODE-UNWIRE ( a pa -- ) what is on that port is cut
+ * NODE-SLEEP ( place -- ) it executes nothing until woken
+ * NODE-WAKE ( place -- )
+ * NODE-KILL ( place -- ) it is removed, whatever it was doing
+ * NODE-PARITY ( place number -- ) the host records the node's parity:
+ * its number and its dictionary hash
+ * CAPSULE-OPEN ( baddr u -- flag ) the capsule of that name is found
+ * and checked, and is read from its
+ * start; 0: there is none, or it is
+ * not what it says it is
+ * CAPSULE-CELL ( -- w flag ) the next cell of a capsule of F18
+ * code; 0 0 when there are no more
+ * CAPSULE-LINE ( baddr -- u | -1 ) the next line of a capsule of
+ * FORTH blocks, put at baddr: at
+ * most 64 characters; -1 at its end
+ *
+ * A place is where a node is in the fabric (fabric.h); it is the host's
+ * name for the node. A node's number, which messages are addressed by, is
+ * the nodes' own affair: Hera gives a newborn its number by telling it.
+ *
+ * Only the node the host has wired to this is answered, so only Hera's
+ * requests are honoured. The unit rule (MESH.md ruling 5) is not here: it
+ * is what Hera does with these, in her capsule.
+ *
+ * Nothing here uses the C library: the bare-metal kernel links this file.
+ */
+#ifndef V4_MANAGE_H
+#define V4_MANAGE_H
+
+#include "v4/fabric.h"
+
+#define V4_MANAGE_WORDS 11
+extern const char *const v4_manage_words[V4_MANAGE_WORDS];
+#define V4_MANAGE_LINE 64 /* the most characters in a line of a block */
+
+typedef struct {
+ v4_fabric *f;
+
+ /* A node is born: the host finds it room, adds it to the fabric and
+ * gives it what a node's hardware gives it. Its place, or -1. */
+ int (*born)(void *self);
+ /* The node that was at `place` has been removed: its room is free. */
+ void (*gone)(void *self, unsigned place, v4_fabric_node *was);
+ /* The capsule called `name`, `len` characters: found, its hash and
+ * signature checked, and to be read from its start. 1, or 0. */
+ int (*open)(void *self, const char *name, unsigned len);
+ /* The next cell of the open capsule: 1 with it in *w, 0 at its end. */
+ int (*cell)(void *self, v4_cell *w);
+ /* The next line of the open capsule that is part of a block, into
+ * `text`, at most V4_MANAGE_LINE characters: its length, or -1. */
+ int (*line)(void *self, char *text);
+ /* The node at `place`, which Hera calls `number`: record its parity. */
+ void (*parity)(void *self, unsigned place, v4_cell number, const v4_node *n);
+ void *self;
+} v4_manage;
+
+/* The node at `place` has written request `n` (0 is NODE-ME) to the port
+ * its requests go to. Its arguments are taken from its data stack and its
+ * results left there. Returns 1; 0 if `n` is not one of the requests, and
+ * then nothing has been done. */
+int v4_manage_serve(const v4_manage *m, unsigned place, unsigned n);
+
+#endif /* V4_MANAGE_H */
diff --git a/v4/src/fabric.c b/v4/src/fabric.c
index 8eb8423b..92f05329 100644
--- a/v4/src/fabric.c
+++ b/v4/src/fabric.c
@@ -180,8 +180,13 @@ unsigned v4_fabric_step(v4_fabric *f)
{
unsigned i, done = 0;
+ /* a node that is blocked fetches nothing, and is looked about for when
+ * it is next to execute */
for (i = 0; i < f->capacity; i++)
- if (awake(f, i)) look_about(f, i);
+ if (awake(f, i)) {
+ const v4_node *n = &f->place[i].node->n;
+ if (!n->stopped && !n->asking && !(n->reading && !n->given)) look_about(f, i);
+ }
for (i = 0; i < f->capacity; i++)
if (awake(f, i)) done += v4_exec_step_word(&f->place[i].node->n, &f->place[i].node->es, &f->place[i].node->h);
diff --git a/v4/src/image.c b/v4/src/image.c
index 9b079d42..c3f60414 100644
--- a/v4/src/image.c
+++ b/v4/src/image.c
@@ -35,3 +35,30 @@ void v4_image_seal(v4_node *n, const v4_image *im)
n->mem[im->boot_cells + 1] = n->mem[im->latest];
n->mem[im->fence] = (n->mem[im->dp] + 3) / 4;
}
+
+/* FNV-1a over the node's memory below HERE, a cell at a time, low byte
+ * first, and then LATEST. */
+#define FNV_OFFSET 0xcbf29ce484222325ULL
+#define FNV_PRIME 0x00000100000001b3ULL
+
+static uint64_t hash_cell(uint64_t h, v4_cell c)
+{
+ v4_ucell u = (v4_ucell)c;
+ unsigned i;
+ for (i = 0; i < V4_CELL_BITS / 8; i++) {
+ h ^= (uint64_t)((u >> (8 * i)) & 0xffu);
+ h *= FNV_PRIME;
+ }
+ return h;
+}
+
+uint64_t v4_image_dict_hash(const v4_node *n, const v4_image *im)
+{
+ uint64_t h = FNV_OFFSET;
+ v4_cell here = (n->mem[im->dp] + 3) / 4, k;
+
+ if (here < 0) here = 0;
+ if (here > (v4_cell)V4_NODE_WORDS) here = (v4_cell)V4_NODE_WORDS;
+ for (k = 0; k < here; k++) h = hash_cell(h, n->mem[k]);
+ return hash_cell(h, n->mem[im->latest]);
+}
diff --git a/v4/src/manage.c b/v4/src/manage.c
new file mode 100644
index 00000000..9b44639f
--- /dev/null
+++ b/v4/src/manage.c
@@ -0,0 +1,107 @@
+/* manage.c -- what Hera asks of whoever holds the fabric. See manage.h.
+ *
+ * Nothing here uses the C library: the bare-metal kernel links this file.
+ */
+#include "v4/manage.h"
+
+const char *const v4_manage_words[V4_MANAGE_WORDS] = {
+ "NODE-ME", "NODE-BORN", "NODE-WIRE", "NODE-UNWIRE", "NODE-SLEEP", "NODE-WAKE", "NODE-KILL", "NODE-PARITY",
+ "CAPSULE-OPEN", "CAPSULE-CELL", "CAPSULE-LINE"
+};
+
+/* A character of the node's memory: four to a cell, the first lowest, as
+ * the nucleus keeps text (v4/capsule/core.v4). */
+static int in_memory(v4_cell baddr) { return baddr >= 0 && (baddr >> 2) < (v4_cell)V4_NODE_WORDS; }
+static char char_at(const v4_node *n, v4_cell baddr)
+{
+ return (char)(((v4_ucell)n->mem[baddr >> 2] >> (8 * (unsigned)(baddr & 3))) & 0xFFu);
+}
+static void char_put(v4_node *n, v4_cell baddr, char c)
+{
+ unsigned shift = 8 * (unsigned)(baddr & 3);
+ v4_ucell cell = (v4_ucell)n->mem[baddr >> 2];
+ cell = (cell & ~((v4_ucell)0xFFu << shift)) | ((v4_ucell)(unsigned char)c << shift);
+ n->mem[baddr >> 2] = (v4_cell)cell;
+}
+
+static unsigned place_of(v4_cell x) { return x < 0 ? ~0u : (unsigned)x; }
+
+int v4_manage_serve(const v4_manage *m, unsigned place, unsigned request)
+{
+ v4_fabric_node *asker = v4_fabric_node_at(m->f, place);
+ v4_node *n;
+ v4_cell a, b, c, d;
+
+ if (!asker || request >= V4_MANAGE_WORDS) return 0;
+ n = &asker->n;
+
+ switch (request) {
+ case 0: /* NODE-ME ( -- place ) */
+ v4_dstack_push(&n->ds, (v4_cell)place);
+ break;
+ case 1: /* NODE-BORN ( -- place | -1 ) */
+ v4_dstack_push(&n->ds, (v4_cell)(m->born ? m->born(m->self) : -1));
+ break;
+ case 2: /* NODE-WIRE ( a pa b pb -- flag ) */
+ d = v4_dstack_pop(&n->ds); c = v4_dstack_pop(&n->ds); b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
+ v4_dstack_push(&n->ds, v4_fabric_wire(m->f, place_of(a), place_of(b), place_of(c), place_of(d)) ? -1 : 0);
+ break;
+ case 3: /* NODE-UNWIRE ( a pa -- ) */
+ b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
+ (void)v4_fabric_unwire(m->f, place_of(a), place_of(b));
+ break;
+ case 4: /* NODE-SLEEP ( place -- ) */
+ a = v4_dstack_pop(&n->ds);
+ v4_fabric_sleep(m->f, place_of(a));
+ break;
+ case 5: /* NODE-WAKE ( place -- ) */
+ a = v4_dstack_pop(&n->ds);
+ v4_fabric_wake(m->f, place_of(a));
+ break;
+ case 6: { /* NODE-KILL ( place -- ) */
+ v4_fabric_node *was;
+ a = v4_dstack_pop(&n->ds);
+ if (place_of(a) == place) break; /* not itself: there would be no one to answer */
+ was = v4_fabric_remove(m->f, place_of(a));
+ if (was && m->gone) m->gone(m->self, place_of(a), was);
+ break;
+ }
+ case 7: { /* NODE-PARITY ( place number -- ) */
+ v4_fabric_node *it;
+ b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
+ it = v4_fabric_node_at(m->f, place_of(a));
+ if (it && m->parity) m->parity(m->self, place_of(a), b, &it->n);
+ break;
+ }
+ case 8: { /* CAPSULE-OPEN ( baddr u -- flag ) */
+ char name[64];
+ unsigned i, len;
+ b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
+ len = b < 0 ? 0u : (unsigned)b;
+ if (len == 0 || len > sizeof name || !in_memory(a) || !in_memory(a + (v4_cell)len - 1) || !m->open) {
+ v4_dstack_push(&n->ds, 0);
+ break;
+ }
+ for (i = 0; i < len; i++) name[i] = char_at(n, a + (v4_cell)i);
+ v4_dstack_push(&n->ds, m->open(m->self, name, len) ? -1 : 0);
+ break;
+ }
+ case 9: /* CAPSULE-CELL ( -- w flag ) */
+ a = 0;
+ if (m->cell && m->cell(m->self, &a)) { v4_dstack_push(&n->ds, a); v4_dstack_push(&n->ds, -1); }
+ else { v4_dstack_push(&n->ds, 0); v4_dstack_push(&n->ds, 0); }
+ break;
+ case 10: { /* CAPSULE-LINE ( baddr -- u | -1 ) */
+ char text[V4_MANAGE_LINE];
+ int len, i;
+ a = v4_dstack_pop(&n->ds);
+ len = m->line ? m->line(m->self, text) : -1;
+ if (len > V4_MANAGE_LINE) len = V4_MANAGE_LINE;
+ if (len > 0 && (!in_memory(a) || !in_memory(a + len - 1))) len = -1;
+ for (i = 0; i < len; i++) char_put(n, a + i, text[i]);
+ v4_dstack_push(&n->ds, (v4_cell)len);
+ break;
+ }
+ }
+ return 1;
+}
diff --git a/v4/system/boot.c b/v4/system/boot.c
index fe8c9e24..e3b9fe88 100644
--- a/v4/system/boot.c
+++ b/v4/system/boot.c
@@ -56,29 +56,9 @@ static void say_hex(const v4_boot *b, uint64_t v)
/* ---- hashing: FNV-1a, 64 bits, as kernel/src/vm/parity.c ----------------- */
-#define FNV_OFFSET 0xcbf29ce484222325ULL
-#define FNV_PRIME 0x00000100000001b3ULL
-
-static uint64_t hash_cell(uint64_t h, v4_cell c)
-{
- v4_ucell u = (v4_ucell)c;
- unsigned i;
- for (i = 0; i < V4_CELL_BITS / 8; i++) {
- h ^= (uint64_t)((u >> (8 * i)) & 0xffu);
- h *= FNV_PRIME;
- }
- return h;
-}
-
uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im)
{
- uint64_t h = FNV_OFFSET;
- v4_cell here = (n->mem[im->dp] + 3) / 4, k;
-
- if (here < 0) here = 0;
- if (here > (v4_cell)V4_NODE_WORDS) here = (v4_cell)V4_NODE_WORDS;
- for (k = 0; k < here; k++) h = hash_cell(h, n->mem[k]);
- return hash_cell(h, n->mem[im->latest]);
+ return v4_image_dict_hash(n, im);
}
/* how many words FORTH holds: the list from LATEST, each entry's link in the
diff --git a/v4/tests/host_map.h b/v4/tests/host_map.h
index e695a344..7ee2e18c 100644
--- a/v4/tests/host_map.h
+++ b/v4/tests/host_map.h
@@ -81,6 +81,7 @@
#define MQ_HEAD (BUF0_W - 35) /* the messages waiting to be dealt with: where the oldest begins, */
#define MQ_TAIL (BUF0_W - 36) /* where the next will go, */
#define MQ_COUNT (BUF0_W - 37) /* and how many cells they take */
+#define AWAIT_FROM (BUF0_W - 39) /* the node whose word of how text ended is waited for */
#define GATE_PORT (BUF0_W - 38) /* the port address a message is about to be begun on */
#define NEAR (BUF0_W - 64) /* for each port, the number of the node on the other end; 0: not told */
#define MQ_HDR (BUF0_W - 56) /* a message being taken in: its seven words and the port it came on */
@@ -194,6 +195,7 @@ static int host_load(v4_text *tx, v4_node *n, const char *const *files, unsigned
v4_text_constant(tx, "-PORTS", -(v4_cell)V4_PORTS);
v4_text_constant(tx, "(NEAR)", NEAR);
v4_text_constant(tx, "(GATE-PORT)", GATE_PORT);
+ 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-ROOM", -(v4_cell)(MQ_CELLS - 7)); /* cells taken + a text's words + this is below zero while the message fits */
diff --git a/v4/tests/test_host_unit.c b/v4/tests/test_host_unit.c
new file mode 100644
index 00000000..6b2126bf
--- /dev/null
+++ b/v4/tests/test_host_unit.c
@@ -0,0 +1,386 @@
+/* test_host_unit.c -- five nodes come up from nothing.
+ * docs/v4.0.0/MESH.md section 9, step 5; acceptance 1 and 2.
+ *
+ * [11]---[12]
+ * | \ / | the four outer nodes are each on a
+ * console --1 [10] | port of Hera, 10, and joined in a
+ * kernel --0 | / \ | square; the corners across from each
+ * [13]---[14] other are not wired
+ *
+ * Hera is born empty and is given the nucleus through the port her requests
+ * go to, as the boot gives it (v4/system/boot.c). She is told who she is,
+ * given the words that ask for nodes (v4/include/v4/manage.h), FORTH-79,
+ * POST, and her own capsule, capsules/v4/hera.4th. Then she is told
+ * `10 UNIT`, and everything after that is hers: she asks for four nodes,
+ * wires them, sends each the nucleus cell by cell through the port that
+ * joins them and then FORTH-79 and POST a line at a time, and joins them.
+ *
+ * What stands in here for the host: the capsules are read from the files
+ * the build bakes in, and the nucleus is assembled as mkimage assembles it.
+ * Each node has a disk of its own, attached when it is born, as the boot's
+ * one node has; storage through the ports is step 6.
+ */
+#include "v4/capsule.h"
+#include "v4/fabric.h"
+#include "v4/image.h"
+#include "v4/manage.h"
+#include "v4/message.h"
+#include "v4/text.h"
+#include
+#include
+#include
+
+#include "host_map.h"
+
+static int failures = 0, checks = 0;
+#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
+
+#define PLACES 10u
+#define DISK_BLOCKS 64u
+static v4_text tx;
+static v4_node nucleus; /* the nucleus, assembled: what the capsule of F18 code is written from */
+static v4_cell w_idle, w_fault;
+static v4_fabric_node pool[PLACES];
+static int pool_used[PLACES];
+static unsigned char disks[PLACES][DISK_BLOCKS * V4_BLOCK_BYTES];
+static v4_place places[PLACES];
+static v4_fabric f;
+static v4_image im; /* only what the dictionary hash needs of it */
+
+/* ---- the capsules ---- */
+static v4_cell f18[2u * V4_NODE_WORDS + 64u]; /* the nucleus as a capsule of F18 code */
+static unsigned f18_count;
+static void f18_put(void *self, v4_cell w) { (void)self; if (f18_count < sizeof f18 / sizeof f18[0]) f18[f18_count++] = w; }
+
+typedef struct { char *text; long len, at; int in_block; unsigned cells_at; int is_f18; } reader;
+
+static int read_file(reader *r, const char *name)
+{
+ char path[600];
+ FILE *in;
+ snprintf(path, sizeof path, "%s/../../capsules/v4/%s", V4_CAPSULE_DIR, name);
+ in = fopen(path, "rb");
+ if (!in) return 0;
+ free(r->text);
+ fseek(in, 0, SEEK_END); r->len = ftell(in); fseek(in, 0, SEEK_SET);
+ r->text = (char *)malloc((size_t)r->len + 1);
+ if (!r->text || fread(r->text, 1, (size_t)r->len, in) != (size_t)r->len) { fclose(in); return 0; }
+ fclose(in);
+ r->at = 0; r->in_block = 0; r->is_f18 = 0;
+ return 1;
+}
+/* the next line of a block that has anything on it: its length, or -1 */
+static int next_line(reader *r, char *text)
+{
+ while (r->at < r->len) {
+ const char *p = r->text + r->at;
+ long n = 0;
+ while (r->at + n < r->len && p[n] != '\n') n++;
+ r->at += n + 1;
+ if (n >= 7 && memcmp(p, "Block ", 6) == 0 && p[6] >= '0' && p[6] <= '9') { r->in_block = 1; continue; }
+ if (n && p[n - 1] == '\r') n--;
+ if (!r->in_block || n == 0) continue;
+ if (n > V4_MANAGE_LINE) return -1;
+ memcpy(text, p, (size_t)n);
+ return (int)n;
+ }
+ return -1;
+}
+
+/* ---- what Hera's requests are answered with ---- */
+static reader hera_reads; /* the capsule Hera has open */
+static unsigned born_count, parity_count;
+static v4_cell parity_number[8];
+static uint64_t parity_hash[8];
+
+static int host_born(void *self)
+{
+ unsigned k;
+ int place;
+ (void)self;
+ for (k = 0; k < PLACES && pool_used[k]; k++) { }
+ if (k == PLACES) return -1;
+ place = v4_fabric_add(&f, &pool[k], PORT);
+ if (place < 0) return -1;
+ pool_used[k] = 1;
+ born_count++;
+ /* what a node's hardware gives it; there is nothing in its memory */
+ v4_node_stack_regs_attach(&pool[k].n, DSTACK_REG, RSTACK_REG);
+ v4_node_error_attach(&pool[k].n, NODE_ERROR);
+ v4_node_fault_attach(&pool[k].n, w_fault);
+ memset(disks[k], 0, sizeof disks[k]);
+ v4_node_storage_attach(&pool[k].n, STORAGE_REG, disks[k], DISK_BLOCKS);
+ return place;
+}
+static void host_gone(void *self, unsigned place, v4_fabric_node *was)
+{
+ (void)self; (void)place;
+ pool_used[was - pool] = 0;
+}
+static int host_open(void *self, const char *name, unsigned len)
+{
+ (void)self;
+ if (len == 17 && memcmp(name, "v4:nucleus-64.f18", 17) == 0) { hera_reads.is_f18 = 1; hera_reads.cells_at = 0; return 1; }
+ if (len == 14 && memcmp(name, "v4:forth79.4th", 14) == 0) return read_file(&hera_reads, "forth79.4th");
+ if (len == 13 && memcmp(name, "v4:post79.4th", 13) == 0) return read_file(&hera_reads, "post79.4th");
+ return 0;
+}
+static int host_cell(void *self, v4_cell *w)
+{
+ (void)self;
+ if (!hera_reads.is_f18 || hera_reads.cells_at >= f18_count) return 0;
+ *w = f18[hera_reads.cells_at++];
+ return 1;
+}
+static int host_line(void *self, char *text)
+{
+ (void)self;
+ return hera_reads.is_f18 ? -1 : next_line(&hera_reads, text);
+}
+static void host_parity(void *self, unsigned place, v4_cell number, const v4_node *n)
+{
+ (void)self; (void)place;
+ if (parity_count < 8) { parity_number[parity_count] = number; parity_hash[parity_count] = v4_image_dict_hash(n, &im); }
+ parity_count++;
+}
+static const v4_manage manage = { &f, host_born, host_gone, host_open, host_cell, host_line, host_parity, 0 };
+
+/* ---- the kernel: a device on Hera's port 0 ---- */
+#define FIRST_REQUEST 1 /* the number of NODE-ME on her */
+static unsigned hera_place, given_cells, refused;
+static int kernel_give(void *self, v4_cell *value) /* the nucleus, to a node that has nothing in it */
+{
+ (void)self;
+ if (given_cells >= f18_count) return 0;
+ *value = f18[given_cells++];
+ return 1;
+}
+static int kernel_take(void *self, v4_cell value) /* a request */
+{
+ (void)self;
+ if (value < FIRST_REQUEST || !v4_manage_serve(&manage, hera_place, (unsigned)(value - FIRST_REQUEST))) refused++;
+ return 1;
+}
+static const v4_device kernel = { kernel_take, kernel_give, 0 };
+
+/* ---- the console: a device on Hera's port 1 ---- */
+#define CONSOLE_ID 1
+static v4_message going, coming;
+static unsigned going_at;
+static char printed[8192]; /* what has come back since the console last sent */
+static unsigned printed_len;
+static v4_cell printed_from;
+static int ended;
+static v4_cell ended_from, ended_how;
+static unsigned post_passed[32]; /* by node number: POST tallies with no failures seen from it */
+static unsigned post_other; /* tallies with failures */
+
+static int console_give(void *self, v4_cell *value)
+{
+ (void)self;
+ if (going_at >= going.count) return 0;
+ *value = going.word[going_at++];
+ return 1;
+}
+static int console_take(void *self, v4_cell value)
+{
+ (void)self;
+ if (v4_message_word(&coming, value)) {
+ if (v4_message_type(&coming) == V4_MSG_OUTPUT) {
+ unsigned i, chars = v4_message_length(&coming), start = printed_len;
+ for (i = 0; i < chars && printed_len + 1 < sizeof printed; i++) printed[printed_len++] = v4_message_char(&coming, i);
+ printed[printed_len] = 0;
+ printed_from = v4_message_from(&coming);
+ if (strstr(printed + start, "PARITY:V4_POST")) {
+ if (strstr(printed + start, " fail=0\n") && printed_from >= 0 && printed_from < 32) post_passed[printed_from]++;
+ else post_other++;
+ }
+ } else if (v4_message_type(&coming) == V4_MSG_DONE) {
+ ended++;
+ ended_from = v4_message_from(&coming);
+ ended_how = coming.word[V4_MSG_HEADER];
+ }
+ coming.count = 0;
+ }
+ return 1;
+}
+static const v4_device console = { console_take, console_give, 0 };
+
+/* Is there a node that will execute at the next step? A step in which a
+ * node only faulted counts nothing done (exec.h), and it goes on from its
+ * fault handler at the next. */
+static int any_running(void)
+{
+ unsigned k;
+ for (k = 0; k < PLACES; k++) {
+ const v4_fabric_node *x = v4_fabric_node_at(&f, k);
+ if (x && !places[k].asleep && !x->n.stopped && !x->n.asking && !(x->n.reading && !x->n.given)) return 1;
+ }
+ return 0;
+}
+
+/* Send text to a node and let everything that follows from it happen. */
+static const char *tell(v4_cell node, const char *text)
+{
+ unsigned long steps = 0;
+ printed_len = 0; printed[0] = 0; printed_from = -1;
+ ended = 0; ended_from = -1; ended_how = -1;
+ if (!v4_message_text(&going, node, CONSOLE_ID, V4_MSG_TEXT, text, (unsigned)strlen(text))) return "(too long)";
+ going_at = 0;
+ while (steps < 4000000000ul && (v4_fabric_step(&f) != 0 || any_running())) steps++;
+ if (steps >= 4000000000ul) return "(still running)";
+ return printed;
+}
+static int told(v4_cell node, const char *text) { return tell(node, text)[0] == 0 && ended == 1 && ended_how == V4_TEXT_COMPLETED; }
+
+/* Every line of a capsule of FORTH blocks, told to a node from the console:
+ * how many lines did not complete. */
+static unsigned load(v4_cell node, const char *file)
+{
+ static reader r;
+ char line[V4_MANAGE_LINE + 1];
+ unsigned bad = 0;
+ int n;
+ if (!read_file(&r, file)) return 1;
+ while ((n = next_line(&r, line)) >= 0) {
+ line[n] = 0;
+ (void)tell(node, line);
+ if (ended != 1 || ended_how != V4_TEXT_COMPLETED) { if (!bad) { const v4_node *q = &pool[0].n; printf(" %s: \"%s\" -> \"%s\" ended=%d how=%ld; stopped=%d asking=%d port=%u reading=%d rport=%u p=%ld fault=%u@%ld\n", file, line, printed, ended, (long)ended_how, q->stopped, q->asking, q->ask_port, q->reading, q->read_port, (long)q->p, q->fault_kind, (long)q->fault_addr); } bad++; }
+ }
+ return bad;
+}
+
+static v4_node *node_numbered(v4_cell number)
+{
+ unsigned k;
+ for (k = 0; k < PLACES; k++) if (v4_fabric_node_at(&f, k) && v4_fabric_node_at(&f, k)->n.mem[ME] == number) return &v4_fabric_node_at(&f, k)->n;
+ return 0;
+}
+static v4_uheat_t clock_of(v4_cell number)
+{
+ unsigned k;
+ for (k = 0; k < PLACES; k++) if (v4_fabric_node_at(&f, k) && v4_fabric_node_at(&f, k)->n.mem[ME] == number) return v4_fabric_node_at(&f, k)->es.anticlock;
+ return 0;
+}
+static int waiting(v4_cell number)
+{
+ const v4_node *n = node_numbered(number);
+ return n && n->reading && !n->given && n->read_port == V4_PORT_ANY;
+}
+
+int main(void)
+{
+ unsigned i;
+ int place;
+
+ printf("v4 unit tests: V4_CELL_BITS=%d, V4_NODE_WORDS=%u\n", V4_CELL_BITS, (unsigned)V4_NODE_WORDS);
+ if (V4_CELL_BITS != 64) {
+ /* The capsules the unit is built from are the 64-bit products': the
+ * expected results in post79.4th are v3's, on 64-bit cells, and the
+ * nucleus capsule is nucleus-64. POST has never been run on 32-bit
+ * cells (docs/v4.0.0/MESH.md, step 5). */
+ printf(" not run at this width: the capsules are the 64-bit products'\n");
+ return 0;
+ }
+ {
+ static const char *const files[] = { "core.v4", "input.v4", "dict.v4", "codegen.v4", "compile.v4", "quit.v4", "forth.v4",
+ "numout.v4", "words.v4", "system.v4", "qmath.v4", "blocks.v4", "log.v4", "acl.v4" };
+ CHECK(host_load(&tx, &nucleus, files, 14), "the nucleus assembles");
+ }
+ CHECK(v4_text_finish(&tx), "everything is defined: %s", v4_text_error(&tx));
+ if (failures) { printf(" %d checks, %d failures\n", checks, failures); return 1; }
+ w_idle = v4_text_word(&tx, "(IDLE)");
+ w_fault = v4_text_word(&tx, "(FAULTS)");
+ nucleus.mem[DP] = DICT_W * 4; /* the variables, as at switch-on (tools/mkimage.c) */
+ nucleus.mem[LATEST] = v4_text_latest(&tx);
+ nucleus.mem[CFP] = CFS_W;
+ nucleus.mem[BASE] = 10;
+ nucleus.mem[FENCE] = DICT_W;
+ nucleus.mem[BOOT_CELLS] = nucleus.mem[DP];
+ nucleus.mem[BOOT_CELLS + 1] = nucleus.mem[LATEST];
+ nucleus.mem[LOG_LEVEL] = 2;
+ nucleus.mem[CONTEXT] = LATEST;
+ nucleus.mem[CURRENT] = LATEST;
+ nucleus.mem[SRC] = TIB;
+ nucleus.mem[LINE_STATUS] = 1;
+ HOST_MESSAGES_EMPTY(&nucleus);
+ (void)v4_capsule_write(&nucleus, w_idle, f18_put, 0);
+ CHECK(f18_count > 1000 && f18_count < sizeof f18 / sizeof f18[0], "the nucleus is a capsule of F18 code: %u cells", f18_count);
+ memset(&im, 0, sizeof im);
+ im.dp = DP; im.latest = LATEST;
+
+ /* ---- Hera is born empty, and takes in the nucleus ---- */
+ v4_fabric_init(&f, places, PLACES);
+ place = host_born(0);
+ CHECK(place == 0, "a node is born");
+ hera_place = (unsigned)place;
+ for (i = 0; i < 4096 && pool[0].n.mem[i] == 0; i++) { }
+ CHECK(i == 4096 && pool[0].n.p == PORT + (v4_cell)V4_PORT_ANY, "with nothing in it, listening at its ports");
+ CHECK(v4_fabric_wire_device(&f, hera_place, 0, &kernel) && v4_fabric_wire_device(&f, hera_place, 1, &console), "the kernel is on its port 0 and the console on its port 1");
+ going.count = 0; going_at = 0;
+ for (i = 0; i < 400000 && v4_fabric_step(&f) != 0; i++) { }
+ CHECK(given_cells == f18_count && pool[0].n.reading && pool[0].n.read_port == V4_PORT_ANY && !pool[0].n.stopped,
+ "it takes in the nucleus through its port and waits for a message: %u of %u cells", given_cells, f18_count);
+
+ CHECK(told(0, "10 (ME) ! 1 (CONSOLE) ! 1 1 ROUTE") && ended_from == 10, "it is told it is 10, and where the console is");
+ {
+ char text[600];
+ size_t at = 0;
+ for (i = 0; i < V4_MANAGE_WORDS; i++) at += (size_t)snprintf(text + at, sizeof text - at, "%u KERNEL-WORD %s ", FIRST_REQUEST + i, v4_manage_words[i]);
+ CHECK(told(10, text), "and given the words that ask for nodes and capsules: \"%s\"", printed);
+ }
+ CHECK(strcmp(tell(10, "NODE-ME ."), "0 ") == 0 && refused == 0, "it asks where it is, and is answered");
+ CHECK(load(10, "forth79.4th") == 0, "it takes in FORTH-79");
+ CHECK(load(10, "post79.4th") == 0 && post_passed[10] == 1 && post_other == 0, "and passes POST");
+ CHECK(load(10, "hera.4th") == 0, "and its own capsule, which makes it Hera");
+ CHECK(told(10, "(SEAL)"), "that is the system it comes back to");
+
+ /* ---- she births the unit ---- */
+ born_count = 0;
+ {
+ const char *r = tell(10, "10 UNIT");
+ CHECK(ended == 1 && ended_how == V4_TEXT_COMPLETED && ended_from == 10, "Hera births four nodes and joins them: \"%s\"", r);
+ }
+ CHECK(born_count == 4 && refused == 0, "four nodes were asked for: %u", born_count);
+ CHECK(post_passed[11] == 1 && post_passed[12] == 1 && post_passed[13] == 1 && post_passed[14] == 1 && post_other == 0,
+ "each took in the nucleus and FORTH-79 through its port from Hera, and passed POST: %u %u %u %u, %u failed",
+ post_passed[11], post_passed[12], post_passed[13], post_passed[14], post_other);
+ CHECK(parity_count == 4 && parity_number[0] == 11 && parity_number[1] == 12 && parity_number[2] == 13 && parity_number[3] == 14, "the parity of each was recorded: %u", parity_count);
+ CHECK(parity_hash[0] == parity_hash[1] && parity_hash[1] == parity_hash[2] && parity_hash[2] == parity_hash[3],
+ "and their dictionary hashes are the same: %016llx %016llx %016llx %016llx",
+ (unsigned long long)parity_hash[0], (unsigned long long)parity_hash[1], (unsigned long long)parity_hash[2], (unsigned long long)parity_hash[3]);
+ printf(" the four outer nodes: dict_hash=0x%016llx\n", (unsigned long long)parity_hash[0]);
+ CHECK(parity_hash[0] != v4_image_dict_hash(node_numbered(10), &im), "Hera's is not: she has her own capsule");
+ CHECK(waiting(10) && waiting(11) && waiting(12) && waiting(13) && waiting(14), "all five are waiting at their ports");
+ {
+ v4_uheat_t c[5];
+ for (i = 0; i < 5; i++) c[i] = clock_of(10 + (v4_cell)i);
+ for (i = 0; i < 200; i++) (void)v4_fabric_step(&f);
+ for (i = 0; i < 5 && clock_of(10 + (v4_cell)i) == c[i]; i++) { }
+ CHECK(i == 5, "and execute nothing while they wait");
+ }
+
+ /* ---- they talk ---- */
+ CHECK(strcmp(tell(10, "1 2 + ."), "3 ") == 0 && printed_from == 10, "a line typed at the console is done by Hera");
+ for (i = 11; i <= 14; i++) {
+ char text[32], want[16];
+ snprintf(text, sizeof text, "%u 100 * .", i);
+ snprintf(want, sizeof want, "%u ", i * 100);
+ CHECK(strcmp(tell((v4_cell)i, text), want) == 0 && printed_from == (v4_cell)i && ended_from == (v4_cell)i, "text for node %u is done there and what it prints comes back: \"%s\"", i, printed);
+ }
+ CHECK(strcmp(tell(13, "3 4 U* . ."), "0 12 ") == 0, "an outer node has FORTH-79: U* is a word of the capsule");
+ CHECK(strcmp(tell(13, ": SQ DUP * ; 7 SQ ."), "49 ") == 0 && strstr(tell(12, "7 SQ ."), "UNKNOWN WORD") != NULL, "each has its own dictionary");
+ CHECK(strstr(tell(13, "COLD"), "Cold Start") != NULL && strcmp(tell(13, "3 4 U* . ."), "0 12 ") == 0 && strstr(tell(13, "7 SQ ."), "UNKNOWN WORD") != NULL,
+ "COLD on it comes back to the nucleus and FORTH-79");
+ {
+ v4_uheat_t hera = clock_of(10);
+ CHECK(strcmp(tell(11, ": T S\" 65 EMIT\" 12 SEND ; T"), "A") == 0 && printed_from == 12, "an outer node sends text to the one beside it: \"%s\"", printed);
+ hera = clock_of(10);
+ CHECK(strcmp(tell(11, ": X S\" 66 EMIT\" 14 SEND ; X"), "B") == 0 && printed_from == 14, "and to the one across from it, which it is not wired to: \"%s\"", printed);
+ CHECK(clock_of(10) > hera, "that went by a node in between");
+ }
+ CHECK(strcmp(tell(10, "(LOST) @ ."), "0 ") == 0 && strcmp(tell(11, "(LOST) @ ."), "0 ") == 0 && strcmp(tell(14, "(LOST) @ ."), "0 ") == 0, "nothing was let go");
+
+ printf(" %d checks, %d failures\n", checks, failures);
+ return failures != 0;
+}