From 9af442f79314020b2c4abcb8b33da533f999f557 Mon Sep 17 00:00:00 2001 From: rajames Date: Tue, 6 Oct 2026 07:58:00 -0400 Subject: [PATCH] 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 --- capsules/BLOCK_MAP.md | 2 +- docs/v4.0.0/DECOMPOSITION.md | 16 +- docs/v4.0.0/MESH.md | 10 + kernel/Makefile | 2 +- .../amd64/qemu-amd64-20261006-074907.log | 129 ++++++++ .../aarch64/qemu-aarch64-20261006-075150.log | 103 ++++++ .../riscv64/qemu-riscv64-20261006-075532.log | 173 +++++++++++ v4/Makefile | 2 +- v4/README.md | 16 +- v4/include/v4/fabric.h | 99 ++++++ v4/include/v4/node.h | 87 ++++-- v4/src/exec.c | 35 ++- v4/src/fabric.c | 171 ++++++++++ v4/src/node.c | 68 +++- v4/tests/host_map.h | 2 +- v4/tests/test_fabric.c | 292 ++++++++++++++++++ v4/tests/test_port.c | 2 +- 17 files changed, 1170 insertions(+), 39 deletions(-) create mode 100644 logs/20261006-074907/amd64/qemu-amd64-20261006-074907.log create mode 100644 logs/20261006-075150/aarch64/qemu-aarch64-20261006-075150.log create mode 100644 logs/20261006-075532/riscv64/qemu-riscv64-20261006-075532.log create mode 100644 v4/include/v4/fabric.h create mode 100644 v4/src/fabric.c create mode 100644 v4/tests/test_fabric.c diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index eb03f270..ca302eef 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,5 @@ # Capsule Block Manifest — Auto-generated - + diff --git a/docs/v4.0.0/DECOMPOSITION.md b/docs/v4.0.0/DECOMPOSITION.md index 2dc5a47d..6d0ee2a2 100644 --- a/docs/v4.0.0/DECOMPOSITION.md +++ b/docs/v4.0.0/DECOMPOSITION.md @@ -1049,9 +1049,19 @@ The runtime governor moves into hardware as the multi-level Rolling Window of Tr ## 6. Messaging between nodes -Each node has four neighbour ports (`PORT-UP`, `PORT-DOWN`, `PORT-LEFT`, `PORT-RIGHT`) mapped into its -address space. A read from a port blocks until the neighbour writes; a write blocks until the neighbour -reads. This is the GA144 model. No instruction is added: a port is an address. +Each node has ports mapped into its address space. A read from a port blocks until the neighbour writes; +a write blocks until the neighbour reads. This is the GA144 model. No instruction is added: a port is an +address. + +**How many ports, and what they connect to (ruled 2026-10-05 and -06; `MESH.md`).** This section first +gave a node four ports, `PORT-UP`, `PORT-DOWN`, `PORT-LEFT` and `PORT-RIGHT`: a flat grid. That is +withdrawn. The number of ports is a build parameter of the node (`V4_PORTS`), as cell width and node +memory are, and which port connects to what is a table that can change while the system runs — "not +constrained by a 3D world; other geometries might be better." The ports are numbered, not named for +directions. Two addresses follow them: "any port", a read of which takes from whichever port has a +neighbour writing, and the port the last such read came from. A node at reset has `P` at "any port" and +executes what a neighbour sends it. Built and tested in the golden model 2026-10-06 (`v4/src/node.c`, +`exec.c`, `fabric.c`; `v4/tests/test_fabric.c`). ### 6.1 Packet format (proposal) diff --git a/docs/v4.0.0/MESH.md b/docs/v4.0.0/MESH.md index f8f017ba..02e169bb 100644 --- a/docs/v4.0.0/MESH.md +++ b/docs/v4.0.0/MESH.md @@ -252,6 +252,16 @@ Each is tested, committed and pushed before the next. words; an empty node is filled through its port and runs what it was sent; a word is passed on by a node in between; a node waiting executes nothing; a node is put to sleep, woken, and removed while looping. + **Done 2026-10-06.** `v4/src/node.c` (the ports, `v4_node_born`), + `v4/src/exec.c` (a fetch from a port waits; execution from a port; the + slot a blocked node goes on from), `v4/src/fabric.c`. `V4_PORTS` is 8. + `v4/tests/test_fabric.c`, 53 checks at both cell widths and under ASan + and UBSan: all of the above, with the empty node filled once by a device + and once by another node holding the capsule in its own memory; the + fabric given more room while nodes run; what cannot be wired refused. + The single-node products are unchanged by it: `hosted-check` on three + ISAs, and the three bare-metal boots with lines typed at each prompt + (`logs/20261006-074907`, `-075150`, `-075532`). 2. **The nucleus as a capsule, and an empty node made a StarForth node through its port (section 5).** 3. **Messages (section 6):** a StarForth node that reads messages when diff --git a/kernel/Makefile b/kernel/Makefile index 17416ec2..42932e8a 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -613,7 +613,7 @@ V4_DEFS := -DSTARFORTH_V4=1 -Iv4/include \ KERNEL_CFLAGS += $(V4_DEFS) LOADER_CFLAGS += $(V4_DEFS) -V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c) +V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c) V4_IMAGE_C := $(BUILD_DIR)/v4_image_64.c LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c diff --git a/logs/20261006-074907/amd64/qemu-amd64-20261006-074907.log b/logs/20261006-074907/amd64/qemu-amd64-20261006-074907.log new file mode 100644 index 00000000..13c1a363 --- /dev/null +++ b/logs/20261006-074907/amd64/qemu-amd64-20261006-074907.log @@ -0,0 +1,129 @@ +[=3hBdsDxe: 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) +[=3hStarKernel 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 07:48:38 + +UEFI BootServices: EXITED + +=== StarKernel Boot Information === +Memory map entries: 131 +Total memory: 1023 MB +Usable memory: 968 MB +=================================== + +PMM initialized. +PMM statistics: + Total pages: 249243 + Free pages : 247720 + 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=2101992017 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=1007298008, tick_hz=100, initial_count=10072980 +APIC Timer: configured (masked, ready to start) +Heartbeat: init done +Kernel initialization complete. +Boot successful! + +StarForth v4: one host node, the F18-derived engine +PARITY:V4_NUCLEUS words=298 image_hash=0xd982cf093aadb8b9 +V4: capsule v4:forth79.4th signature: missing (unsigned) +PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x88fb8dcbb051f7b9 +V4: capsule v4:post79.4th signature: missing (unsigned) +PARITY:V4_POST tests=550 pass=550 fail=0 +PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x843716689a31b60a +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-075150/aarch64/qemu-aarch64-20261006-075150.log b/logs/20261006-075150/aarch64/qemu-aarch64-20261006-075150.log new file mode 100644 index 00000000..9743d97b --- /dev/null +++ b/logs/20261006-075150/aarch64/qemu-aarch64-20261006-075150.log @@ -0,0 +1,103 @@ +UEFI firmware (version 2025.11-3ubuntu7.3 built at 15:40:26 on Sep 23 2026) +[=3hBdsDxe: 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) +[=3hStarKernel 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 07:51:22 + +UEFI BootServices: EXITED + +=== StarKernel Boot Information === +Memory map entries: 108 +Total memory: 4093 MB +Usable memory: 4056 MB +=================================== + +PMM initialized. +PMM statistics: + Total pages: 1039860 + Free pages : 1038449 + 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 +PARITY:V4_NUCLEUS words=298 image_hash=0xd982cf093aadb8b9 +V4: capsule v4:forth79.4th signature: missing (unsigned) +PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x88fb8dcbb051f7b9 +V4: capsule v4:post79.4th signature: missing (unsigned) +PARITY:V4_POST tests=550 pass=550 fail=0 +PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x843716689a31b60a +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-075532/riscv64/qemu-riscv64-20261006-075532.log b/logs/20261006-075532/riscv64/qemu-riscv64-20261006-075532.log new file mode 100644 index 00000000..7b7c24a6 --- /dev/null +++ b/logs/20261006-075532/riscv64/qemu-riscv64-20261006-075532.log @@ -0,0 +1,173 @@ + +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 +[=3hRISC-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 +[=3hStarKernel 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 = 0x00000000bde958f8 +riscv64: satp cleared -- Bare mode, explicit (item 4.3.5a) + + + _____ _ _ __ _ + / ____| | | |/ / | | + | (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| | + \___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ | + ____) | || (_| | | | . \ __/ | | | | | __/ | + |_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_| + +LithosAnanke v2.1.0 +Architecture: riscv64 +Build: Oct 6 2026 07:54:53 + +UEFI BootServices: EXITED + +=== StarKernel Boot Information === +Memory map entries: 100 +Total memory: 1020 MB +Usable memory: 977 MB +=================================== + +PMM initialized. +PMM statistics: + Total pages: 250773 + Free pages : 250121 + 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 +PARITY:V4_NUCLEUS words=298 image_hash=0xd982cf093aadb8b9 +V4: capsule v4:forth79.4th signature: missing (unsigned) +PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x88fb8dcbb051f7b9 +V4: capsule v4:post79.4th signature: missing (unsigned) +PARITY:V4_POST tests=550 pass=550 fail=0 +PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x843716689a31b60a +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/Makefile b/v4/Makefile index 255c101b..c42e8d5a 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -61,7 +61,7 @@ capsule_dir := -DV4_CAPSULE_DIR='"$(HERE)/capsule"' # The hosted system below and the bare-metal kernel (kernel/Makefile, # STARFORTH_V4=1) link the 64-bit one. docs/v4.0.0/NUCLEUS.md. HOST_DEFS := -DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) -ENGINE_SRCS := $(addprefix $(HERE)/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c) +ENGINE_SRCS := $(addprefix $(HERE)/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c) define IMAGE_RULE $(BINDIR)/mkimage-$(1): $(HERE)/tools/mkimage.c $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/tests/host_map.h $(HERE)/Makefile diff --git a/v4/README.md b/v4/README.md index ab28be40..94aa4ca7 100644 --- a/v4/README.md +++ b/v4/README.md @@ -59,7 +59,7 @@ State, 2026-10-05, after `ENGINE.md` step 1: all six builds start the nucleus, load `v4:forth79.4th`, pass POST and reach `ok>`, with the same lines: ``` -PARITY:V4_NUCLEUS words=298 image_hash=0x2e02147b54f9ff47 +PARITY:V4_NUCLEUS words=298 image_hash=0xd982cf093aadb8b9 PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=... dict_hash=... PARITY:V4_POST tests=550 pass=550 fail=0 PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=... dict_hash=... @@ -68,8 +68,8 @@ POST: PASSED ok> ``` -Bare-metal logs: `logs/20261005-193045/amd64/`, `logs/20261005-193307/aarch64/`, -`logs/20261005-193636/riscv64/`. On each, seven lines were then typed at the +Bare-metal logs: `logs/20261006-074907/amd64/`, `logs/20261006-075150/aarch64/`, +`logs/20261006-075532/riscv64/`. On each, seven lines were then typed at the prompt through the serial port — a definition, its use with the capsule's `U*`, `COLD`, the capsule word again, a `FORGET` of it (refused), a kernel word no one serves, and `BYE` — and each was answered as the hosted binary @@ -101,6 +101,16 @@ product is its own and may become a hosted SDK. The bare-metal product is to be LithosAnanke with the node in the StarForth VM's place, and everything about word records, the fleet and identity is its alone. +**Nodes that talk** (`docs/v4.0.0/MESH.md`) are the next step, ahead of +making v4 equal v3 on bare metal. Its first part is built and tested in +the engine: a node has `V4_PORTS` ports; a read from one blocks until the +neighbour writes and a write until the neighbour reads; a node is born +empty with `P` at its ports and executes what a neighbour sends it; and +the fabric (`v4/src/fabric.c`) is the nodes there are and the table of how +their ports are wired, both of which change while they run. The products +below are still one node each; they become the five nodes at `MESH.md` +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/include/v4/fabric.h b/v4/include/v4/fabric.h new file mode 100644 index 00000000..b9b2eaae --- /dev/null +++ b/v4/include/v4/fabric.h @@ -0,0 +1,99 @@ +/* fabric.h -- the nodes there are, how their ports are wired, and time + * passing for all of them at once. docs/v4.0.0/MESH.md 4.3. + * + * On the fabric every node runs at the same time and a node with nothing to + * do is blocked at a port. Here that is: every node that is awake and not + * blocked executes one instruction word, and then every write that has a + * reader waiting on the other end of its wire is handed over. There is no + * choice of whose turn it is. + * + * THE GEOMETRY IS DATA. Which port connects to what is a table, and it can + * be changed while the nodes run; nodes can be added and removed while they + * run. Nothing here knows what shape the wiring makes. + * + * Nothing here knows what the nodes are for, either: no message, no + * capsule, no StarForth. And nothing here uses the C library: whoever owns + * the fabric supplies the memory for it and for each node. + */ +#ifndef V4_FABRIC_H +#define V4_FABRIC_H + +#include "v4/exec.h" + +/* What is on the other end of a port that is not a node: a console, a disk, + * the kernel that serves a node's requests. Either function may be 0. */ +typedef struct { + int (*take)(void *self, v4_cell value); /* the node wrote `value`: 1 if it is taken, 0 if not yet */ + int (*give)(void *self, v4_cell *value); /* the node is reading: 1 with a word in *value, 0 if not yet */ + void *self; +} v4_device; + +/* One end of a wire. */ +enum { V4_WIRE_NONE = 0, V4_WIRE_NODE, V4_WIRE_DEVICE }; +typedef struct { + int kind; + unsigned node, port; /* V4_WIRE_NODE: the other node and its port */ + const v4_device *device; /* V4_WIRE_DEVICE */ +} v4_wire; + +/* A node, with what the engine needs to run it. Its owner supplies one of + * these for every node there is to be. */ +typedef struct { + v4_node n; + v4_exec_state es; + v4_heat h; +} v4_fabric_node; + +/* A place in the fabric: a node, or nothing. */ +typedef struct { + v4_fabric_node *node; /* 0: no node here */ + int asleep; + v4_wire wire[V4_PORTS]; +} v4_place; + +typedef struct { + v4_place *place; + unsigned capacity; +} v4_fabric; + +/* An empty fabric with room for `capacity` nodes, in `places`. */ +void v4_fabric_init(v4_fabric *f, v4_place *places, unsigned capacity); + +/* 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); + +/* A node is born (node.h, v4_node_born): empty, its ports at `port_base`, + * wired to nothing, awake and blocked reading its ports. Returns its + * number, or -1 if there is no room. */ +int v4_fabric_add(v4_fabric *f, v4_fabric_node *storage, v4_cell port_base); + +/* A node is removed, whatever it was doing. Every wire to it is cut; a + * neighbour blocked on one of them stays blocked, as it would on a port + * with nothing there. Returns the storage it was given, or 0. */ +v4_fabric_node *v4_fabric_remove(v4_fabric *f, unsigned id); + +/* The node numbered `id`, or 0 if there is none. */ +v4_fabric_node *v4_fabric_node_at(const v4_fabric *f, unsigned id); + +/* Wire port `pa` of node `a` to port `pb` of node `b`, or to a device; or + * cut whatever is on it. Whatever was on a port before is cut, at both + * ends. Each returns 1, or 0 if a node or a port is not there. */ +int v4_fabric_wire(v4_fabric *f, unsigned a, unsigned pa, unsigned b, unsigned pb); +int v4_fabric_wire_device(v4_fabric *f, unsigned a, unsigned pa, const v4_device *device); +int v4_fabric_unwire(v4_fabric *f, unsigned a, unsigned pa); + +/* A node that is asleep executes nothing, and nothing is handed to it or + * taken from it. It can be put to sleep and woken at any instruction + * word. */ +void v4_fabric_sleep(v4_fabric *f, unsigned id); +void v4_fabric_wake(v4_fabric *f, unsigned id); + +/* Time passes: every node that is awake and not blocked executes one + * instruction word; then every write with a reader waiting on the other end + * of its wire is handed over, and every device is asked. Returns how many + * instructions were executed and words handed over together: 0 means + * nothing can happen until something outside changes. */ +unsigned v4_fabric_step(v4_fabric *f); + +#endif /* V4_FABRIC_H */ diff --git a/v4/include/v4/node.h b/v4/include/v4/node.h index 2810ff77..33f4c2e7 100644 --- a/v4/include/v4/node.h +++ b/v4/include/v4/node.h @@ -36,6 +36,14 @@ #include "v4/stack.h" /* Words of node memory. Overridable at build time. */ +/* How many ports a node has. A build parameter, as the cell width and the + * node's memory are: the geometry the nodes are wired in is data, and a node + * has as many ports as the geometry in use needs (docs/v4.0.0/MESH.md 4.1). */ +#ifndef V4_PORTS +#define V4_PORTS 8u +#endif +#define V4_PORT_ANY V4_PORTS /* not a port: "whichever port has a neighbour writing" */ + #ifndef V4_NODE_WORDS #define V4_NODE_WORDS 1024u #endif @@ -91,9 +99,16 @@ typedef struct { /* The port (DECOMPOSITION.md section 6): a write to it blocks the node * until its neighbour has taken what was written. See * v4_node_port_attach below. */ - v4_cell port; /* its word address, or -1: no port */ - int asking; /* non-zero: blocked, having written `request` */ + v4_cell port; /* word address of port 0, or -1: no ports */ + int asking; /* non-zero: blocked, having written `request` to port `ask_port` */ v4_cell request; /* what it wrote */ + unsigned ask_port; /* to which port */ + int reading; /* non-zero: blocked, wanting a word from port `read_port` */ + unsigned read_port; /* a port, or V4_PORT_ANY */ + int given; /* non-zero: `given_value` has come from port `given_port` and not been taken yet */ + v4_cell given_value; + unsigned given_port; + unsigned last_from; /* the port the last read from "any" took its word from */ /* DSTACK-DEPTH and RSTACK-DEPTH (D-16). See v4_node_stack_regs_attach. */ v4_cell dstack_reg; /* its word address, or -1 */ @@ -156,28 +171,62 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value); * Passing -1 detaches. The address is the caller's choice. */ void v4_node_console_attach(v4_node *n, v4_cell addr); -/* THE PORT. A node reaches its neighbour through a port, and a port is an - * address (DECOMPOSITION.md section 6): "a write blocks until the neighbour - * reads. This is the GA144 model. No instruction is added." +/* THE PORTS. A node reaches its neighbours through its ports, and a port is + * an address (DECOMPOSITION.md section 6): "a read from a port blocks until + * the neighbour writes; a write blocks until the neighbour reads. This is + * the GA144 model. No instruction is added." docs/v4.0.0/MESH.md 4.1. * - * After v4_node_port_attach(n, addr), a store to word address `addr` does - * not write memory: the node keeps the value in n->request, sets n->asking - * and is blocked. A blocked node executes nothing -- v4_exec_step_word - * returns 0 and changes nothing -- until its neighbour calls - * v4_node_port_served(n). It then goes on from the opcode after the store, - * in the same instruction word if there are any left. + * After v4_node_port_attach(n, base) the node's V4_PORTS ports are the word + * addresses base .. base + V4_PORTS - 1. Two more follow them: + * base + V4_PORTS "any port": a read here takes from whichever port + * has a neighbour writing + * base + V4_PORTS + 1 which port the last read from "any" came from + * (read only) + * None of them is memory while the ports are attached. * - * This is how a node asks its kernel for something (docs/v4.0.0/ENGINE.md - * 3.3): the neighbour on the port is the kernel, what is written is the - * number of the request, and the arguments and results are on the node's - * data stack, which the kernel may use while the node is blocked. Only the - * write is here. Nothing reads from a port yet, and a fetch from the - * address is a fetch from memory. + * A WRITE. A store to a port does not write memory: the node keeps the + * value in n->request and the port in n->ask_port, sets n->asking and is + * blocked. When its neighbour has taken the value -- v4_node_port_served -- + * it goes on from the opcode after the store, in the same instruction word + * if there are any left. * - * A node has no port until one is attached: v4_node_reset detaches it. */ -void v4_node_port_attach(v4_node *n, v4_cell addr); + * A READ. A fetch from a port -- @ @b @+, the literal fetch @p, or the + * fetch of an instruction word when P is a port -- does not happen until + * there is a word to fetch. The node sets n->reading and n->read_port and + * is blocked, the opcode not yet executed. When its neighbour writes -- + * v4_node_port_give -- the fetch is made, and it gets that word. When P is + * a port it is not advanced by a fetch: the node goes on executing what + * arrives there, as an F18 node does. + * + * A blocked node executes nothing: v4_exec_step_word returns 0 and changes + * nothing. Who the neighbour is, and whether there is one, is not the + * node's to know (fabric.h). A store to "any port" or to the "which port" + * address is an address fault. + * + * A node has no ports until they are attached: v4_node_reset detaches them. */ +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); +/* 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); + +/* Which port `addr` is: 0 .. V4_PORTS - 1, V4_PORT_ANY, V4_PORTS + 1 for the + * "which port" address, or -1 if it is none of them. */ +int v4_node_port_index(const v4_node *n, v4_cell addr); + +/* 1 if a fetch from `addr` can be made now: it is not a port, or the word + * has been given. 0 if it is a port with nothing given: the node is then + * blocked reading it, and the fetch must not be made. */ +int v4_node_read_ready(v4_node *n, v4_cell addr); + +/* A NODE NEWLY BORN (docs/v4.0.0/MESH.md 4.2). Reset, its ports attached at + * `base`, and P at "any port": it is blocked reading its ports, and will + * execute what a neighbour sends it. Nothing else is in it. */ +void v4_node_born(v4_node *n, v4_cell base); + /* CONSOLE-RX and CONSOLE-STATUS, the console's receive side, on the * single-node model. * diff --git a/v4/src/exec.c b/v4/src/exec.c index 8a2bfbc8..cf99a66a 100644 --- a/v4/src/exec.c +++ b/v4/src/exec.c @@ -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++; diff --git a/v4/src/fabric.c b/v4/src/fabric.c new file mode 100644 index 00000000..18168be5 --- /dev/null +++ b/v4/src/fabric.c @@ -0,0 +1,171 @@ +/* fabric.c -- nodes, wiring, and time passing for all of them. See fabric.h. + * + * Nothing here uses the C library: the bare-metal kernel links this file. + */ +#include "v4/fabric.h" + +static void clear_place(v4_place *p) +{ + unsigned k; + p->node = 0; + p->asleep = 0; + for (k = 0; k < V4_PORTS; k++) { p->wire[k].kind = V4_WIRE_NONE; p->wire[k].node = 0; p->wire[k].port = 0; p->wire[k].device = 0; } +} + +void v4_fabric_init(v4_fabric *f, v4_place *places, unsigned capacity) +{ + unsigned i; + f->place = places; + f->capacity = capacity; + for (i = 0; i < capacity; i++) clear_place(&places[i]); +} + +void v4_fabric_grow(v4_fabric *f, v4_place *places, unsigned capacity) +{ + unsigned i; + if (capacity < f->capacity) return; + for (i = 0; i < f->capacity; i++) places[i] = f->place[i]; + for (; i < capacity; i++) clear_place(&places[i]); + f->place = places; + f->capacity = capacity; +} + +int v4_fabric_add(v4_fabric *f, v4_fabric_node *storage, v4_cell port_base) +{ + unsigned i; + for (i = 0; i < f->capacity && f->place[i].node; i++) { } + if (i == f->capacity || !storage) return -1; + clear_place(&f->place[i]); + f->place[i].node = storage; + v4_node_born(&storage->n, port_base); + v4_exec_reset(&storage->es); + v4_heat_reset(&storage->h); + return (int)i; +} + +v4_fabric_node *v4_fabric_node_at(const v4_fabric *f, unsigned id) +{ + return id < f->capacity ? f->place[id].node : 0; +} + +static int there(const v4_fabric *f, unsigned id, unsigned port) +{ + return id < f->capacity && f->place[id].node && port < V4_PORTS; +} + +/* cut what is on a port, at both ends */ +static void cut(v4_fabric *f, unsigned a, unsigned pa) +{ + v4_wire *w = &f->place[a].wire[pa]; + if (w->kind == V4_WIRE_NODE && w->node < f->capacity && w->port < V4_PORTS) { + v4_wire *other = &f->place[w->node].wire[w->port]; + if (other->kind == V4_WIRE_NODE && other->node == a && other->port == pa) { other->kind = V4_WIRE_NONE; other->device = 0; } + } + w->kind = V4_WIRE_NONE; + w->device = 0; +} + +int v4_fabric_wire(v4_fabric *f, unsigned a, unsigned pa, unsigned b, unsigned pb) +{ + if (!there(f, a, pa) || !there(f, b, pb) || (a == b && pa == pb)) return 0; + cut(f, a, pa); + cut(f, b, pb); + f->place[a].wire[pa].kind = V4_WIRE_NODE; f->place[a].wire[pa].node = b; f->place[a].wire[pa].port = pb; + f->place[b].wire[pb].kind = V4_WIRE_NODE; f->place[b].wire[pb].node = a; f->place[b].wire[pb].port = pa; + return 1; +} + +int v4_fabric_wire_device(v4_fabric *f, unsigned a, unsigned pa, const v4_device *device) +{ + if (!there(f, a, pa) || !device) return 0; + cut(f, a, pa); + f->place[a].wire[pa].kind = V4_WIRE_DEVICE; + f->place[a].wire[pa].device = device; + return 1; +} + +int v4_fabric_unwire(v4_fabric *f, unsigned a, unsigned pa) +{ + if (!there(f, a, pa)) return 0; + cut(f, a, pa); + return 1; +} + +v4_fabric_node *v4_fabric_remove(v4_fabric *f, unsigned id) +{ + v4_fabric_node *storage; + unsigned k; + if (id >= f->capacity || !f->place[id].node) return 0; + for (k = 0; k < V4_PORTS; k++) cut(f, id, k); + storage = f->place[id].node; + clear_place(&f->place[id]); + return storage; +} + +void v4_fabric_sleep(v4_fabric *f, unsigned id) +{ + if (id < f->capacity && f->place[id].node) f->place[id].asleep = 1; +} + +void v4_fabric_wake(v4_fabric *f, unsigned id) +{ + if (id < f->capacity && f->place[id].node) f->place[id].asleep = 0; +} + +static int awake(const v4_fabric *f, unsigned id) +{ + return id < f->capacity && f->place[id].node && !f->place[id].asleep; +} + +/* What node `a` wrote to one of its ports, handed to whatever is there. */ +static unsigned hand_over_write(v4_fabric *f, unsigned a) +{ + v4_node *n = &f->place[a].node->n; + const v4_wire *w = &f->place[a].wire[n->ask_port]; + + if (w->kind == V4_WIRE_DEVICE) { + if (w->device->take && w->device->take(w->device->self, n->request)) { v4_node_port_served(n); return 1; } + } else if (w->kind == V4_WIRE_NODE && awake(f, w->node)) { + v4_node *r = &f->place[w->node].node->n; + if (r->reading && !r->given && (r->read_port == w->port || r->read_port == V4_PORT_ANY)) { + v4_node_port_give(r, w->port, n->request); + v4_node_port_served(n); + return 1; + } + } + return 0; +} + +/* Node `a` is reading: a device on that port, or on any of them, is asked. */ +static unsigned hand_over_read(v4_fabric *f, unsigned a) +{ + v4_node *n = &f->place[a].node->n; + unsigned first = n->read_port == V4_PORT_ANY ? 0 : n->read_port; + unsigned last = n->read_port == V4_PORT_ANY ? V4_PORTS - 1 : n->read_port; + unsigned k; + v4_cell value; + + for (k = first; k <= last; k++) { + const v4_wire *w = &f->place[a].wire[k]; + if (w->kind == V4_WIRE_DEVICE && w->device->give && w->device->give(w->device->self, &value)) { + v4_node_port_give(n, k, value); + return 1; + } + } + return 0; +} + +unsigned v4_fabric_step(v4_fabric *f) +{ + unsigned i, done = 0; + + 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); + + for (i = 0; i < f->capacity; i++) + if (awake(f, i) && f->place[i].node->n.asking) done += hand_over_write(f, i); + 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); + + return done; +} diff --git a/v4/src/node.c b/v4/src/node.c index a1746239..211a54a8 100644 --- a/v4/src/node.c +++ b/v4/src/node.c @@ -120,9 +120,18 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value) n->console_dropped++; return; } - /* The port: the node keeps what it wrote and is blocked until its - * neighbour has taken it. -1 when no port is attached. */ - if (addr == n->port) { n->request = value; n->asking = 1; return; } + /* A port: the node keeps what it wrote and is blocked until its + * neighbour has taken it. */ + { + int port = v4_node_port_index(n, addr); + if (port >= 0 && port < (int)V4_PORTS) { + n->request = value; + n->ask_port = (unsigned)port; + n->asking = 1; + return; + } + if (port >= 0) { v4_node_fault(n, V4_FAULT_ADDRESS, addr); return; } /* "any" and "which port" are not written to */ + } /* The stack registers: a store empties the stack. -1 when not attached. */ if (addr == n->dstack_reg) { v4_dstack_clear(&n->ds); return; } if (addr == n->rstack_reg) { v4_rstack_clear(&n->rs); return; } @@ -147,6 +156,17 @@ v4_cell v4_node_fetch(v4_node *n, v4_cell addr) if (n->input_pos == n->input_len) n->input_pos = n->input_len = 0; return c; } + { + int port = v4_node_port_index(n, addr); + if (port == (int)V4_PORTS + 1) return (v4_cell)n->last_from; + if (port >= 0) { + /* the word its neighbour wrote; v4_node_read_ready has said it is there */ + if (!n->given) return 0; + n->given = 0; + if (port == (int)V4_PORT_ANY) n->last_from = n->given_port; + return n->given_value; + } + } if (addr == n->dstack_reg) return (v4_cell)n->ds.depth; if (addr == n->rstack_reg) return (v4_cell)n->rs.depth; return v4_node_load(n, addr); @@ -182,11 +202,49 @@ void v4_node_console_attach(v4_node *n, v4_cell addr) n->console_dropped = 0; } -void v4_node_port_attach(v4_node *n, v4_cell addr) +void v4_node_port_attach(v4_node *n, v4_cell base) { - n->port = addr; + n->port = base; n->asking = 0; n->request = 0; + n->ask_port = 0; + n->reading = 0; + n->read_port = 0; + n->given = 0; + n->given_value = 0; + n->given_port = 0; + n->last_from = 0; +} + +int v4_node_port_index(const v4_node *n, v4_cell addr) +{ + if (n->port < 0 || addr < n->port || addr > n->port + (v4_cell)V4_PORTS + 1) return -1; + return (int)(addr - n->port); +} + +int v4_node_read_ready(v4_node *n, v4_cell addr) +{ + int port = v4_node_port_index(n, addr); + if (port < 0 || port == (int)V4_PORTS + 1) return 1; + if (n->given && (port == (int)V4_PORT_ANY || (unsigned)port == n->given_port)) return 1; + n->reading = 1; + n->read_port = (unsigned)port; + return 0; +} + +void v4_node_port_give(v4_node *n, unsigned port, v4_cell value) +{ + n->given = 1; + n->given_value = value; + n->given_port = port; + n->reading = 0; +} + +void v4_node_born(v4_node *n, v4_cell base) +{ + v4_node_reset(n); + v4_node_port_attach(n, base); + n->p = base + (v4_cell)V4_PORT_ANY; } void v4_node_port_served(v4_node *n) diff --git a/v4/tests/host_map.h b/v4/tests/host_map.h index 3f2f33b8..d1b6e34c 100644 --- a/v4/tests/host_map.h +++ b/v4/tests/host_map.h @@ -68,7 +68,7 @@ #define EMIT_HOOK (BUF0_W - 7) /* the xt of a word EMIT gives its character to, or 0: the console */ #define CATCH (BUF0_W - 8) /* non-zero: a line's error is recorded here, -1, and the line ends " ok" */ #define LINE_STATUS (BUF0_W - 9) /* how the last line ended: 0 QUIT, 1 completed, 2 an error */ -#define PORT (BUF0_W - 10) /* the node's port: a write to it blocks until its neighbour, the kernel, has served it */ +#define PORT (BUF0_W - 32) /* the node's ports (node.h): V4_PORTS of them, then "any port" and "which port". Port 0 is where its requests go. */ #define WORD_DEFINED (BUF0_W - 11) /* the xt of the word that is told of each new entry, or 0 */ #define WORD_FORGOTTEN (BUF0_W - 12) /* the xt of the word that is told when entries go, or 0 */ #define LOG_LEVEL (BUF0_W - 5) /* the level at or below which a message is printed */ diff --git a/v4/tests/test_fabric.c b/v4/tests/test_fabric.c new file mode 100644 index 00000000..bfd56fe9 --- /dev/null +++ b/v4/tests/test_fabric.c @@ -0,0 +1,292 @@ +/* test_fabric.c -- nodes that talk: ports that block on a read as well as a + * write, a node born empty that executes what arrives at its port, and the + * fabric of nodes and wiring that changes while they run. + * docs/v4.0.0/MESH.md section 4 and step 1. + * + * At the level of opcodes: no word of any vocabulary is defined here. + */ +#include "v4/asm.h" +#include "v4/fabric.h" +#include +#include + +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) + +/* The memory map is open (D-4); the test chooses where the ports are. */ +#define PB ((v4_cell)(V4_NODE_WORDS - 16u)) /* port 0 */ +#define ANY (PB + (v4_cell)V4_PORT_ANY) +#define ORG ((v4_cell)16) /* where a test's programme starts */ +#define OUT ((v4_cell)900) /* where it leaves what it found */ + +#define NODES 8 +static v4_fabric_node pool[NODES]; +static v4_place places[4], more_places[NODES]; +static v4_fabric f; +static v4_asm as; + +/* the node with that number in the fabric */ +#define ND(id) (v4_fabric_node_at(&f, (id))) + +#define O(name) v4_asm_op(&as, V4_OP_##name) +#define LIT(v) v4_asm_lit(&as, (v4_cell)(v)) + +/* A node with a programme already in it, started at ORG: for the tests that + * are about talking and not about being born. */ +static unsigned loaded(unsigned k) +{ + int id = v4_fabric_add(&f, &pool[k], PB); + pool[k].n.p = ORG; + v4_asm_begin(&as, &pool[k].n, ORG); + return (unsigned)id; +} + +/* the programme ends by waiting at a port nothing is wired to */ +static void wait_for_ever(void) { LIT(PB + 7); O(BANG_B); O(FETCH_B); (void)v4_asm_label(&as); } /* and the last word is written out */ + +/* Let time pass until nothing more can happen, or `max` steps. */ +static unsigned settle(unsigned max) +{ + unsigned steps = 0; + while (steps < max && v4_fabric_step(&f) != 0) steps++; + return steps; +} + +/* ---- a device: a neighbour that is not a node ---- */ +typedef struct { const v4_cell *words; unsigned count, at; v4_cell took[16]; unsigned took_n; } stream; +static int stream_give(void *self, v4_cell *value) +{ + stream *s = (stream *)self; + if (s->at >= s->count) return 0; + *value = s->words[s->at++]; + return 1; +} +static int stream_take(void *self, v4_cell value) +{ + stream *s = (stream *)self; + if (s->took_n >= 16) return 0; + s->took[s->took_n++] = value; + return 1; +} + +/* What a neighbour sends an empty node to put `count` words at `addr` and + * start there (MESH.md section 5): for the F18, and for this node, code a + * node executes straight from its port. */ +static unsigned capsule(v4_cell *out, const v4_cell *words, unsigned count, v4_cell addr) +{ + static const unsigned set_up[V4_SLOT_COUNT] = { V4_OP_FETCH_P, V4_OP_BANG_A, V4_OP_FETCH_P, V4_OP_PUSH, V4_OP_NOP, V4_OP_NOP }; + static const unsigned pump[V4_SLOT_COUNT] = { V4_OP_FETCH_P, V4_OP_STORE_INC, V4_OP_UNEXT, V4_OP_NOP, V4_OP_NOP, V4_OP_NOP }; + static v4_node scratch; + v4_asm j; + unsigned at = 0, i; + + out[at++] = (v4_cell)v4_iword_assemble(set_up, 0); /* @p a! @p push */ + out[at++] = addr; + out[at++] = (v4_cell)(count - 1); + out[at++] = (v4_cell)v4_iword_assemble(pump, 0); /* @p !+ unext */ + for (i = 0; i < count; i++) out[at++] = words[i]; + v4_node_reset(&scratch); + v4_asm_begin(&j, &scratch, 0); + v4_asm_branch(&j, V4_OP_JUMP, addr); /* jump addr */ + (void)v4_asm_label(&j); + out[at++] = scratch.mem[0]; + return at; +} + +int main(void) +{ + unsigned a, b, c, i, steps; + + printf("v4 fabric tests: V4_CELL_BITS=%d, %u ports to a node\n", V4_CELL_BITS, (unsigned)V4_PORTS); + + /* ---- a node newly born ---- */ + v4_fabric_init(&f, places, 4); + CHECK(v4_fabric_node_at(&f, 0) == 0 && v4_fabric_step(&f) == 0, "an empty fabric: no nodes, and nothing happens"); + a = (unsigned)v4_fabric_add(&f, &pool[0], PB); + CHECK(a == 0 && v4_fabric_node_at(&f, 0) == &pool[0], "a node is born and has a number"); + CHECK(pool[0].n.p == ANY && pool[0].n.ds.depth == 0 && pool[0].n.rs.depth == 0, "its P is at \"any port\" and its stacks are empty"); + for (i = 0; i < V4_NODE_WORDS && pool[0].n.mem[i] == 0; i++) { } + CHECK(i == V4_NODE_WORDS, "and there is nothing in its memory"); + CHECK(v4_fabric_step(&f) == 0 && pool[0].n.reading && pool[0].n.read_port == V4_PORT_ANY, "it is blocked reading its ports"); + CHECK(v4_fabric_step(&f) == 0 && pool[0].es.anticlock == 0, "and executes nothing while it waits: its anti-clock has not moved"); + CHECK(v4_fabric_remove(&f, a) == &pool[0] && v4_fabric_node_at(&f, 0) == 0, "it is removed, and its storage comes back"); + + /* ---- two nodes exchange words ---- */ + v4_fabric_init(&f, places, 4); + a = loaded(0); /* writes three words, then reads one */ + LIT(PB + 2); O(BANG_B); LIT(10); O(STORE_B); LIT(20); O(STORE_B); LIT(12); O(STORE_B); + O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); + wait_for_ever(); + CHECK(v4_asm_ok(&as), "the writer assembles"); + b = loaded(1); /* reads three, writes their sum back */ + LIT(PB + 5); O(BANG_B); O(FETCH_B); O(FETCH_B); O(FETCH_B); O(ADD); O(ADD); O(STORE_B); + wait_for_ever(); + CHECK(v4_asm_ok(&as), "the reader assembles"); + + steps = settle(1000); + CHECK(ND(a)->n.asking && ND(a)->n.ask_port == 2 && ND(a)->n.request == 10, "with no wire, the writer is blocked at its first write"); + CHECK(ND(b)->n.reading && ND(b)->n.read_port == 5, "and the reader at its first read"); + CHECK(v4_fabric_wire(&f, a, 2, b, 5), "port 2 of one is wired to port 5 of the other, while they wait"); + steps = settle(1000); + CHECK(steps < 1000, "then everything that can happen happens, and stops: %u steps", steps); + CHECK(ND(a)->n.mem[OUT] == 42, "three words went one way and their sum came back: %ld", (long)ND(a)->n.mem[OUT]); + CHECK(ND(a)->n.reading && ND(b)->n.reading, "and both are waiting again"); + { + v4_uheat_t ca = ND(a)->es.anticlock, cb = ND(b)->es.anticlock; + for (i = 0; i < 20; i++) (void)v4_fabric_step(&f); + CHECK(ND(a)->es.anticlock == ca && ND(b)->es.anticlock == cb, "a node that is waiting executes nothing, however long"); + } + + /* ---- an empty node is filled through its port, and runs what it was sent ---- */ + { + static v4_node src; + static v4_cell prog[64], sent[96]; + static stream s; + static v4_device dev; + unsigned len, count; + + v4_node_reset(&src); /* the programme, assembled somewhere to be sent */ + v4_asm_begin(&as, &src, ORG); + LIT(77); LIT(OUT); O(BANG_A); O(STORE_A); + LIT(PB + 1); O(BANG_B); LIT(99); O(STORE_B); + wait_for_ever(); + count = (unsigned)(v4_asm_label(&as) - ORG); + CHECK(v4_asm_ok(&as) && count < 64, "a programme to send: %u words", count); + for (i = 0; i < count; i++) prog[i] = src.mem[ORG + i]; + len = capsule(sent, prog, count, ORG); + + /* sent by a device */ + v4_fabric_init(&f, places, 4); + memset(&s, 0, sizeof s); + s.words = sent; s.count = len; + dev.give = stream_give; dev.take = stream_take; dev.self = &s; + b = (unsigned)v4_fabric_add(&f, &pool[2], PB); + CHECK(v4_fabric_wire_device(&f, b, 3, &dev), "a device is wired to port 3 of a node newly born"); + steps = settle(5000); + CHECK(s.at == len, "the node took every word it was sent: %u of %u", s.at, len); + for (i = 0; i < count && ND(b)->n.mem[ORG + i] == prog[i]; i++) { } + CHECK(i == count, "the programme is in its memory, where it was sent to"); + CHECK(ND(b)->n.mem[OUT] == 77, "and it ran: it left its mark"); + CHECK(ND(b)->n.asking && ND(b)->n.ask_port == 1 && ND(b)->n.request == 99, "and is now writing to its port 1, which nothing is wired to"); + CHECK(v4_fabric_wire_device(&f, b, 1, &dev) && settle(100) < 100 && s.took_n == 1 && s.took[0] == 99, + "a device wired there takes what it wrote"); + + /* sent by another node, which holds it in its own memory */ + v4_fabric_init(&f, places, 4); + a = loaded(0); + for (i = 0; i < len; i++) ND(a)->n.mem[200 + i] = sent[i]; + LIT(200); O(BANG_A); LIT(len - 1); O(PUSH); LIT(PB + 4); O(BANG_B); + (void)v4_asm_label(&as); + O(FETCH_INC); O(STORE_B); O(UNEXT); /* @+ !b unext: each write blocks in the middle of the word */ + wait_for_ever(); + CHECK(v4_asm_ok(&as), "a node that sends what is in its memory"); + b = (unsigned)v4_fabric_add(&f, &pool[2], PB); + CHECK(v4_fabric_wire(&f, a, 4, b, 0), "is wired to a node newly born"); + steps = settle(5000); + CHECK(ND(b)->n.mem[OUT] == 77 && ND(b)->n.asking && ND(b)->n.request == 99, "which takes it in and runs it, as from the device"); + CHECK(ND(b)->n.last_from == 0, "it was listening to any port, and knows which it heard from: %u", ND(b)->n.last_from); + } + + /* ---- a word is passed on by a node in between ---- */ + v4_fabric_init(&f, places, 4); + a = loaded(0); + LIT(PB + 0); O(BANG_B); LIT(7); O(STORE_B); LIT(8); O(STORE_B); LIT(9); O(STORE_B); + wait_for_ever(); + b = loaded(1); /* whatever comes on any port goes out on port 6 */ + LIT(ANY); O(BANG_A); LIT(PB + 6); O(BANG_B); + { + v4_cell again = v4_asm_label(&as); + O(FETCH_A); O(STORE_B); + v4_asm_branch(&as, V4_OP_JUMP, again); + } + c = loaded(2); + LIT(PB + 1); O(BANG_B); LIT(OUT); O(BANG_A); O(FETCH_B); O(STORE_INC); O(FETCH_B); O(STORE_INC); O(FETCH_B); O(STORE_INC); + wait_for_ever(); + CHECK(v4_asm_ok(&as), "three nodes in a row assemble"); + CHECK(v4_fabric_wire(&f, a, 0, b, 2) && v4_fabric_wire(&f, b, 6, c, 1), "the first is wired to the second, the second to the third"); + steps = settle(2000); + CHECK(ND(c)->n.mem[OUT] == 7 && ND(c)->n.mem[OUT + 1] == 8 && ND(c)->n.mem[OUT + 2] == 9, + "what the first wrote reached the third, in order, through the one in between"); + CHECK(ND(b)->n.last_from == 2, "the one in between heard it on its port 2"); + + /* ---- the wiring is changed while they run ---- */ + v4_fabric_init(&f, places, 4); + a = loaded(0); /* writes 1 2 3 4 ... to its port 0 for ever */ + LIT(PB + 0); O(BANG_B); LIT(0); + { + v4_cell again = v4_asm_label(&as); + LIT(1); O(ADD); O(DUP); O(STORE_B); + v4_asm_branch(&as, V4_OP_JUMP, again); + } + b = loaded(1); LIT(PB + 3); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever(); + c = loaded(2); LIT(PB + 3); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever(); + CHECK(v4_asm_ok(&as), "a writer and two readers assemble"); + CHECK(v4_fabric_wire(&f, a, 0, b, 3), "the writer is wired to the first reader"); + (void)settle(500); + CHECK(ND(b)->n.mem[OUT] == 1 && ND(c)->n.mem[OUT] == 0, "who gets its first word"); + CHECK(v4_fabric_wire(&f, a, 0, c, 3), "the same port is wired to the second reader instead"); + (void)settle(500); + 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"); + + /* ---- asleep, awake, and removed, while looping; and a node born while others run ---- */ + v4_fabric_init(&f, places, 4); + a = loaded(0); /* counts in OUT for ever */ + LIT(OUT); O(BANG_A); + { + v4_cell again = v4_asm_label(&as); + O(FETCH_A); LIT(1); O(ADD); O(STORE_A); + v4_asm_branch(&as, V4_OP_JUMP, again); + } + b = loaded(1); /* and so does this one */ + LIT(OUT); O(BANG_A); + { + v4_cell again = v4_asm_label(&as); + O(FETCH_A); LIT(1); O(ADD); O(STORE_A); + v4_asm_branch(&as, V4_OP_JUMP, again); + } + CHECK(v4_asm_ok(&as), "two nodes that never stop assemble"); + for (i = 0; i < 100; i++) (void)v4_fabric_step(&f); + { + v4_cell ca = ND(a)->n.mem[OUT], cb = ND(b)->n.mem[OUT]; + v4_uheat_t clock = ND(a)->es.anticlock; + CHECK(ca > 0 && ca == cb, "each has had as much time as the other: %ld and %ld", (long)ca, (long)cb); + v4_fabric_sleep(&f, a); + for (i = 0; i < 100; i++) (void)v4_fabric_step(&f); + CHECK(ND(a)->n.mem[OUT] == ca && ND(a)->es.anticlock == clock, "put to sleep in the middle of its loop, one executes nothing"); + CHECK(ND(b)->n.mem[OUT] == 2 * cb, "and the other goes on as before: %ld", (long)ND(b)->n.mem[OUT]); + v4_fabric_wake(&f, a); + for (i = 0; i < 100; i++) (void)v4_fabric_step(&f); + CHECK(ND(a)->n.mem[OUT] == 2 * ca, "woken, it goes on from where it was: %ld", (long)ND(a)->n.mem[OUT]); + CHECK(v4_fabric_remove(&f, a) != 0 && v4_fabric_node_at(&f, a) == 0, "it is removed while it loops"); + cb = ND(b)->n.mem[OUT]; + for (i = 0; i < 100; i++) (void)v4_fabric_step(&f); + CHECK(ND(b)->n.mem[OUT] > cb, "and the other goes on"); + c = (unsigned)v4_fabric_add(&f, &pool[3], PB); + CHECK(c == a && pool[3].n.p == ANY, "a node is born while the other runs, into the place that was freed"); + for (i = 0; i < 10; i++) (void)v4_fabric_step(&f); + CHECK(pool[3].n.reading && pool[3].es.anticlock == 0, "and waits, empty, while the other runs"); + } + + /* ---- more room, while they run ---- */ + v4_fabric_init(&f, places, 4); + a = loaded(0); LIT(PB + 0); O(BANG_B); LIT(5); O(STORE_B); wait_for_ever(); + b = loaded(1); LIT(PB + 0); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever(); + (void)loaded(2); wait_for_ever(); + (void)loaded(3); wait_for_ever(); + CHECK(v4_fabric_add(&f, &pool[4], PB) == -1, "a fabric with no room left takes no more nodes"); + CHECK(v4_fabric_wire(&f, a, 0, b, 0), "two of its nodes are wired"); + v4_fabric_grow(&f, more_places, NODES); + CHECK(v4_fabric_add(&f, &pool[4], PB) == 4, "given more room, it takes another"); + (void)settle(500); + CHECK(ND(b)->n.mem[OUT] == 5, "and the nodes that were there are wired as they were"); + + /* ---- what cannot be wired ---- */ + CHECK(!v4_fabric_wire(&f, a, V4_PORTS, b, 0) && !v4_fabric_wire(&f, a, 0, 7, 0) && !v4_fabric_wire(&f, a, 1, a, 1), + "a port that is not there, a node that is not there, a port to itself: refused"); + CHECK(v4_fabric_wire(&f, a, 1, a, 2), "but one port of a node may be wired to another of the same node"); + + printf(" %d checks, %d failures\n", checks, failures); + return failures != 0; +} diff --git a/v4/tests/test_port.c b/v4/tests/test_port.c index 5c9e1fe6..60f0b8cc 100644 --- a/v4/tests/test_port.c +++ b/v4/tests/test_port.c @@ -12,7 +12,7 @@ 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) /* The memory map is open (D-4); the test chooses the address. */ -#define PORT ((v4_cell)(V4_NODE_WORDS - 9u)) +#define PORT ((v4_cell)(V4_NODE_WORDS - 16u)) #define MARK ((v4_cell)0x1234) #define HOME ((v4_cell)8) /* where a word returns to when the test called it */