From 046ce0be65e326aa6cf01d17f5230de7c7f769c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Nov 2026 17:00:08 +0000 Subject: [PATCH] feat(v4): the mesh boot to spec -- Hera born empty in the kernel's fabric; births, kills, every node stepping together (#189) ENGINE step 4 Phase 1, Tasks 5b-5d, and the edge cases (Captain Bob, 2026-11-13: "Build it to proper spec"; "Check for the edge cases"; "Yes, add it to v4-checks"). - v4/system/mesh.c, v4/include/v4/mesh.h: the nucleus assembled by the ROM and given to Hera as F18 cells; Hera born empty at place 0, the kernel on port 0, the console on port 1; told she is 10, in the registry, the kernel's words, FORTH-79, sealed, POST, the kernel's capsules. NODE-BORN / NODE-KILL in the owner's room. - No scheduler: v4_mesh_tell returns once the node told says how its text ended, and v4_mesh_step / v4_mesh_busy let the host step the fabric while it waits, so nodes run between lines as during them. A text left "waiting" is owed its end: the next line ends it "Interrupted" and is then run (MESH.md 7b.6). - kernel: sk_v4.c boots through the mesh; its idle steps the fabric while a node can run, and halts only when none can (#151 kept). stadium_return_quota and stadium_ops_forget on a kill. - Tests: v4/tests/test_mesh_boot.c (32 checks: the boot, births, kills, rebirth, a live number, a fault, no such node, Hera not killing herself, no room, a line too long, a node running for ever, a wait broken by the next line), run by `make -C v4 test` and under ASan+UBSan by `sanitize` (v4-checks); kernel/tests quota return; scripts/qemu_session.py births() and the edges on bare metal. - Docs: ENGINE.md 6 "who runs next" struck (settled: no scheduler); the Phase 1 plan; FABRIC-3 XLVIII.23; .claude/memory/rules.md: alpha.5 closed to new work. - Evidence: v4-checks 80 programs under the sanitizers, 0 failures; the hosted builds alike; two stopped v4-checks runs labelled STOPPED; three development amd64 boots labelled. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm --- .claude/memory/rules.md | 4 + docs/fabric/FABRIC-3.md | 21 + docs/v4.0.0/ENGINE.md | 2 +- ...ine-step4-phase1-v4-in-the-kernels-boot.md | 88 ++-- .../runs/doe-amd64-20261113-153617.csv | 0 .../runs/doe-amd64-20261113-153958.csv | 0 .../runs/doe-amd64-20261113-154853.csv | 0 kernel/Makefile | 20 +- kernel/include/starkernel/vm/stadium.h | 10 + kernel/include/starkernel/vm/stadium_ops.h | 4 + kernel/src/v4/sk_v4.c | 221 ++++----- kernel/src/vm/stadium.c | 36 ++ kernel/src/vm/stadium_ops.c | 14 + kernel/tests/test_stadium_ops.c | 18 + logs/20261113-153617/amd64/README.md | 1 + .../amd64/qemu-amd64-20261113-153617.log | 3 + logs/20261113-153958/amd64/README.md | 1 + .../amd64/qemu-amd64-20261113-153958.log | 3 + logs/20261113-154853/amd64/README.md | 1 + .../amd64/qemu-amd64-20261113-154853.log | 3 + logs/20261113-161229/v4-checks/STOPPED.md | 1 + .../v4-checks/v4-checks-20261113-161229.log | 3 + logs/20261113-161614/v4-checks/STOPPED.md | 1 + .../v4-checks/v4-checks-20261113-161614.log | 3 + .../v4-checks/v4-checks-20261113-162622.log | 3 + scripts/qemu_session.py | 40 ++ v4/Makefile | 37 +- v4/include/v4/boot.h | 24 +- v4/include/v4/image.h | 1 + v4/include/v4/mesh.h | 112 +++++ v4/system/boot.c | 32 +- v4/system/mesh.c | 466 ++++++++++++++++++ v4/tests/test_mesh_boot.c | 170 +++++++ v4/tools/mkmap.c | 4 +- 34 files changed, 1166 insertions(+), 181 deletions(-) create mode 100644 experiments/bare_metal/runs/doe-amd64-20261113-153617.csv create mode 100644 experiments/bare_metal/runs/doe-amd64-20261113-153958.csv create mode 100644 experiments/bare_metal/runs/doe-amd64-20261113-154853.csv create mode 100644 logs/20261113-153617/amd64/README.md create mode 100644 logs/20261113-153617/amd64/qemu-amd64-20261113-153617.log create mode 100644 logs/20261113-153958/amd64/README.md create mode 100644 logs/20261113-153958/amd64/qemu-amd64-20261113-153958.log create mode 100644 logs/20261113-154853/amd64/README.md create mode 100644 logs/20261113-154853/amd64/qemu-amd64-20261113-154853.log create mode 100644 logs/20261113-161229/v4-checks/STOPPED.md create mode 100644 logs/20261113-161229/v4-checks/v4-checks-20261113-161229.log create mode 100644 logs/20261113-161614/v4-checks/STOPPED.md create mode 100644 logs/20261113-161614/v4-checks/v4-checks-20261113-161614.log create mode 100644 logs/20261113-162622/v4-checks/v4-checks-20261113-162622.log create mode 100644 v4/include/v4/mesh.h create mode 100644 v4/system/mesh.c create mode 100644 v4/tests/test_mesh_boot.c diff --git a/.claude/memory/rules.md b/.claude/memory/rules.md index 20af0c4e..b71c6d8a 100644 --- a/.claude/memory/rules.md +++ b/.claude/memory/rules.md @@ -136,3 +136,7 @@ him -> the tag, on his word. - The gate refuses unsigned builds (#174); a protocol run here is unsigned -- the gate's signing is CI's. - POST's floor is v4/system/POST_FLOOR (#173): raise it when POST grows; never lower it without him. - Kernel host tests: make -C kernel/tests test (docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md). +- **alpha.5 is closed to new work** (Captain Bob, 2026-11-13: "Don't let me add anything else to this + milestone."). What it holds now is all it gets: #189 (the blocker), #191, #192, #193 and what was already in it. + If he proposes another item for alpha.5, remind him of this ruling and offer the next milestone; only his + explicit word that he is overriding it reopens it. diff --git a/docs/fabric/FABRIC-3.md b/docs/fabric/FABRIC-3.md index 7616aebf..51178b76 100644 --- a/docs/fabric/FABRIC-3.md +++ b/docs/fabric/FABRIC-3.md @@ -6625,3 +6625,24 @@ and memorize frequently so we can pick up where we left off"). `test_host_quit.c`), A2's five (`test_host_compile.c`); the depth checks moved to the new figures. - **Next:** Phase 1 -- v4 in the kernel's boot, the Stadium and the heartbeat started; then Phase 2, the word accounts at `call`; then Phase 7, the loops on them. + +### 23. #189 Phase 1: v4 in the kernel's boot -- the Stadium, the opcodes, the fabric, births (2026-11-13) + +- **The Stadium on a v4 boot** (plan `2026-11-13-engine-step4-phase1-v4-in-the-kernels-boot.md`, + Tasks 1-4): made for both systems in v3's order, keyed by node ("A new node key": Hera, node 10, is + patron zero under hers); v3's checks after POST; the 32 opcodes as Stadium patrons, charged per + heartbeat tick ("Both": HEAT-OP's feed, about 0.5 us a charge -- kept); acceptance fails a v4 boot + without the physics. +- **A wrong turn, and its correction.** Asked "who runs next", I offered choices of scheduling + policy, and "Hera by compudynamics now" was ruled on that framing. Captain Bob: "Scheduler? Have we + regressed?" The design has none (`JUSTIFICATION.md` 4; `fabric.h`; this file's turn-attractor, + "without a scheduler"). The ruling was withdrawn; "Build it to proper spec." And 5a's "Hera as + now, in a fabric" could not carry births: the old boot spoke only her console's wire and ran no + other node. +- **To spec** (Task 5): the kernel boots the fabric (`v4/mesh.h`): the nucleus assembled by the + machine's ROM, Hera born empty and given it, every node stepped together. A born node's room comes + from the kernel's heap and its Stadium quota from Hera's; killed, both are given back + (`stadium_return_quota`). Seen on bare metal: `11 2 2 BIRTH` -- node 11's parity line; Hera sends + it `6 7 * .`, it answers 42; `11 KILL`; the Stadium CONSERVED after. +- **Next:** Phase 2, the word accounts at `call` -- the kernel's record per word, fed by the hook + (A1); then Phase 7, the loops on those records. And retiring v3 as a boot target (#156). diff --git a/docs/v4.0.0/ENGINE.md b/docs/v4.0.0/ENGINE.md index 3a9922d3..6aae5824 100644 --- a/docs/v4.0.0/ENGINE.md +++ b/docs/v4.0.0/ENGINE.md @@ -471,7 +471,7 @@ these, a group at a time, with POST after each. | ~~How the node tells a dictionary entry from a bare address at `call`~~ Ruled 2026-10-20: the kernel's table of entry addresses, filled by `(WORD-DEFINED)`, emptied by `(WORD-FORGOTTEN)`; a target not in it is a bare routine | Step 3 | | ~~`EXECUTE`, which enters a word by a return~~ Ruled 2026-10-20: `EXECUTE` calls the xt | Step 3 | | ~~`>R R> R@ I J LEAVE`, which cannot be called~~ Ruled 2026-10-20: they stay in line and are checked when the definition holding them is compiled; every other in-line word is called | Step 3 | -| Who decides which node runs next, and when: Hera, by compudynamics; preemptive and cooperative | Step 7 | +| ~~Who decides which node runs next, and when: Hera, by compudynamics; preemptive and cooperative~~ **Settled 2026-11-13 (#189; Captain Bob, "Build it to proper spec"): no one decides -- there is no scheduler (`JUSTIFICATION.md` 4). Every awake node executes an instruction word at each step of the fabric (`v4/include/v4/fabric.h`), and a node with nothing to do waits at a port. The kernel boots the fabric so (`v4/include/v4/mesh.h`).** | Step 7 | | The node's safe moment for message delivery and switching | Step 7 | | `PAD 42 OVER !`, a byte address given to `!` (D-1) | Step 6 | | Which word patrons' accounts the hosted product keeps, having no Stadium | Step 3 | diff --git a/docs/v4.0.0/plans/2026-11-13-engine-step4-phase1-v4-in-the-kernels-boot.md b/docs/v4.0.0/plans/2026-11-13-engine-step4-phase1-v4-in-the-kernels-boot.md index 679813d7..e9812423 100644 --- a/docs/v4.0.0/plans/2026-11-13-engine-step4-phase1-v4-in-the-kernels-boot.md +++ b/docs/v4.0.0/plans/2026-11-13-engine-step4-phase1-v4-in-the-kernels-boot.md @@ -41,13 +41,18 @@ then Phase 7. HEAT-OP's Stadium feed was ruled into this phase ("Both": the regi - **Q4: "In Phase 1."** The registry (`v4/src/manage.c`) comes into the kernel build in this phase: Task 5 is in. Asked how much -- it needs a fabric of nodes, births that make them, and who runs next (`ENGINE.md` 6, open) -- **"Full births now."** -- **Who runs next (`ENGINE.md` 6): "Hera by compudynamics now."** As v3's turn-attractor, agreed - with Captain Bob (`FABRIC-3.md`, the turn-attractor: "the next turn to whichever live identity VM - currently has the lowest `execution_heat_q48` ... No priority levels, no time slices, no - preemption -- the same density comparison Stadium already uses"): the next instruction word goes - to the **coolest awake node** that can run, by its resident heat in the Stadium -- its opcode - patrons (Task 3) until Phase 2 adds its words. Switching only between instruction words - (`ENGINE.md` 3a). A node asleep at its wires, or blocked, is not a candidate. +- ~~**Who runs next (`ENGINE.md` 6): "Hera by compudynamics now."**~~ **Withdrawn 2026-11-13** -- the + question was put wrongly, as a choice of scheduling policy. The design has no scheduler + (`JUSTIFICATION.md` 4: "There is no scheduler to write"; `fabric.h`: "There is no choice of whose + turn it is"; FABRIC-3's turn-attractor: "fix the gap without a scheduler"): every awake node steps + together, and a node with nothing to do waits at a port. Captain Bob, asked "Scheduler? Have we + regressed?", then: **"Build it to proper spec."** The withdrawn ruling, as it was written: + ~~As v3's turn-attractor, agreed with Captain Bob (`FABRIC-3.md`, the turn-attractor: "the next + turn to whichever live identity VM currently has the lowest `execution_heat_q48` ... No priority + levels, no time slices, no preemption -- the same density comparison Stadium already uses"): the + next instruction word goes to the **coolest awake node** that can run, by its resident heat in the + Stadium -- its opcode patrons (Task 3) until Phase 2 adds its words. Switching only between + instruction words (`ENGINE.md` 3a). A node asleep at its wires, or blocked, is not a candidate.~~ ## Questions (as they were put) @@ -135,30 +140,55 @@ own sources. item: "a boot without the physics fails"). Seen to fail on today's build (Task 1 Step 1). - [ ] **Step 2:** the protocol, all eight stages; then the push, on Captain Bob's word. -### Task 5: the registry and births (Q4: "Full births now") +### Task 5: the registry and births, to spec -Each its own increment, each test first, each a boot: +**5a's "Hera as now, in a fabric" is superseded for births** (2026-11-13): with Hera booted by +`v4_boot_run`, her messages to a child are answered "no one is there" (`boot.c` speaks only her +console's wire) and nothing runs a child while the boot drives her. To spec -- `MESH.md` 9, +`MACHINE.md` 3, and as `v4/tests/test_host_unit.c` already does on the host -- **Hera is born empty in +the kernel's fabric, and every node steps together** (`v4_fabric_step`). No scheduler. -- [x] **5a. A fabric in the kernel, Hera in it.** Ruled 2026-11-13 (Captain Bob): **"Hera as now, in - a fabric"** -- she boots by `v4_boot_run` as before (the hosted product's parity kept), and her node - is place 0 of the kernel's fabric (`sk_v4.c`), entered in the registry as node 10; her registry - requests go to `v4_manage_serve` through `v4_boot`'s new `request` hook, and `centre.4th` and - `hera.4th` are loaded on her through its new `capsules` list -- before the seal, so POST runs on - them. The fabric has one place: `NODE-BORN` is answered "no room" until 5b. `CAPSULE-OPEN/-LINE` - read the kernel's capsule directory, checked as the boot checks; no capsule of F18 code is there - until 5b. She is not told her number ((ME) stays 0): the boot's console addresses node 0. Word - count 375 (55 from the two capsules), each in the dictionary; `mkdictionary.py` loads the kernel's - capsules into the hosted system before `WORDS`, so the live list is the kernel's. - Seen to fail first: the acceptance check `NODE-ME .` gave UNKNOWN WORD; then `0 ok`. -- [ ] **5b. Births.** `NODE-BORN`: room for a node from the kernel's heap, a place in the fabric, a - Stadium quota granted from Hera's under the new node's key (`stadium_node_key(number)`), its - opcode patrons; `NODE-KILL` gives the room and the quota back. The capsule words - (`CAPSULE-OPEN/-CELL/-LINE`) from the kernel's capsule directory. -- [ ] **5c. Who runs next.** The turn-attractor: each step, the coolest awake node that can run - executes an instruction word; the opcode charge for every node at the heartbeat's service points. - A test that two busy nodes take turns as their heat says, and a sleeping node is never chosen. -- [ ] **5d. Acceptance.** A boot births a node from Hera, it answers a line, Hera kills it, and the - Stadium is CONSERVED after. +- [x] **5a. The registry** (2026-11-13; `f10d2bc8`): Hera at place 0, entered as node 10; `centre.4th` + and `hera.4th`; `NODE-ME`. Kept; its boot is replaced by 5b. +- [x] **5b. The mesh boot** -- `v4/system/mesh.c` (new), the kernel's boot of the fabric, a host test + first (`v4/tests/test_host_meshboot.c`, 64-bit cells, as `test_host_unit.c`): + 1. the nucleus: the machine's ROM assembles the system on a node of its own (`boot.c`'s + `load_system`, shared); written as a capsule of F18 cells (`v4_capsule_write`) -- "a node that + is born is given assembled code, never the source" (Captain Bob, 2026-10-15); + 2. Hera born empty at place 0 of the fabric: the kernel on her port 0 (it gives her the nucleus + a cell at a time, and takes her requests -- blocks, the kernel's words, the registry), the + console on her port 1 (whole messages); stepped until she sleeps at her wires; + 3. told she is 10 and where the console is; entered in the registry; the kernel's words; + `forth79.4th` a line at a time; sealed; POST (`v4_post_run`, its lines as messages); the + kernel's capsules (`centre.4th`, `hera.4th`); sealed; + 4. the same parity lines as `v4_boot_run` prints, so acceptance reads a mesh boot as it read the + other; + 5. a line from the console is a message to node 10; what any node prints comes back as messages; + how it ended is said; `KEY` reads her console registers, fed by the host; + 6. births: `NODE-BORN` makes a node in room the owner gives, carrying the mesh ROM, wired to the + kernel on its port 0 for its blocks; `NODE-KILL` gives the room back. `CAPSULE-OPEN` of + `v4:nucleus-64.f18` reads the nucleus a cell at a time. + The test: the parity lines; POST passes; `NODE-ME .` 0; `KEY`; `11 2 2 BIRTH` -- node 11 takes in + the nucleus and FORTH-79 and answers a line; `11 KILL`. +- [x] **5c. The kernel boots by it** (`sk_v4.c`): the prompt's lines go to node 10 as messages; the + identity gate, the block chain after POST, the heartbeat's service; a born node's room from the + kernel's heap, its Stadium quota granted from Hera's under its node key (`stadium_node_key`), its + opcode patrons charged with Hera's; its quota given back when it is killed. The hosted product + keeps `v4_boot_run`; `boot.h`'s "differ only in where the text goes" is corrected. +- [x] **5d. Acceptance:** a boot births a node from Hera, it answers a line, Hera kills it, the + Stadium CONSERVED after; then the protocol. + + **Built (2026-11-13).** 5b: `v4/system/mesh.c`, `v4/include/v4/mesh.h`; `boot.c` shares + `v4_boot_assemble` (which `v4_boot_run` now calls), `v4_boot_capsule` and its number printing; + the image carries `(ME)`'s address. `v4/tests/test_mesh_boot.c` (in `make -C v4 test`): failed + first (no `mesh.c`), then 16 checks -- POST 539/539, `NODE-ME` 0, `11 2 2 BIRTH`, node 11 answers + directly and through Hera, `11 KILL`. 5c: `sk_v4.c` boots by it -- 16 places, a born node's room + from the kernel's heap, its Stadium quota under its node key granted from Hera's on its first + charge and given back when it is killed (`stadium_return_quota`, `stadium_ops_forget`: failed first, + then `test_stadium_ops` 20 checks); every node's opcodes charged. A line that waits for an answer + is broken by the next line, as a message (`MESH.md` 7b.6). 5d: `qemu_session.py births()`. The + amd64 boot: 160 s (about 90 s before -- POST and the lines now go as messages); `dict_hash` + changes (Hera is told she is 10). ## What this phase does not do diff --git a/experiments/bare_metal/runs/doe-amd64-20261113-153617.csv b/experiments/bare_metal/runs/doe-amd64-20261113-153617.csv new file mode 100644 index 00000000..e69de29b diff --git a/experiments/bare_metal/runs/doe-amd64-20261113-153958.csv b/experiments/bare_metal/runs/doe-amd64-20261113-153958.csv new file mode 100644 index 00000000..e69de29b diff --git a/experiments/bare_metal/runs/doe-amd64-20261113-154853.csv b/experiments/bare_metal/runs/doe-amd64-20261113-154853.csv new file mode 100644 index 00000000..e69de29b diff --git a/kernel/Makefile b/kernel/Makefile index 05171050..cca892f6 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -730,14 +730,15 @@ LOADER_CFLAGS += $(V4_DEFS) V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c wire.c manage.c) V4_ROM_C := $(BUILD_DIR)/v4_rom_64.c V4_MAP_C := $(BUILD_DIR)/v4_map_64.c +V4_MESH_ROM_C := $(BUILD_DIR)/v4_mesh_rom_64.c # a mesh node's ROM: the nodes Hera births carry it (v4/mesh.h, #189) LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c $(KERNEL_SRC)/v4/sk_v4_stop.c $(KERNEL_SRC)/v4/sk_v4_slots.c KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c $(KERNEL_SRC)/v4/sk_v4_stop.c $(KERNEL_SRC)/v4/sk_v4_slots.c # v4/system/boot.c is the boot the hosted v4 system runs too: the ROM # assembles the system, then the capsules from this kernel's own capsule # directory, then the prompt. -LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_rom.o $(LOADER_OBJ_DIR)/v4engine/v4_map.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o $(LOADER_OBJ_DIR)/v4system/post.o $(LOADER_OBJ_DIR)/v4system/post_cases.o -KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o $(KERNEL_OBJ_DIR)/v4engine/v4_map.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o $(KERNEL_OBJ_DIR)/v4system/post.o $(KERNEL_OBJ_DIR)/v4system/post_cases.o +LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_rom.o $(LOADER_OBJ_DIR)/v4engine/v4_map.o $(LOADER_OBJ_DIR)/v4engine/v4_mesh_rom.o $(LOADER_OBJ_DIR)/v4system/mesh.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o $(LOADER_OBJ_DIR)/v4system/post.o $(LOADER_OBJ_DIR)/v4system/post_cases.o +KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o $(KERNEL_OBJ_DIR)/v4engine/v4_map.o $(KERNEL_OBJ_DIR)/v4engine/v4_mesh_rom.o $(KERNEL_OBJ_DIR)/v4system/mesh.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o $(KERNEL_OBJ_DIR)/v4system/post.o $(KERNEL_OBJ_DIR)/v4system/post_cases.o endif LOADER_SRCS := $(LOADER_SRCS_BASE) $(LOADER_EXTRA_SRCS) @@ -903,12 +904,13 @@ endif # two copies met ("cp: cannot create regular file ... File exists", fixed # 2026-10-26). ifeq ($(STARFORTH_V4),1) -$(V4_ROM_C) $(V4_MAP_C) &: FORCE +$(V4_ROM_C) $(V4_MAP_C) $(V4_MESH_ROM_C) &: FORCE @mkdir -p $(dir $(V4_ROM_C)) - @echo " V4ROM capsules/v4/rom -> $(V4_ROM_C) $(V4_MAP_C)" - @$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c + @echo " V4ROM capsules/v4/rom -> $(V4_ROM_C) $(V4_MAP_C) $(V4_MESH_ROM_C)" + @$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c $(CURDIR)/v4/build/mesh-rom-64.c @cmp -s v4/build/rom-64.c $(V4_ROM_C) || cp v4/build/rom-64.c $(V4_ROM_C) @cmp -s v4/build/v4_map_64.c $(V4_MAP_C) || cp v4/build/v4_map_64.c $(V4_MAP_C) + @cmp -s v4/build/mesh-rom-64.c $(V4_MESH_ROM_C) || cp v4/build/mesh-rom-64.c $(V4_MESH_ROM_C) # The system's memory map, capsules/v4/sys/map-.f18, is generated from # the same description as the ROM and v4_map_image (v4/tests/host_map.h), and @@ -928,6 +930,14 @@ $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o: $(V4_ROM_C) | $(KERNEL_OBJ_DIR) @mkdir -p $(dir $@) @echo "CC (kernel) $<" @$(CC) $(KERNEL_CFLAGS) -c $< -o $@ +$(LOADER_OBJ_DIR)/v4engine/v4_mesh_rom.o: $(V4_MESH_ROM_C) | $(LOADER_OBJ_DIR) + @mkdir -p $(dir $@) + @echo "CC (loader) $<" + @$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@ +$(KERNEL_OBJ_DIR)/v4engine/v4_mesh_rom.o: $(V4_MESH_ROM_C) | $(KERNEL_OBJ_DIR) + @mkdir -p $(dir $@) + @echo "CC (kernel) $<" + @$(CC) $(KERNEL_CFLAGS) -c $< -o $@ $(LOADER_OBJ_DIR)/v4engine/v4_map.o: $(V4_MAP_C) | $(LOADER_OBJ_DIR) @mkdir -p $(dir $@) @echo "CC (loader) $<" diff --git a/kernel/include/starkernel/vm/stadium.h b/kernel/include/starkernel/vm/stadium.h index 9ef41cae..0e846c13 100644 --- a/kernel/include/starkernel/vm/stadium.h +++ b/kernel/include/starkernel/vm/stadium.h @@ -195,6 +195,16 @@ VMUuid stadium_node_key(uint32_t node); int stadium_boot_init_for(VMUuid first); int stadium_birth_node(VMUuid key, const char *name); +/* + * stadium_return_quota(vm, to) -- a node is gone: its quota is given back (#189, Phase 1 Task 5c), + * the inverse of stadium_grant_quota(). Every resident patron of it is evicted, its heat cooling to + * its reservoir as any eviction's does; its free cells go to `to`'s free list; its slot is free for + * the next grant. Its reservoir -- the share of 1.0 its grant gave it, its own -- goes with it, as + * the grant made it. 0, or -1: no such quota, `to` none or the same, the first quota (patron zero's), + * or a patron that cannot be evicted (pinned, or holding another). + */ +int stadium_return_quota(VMUuid vm_id, VMUuid to_vm_id); + /* stadium_is_initialized - Whether stadium_boot_init() has succeeded. */ int stadium_is_initialized(void); diff --git a/kernel/include/starkernel/vm/stadium_ops.h b/kernel/include/starkernel/vm/stadium_ops.h index 986ed327..344d2959 100644 --- a/kernel/include/starkernel/vm/stadium_ops.h +++ b/kernel/include/starkernel/vm/stadium_ops.h @@ -38,6 +38,10 @@ void stadium_ops_init(void); * `tick` the heartbeat's count now. Nothing for a key with no quota. */ void stadium_ops_charge(VMUuid vm_id, const uint64_t counts[STADIUM_OPS_COUNT], uint64_t tick); +/* A node is gone: its opcode counts are forgotten, so a node that later has its quota's slot starts + * from none (#189). Before stadium_return_quota(), while the key still has its slot. */ +void stadium_ops_forget(VMUuid vm_id); + /* An opcode patron's heat now (Q48.16), or 0: not resident, or no quota. */ uint64_t stadium_ops_heat(VMUuid vm_id, unsigned op); diff --git a/kernel/src/v4/sk_v4.c b/kernel/src/v4/sk_v4.c index 91c526c9..c527bfb2 100644 --- a/kernel/src/v4/sk_v4.c +++ b/kernel/src/v4/sk_v4.c @@ -4,7 +4,8 @@ /* sk_v4.c -- StarForth v4 on bare metal: one host node at a prompt. * * The node is the golden model of the F18-derived engine (v4/src). It comes - * up as the hosted v4 system does, by v4_boot_run (v4/include/v4/boot.h): + * up in the kernel's fabric (v4/include/v4/mesh.h; #189 -- until 2026-11-13 as the hosted + * system still does, by v4_boot_run, v4/include/v4/boot.h): * the machine's ROM assembles the system from its source, then the capsules * from the directory baked into this kernel are loaded, each checked and its * parity line printed, then the prompt. @@ -34,6 +35,8 @@ #include "v4/boot.h" #include "v4/fabric.h" #include "v4/manage.h" +#include "v4/mesh.h" +#include "starkernel/kmalloc.h" #include "block_subsystem.h" #include "starkernel/capsule.h" #include "starkernel/capsule_generated.h" @@ -42,6 +45,7 @@ #ifdef STARFORTH_ENABLE_VM #include "starkernel/vm/stadium.h" #include "starkernel/vm/stadium_ops.h" +#include "starkernel/vm/stadium_words.h" #endif #define SK_V4_LINE 1024u /* what the line editor here takes: a line, as the boot takes one (v4/system/boot.c) */ @@ -50,95 +54,46 @@ * console's own "[zuse@Hera] " line prefix. */ #define SK_V4_PROMPT "\x1b[38;2;85;255;255mok> \x1b[39m" -/* THE KERNEL'S FABRIC (#189, ENGINE step 4 Phase 1, Task 5a; Captain Bob, 2026-11-13: "Hera as now, - * in a fabric"). Hera -- node 10, the one the kernel brings up -- is booted as before (v4_boot_run) - * and is the node at place 0 of the kernel's fabric, entered in its registry (v4/manage.h, - * MESH.md 9a): her registry requests (centre.4th, hera.4th) are served by v4_manage_serve. The - * fabric has her one place: a birth is refused, "no room" (-1), until the fabric has room for more - * (Task 5b). */ -#define SK_V4_PLACES 1u -static v4_fabric_node sk_v4_hera; /* her node, its exec state and its heat */ +/* THE KERNEL'S FABRIC (#189, ENGINE step 4 Phase 1, Task 5c; Captain Bob, 2026-11-13: "Build it to + * proper spec"). The kernel boots the fabric by v4/mesh.h: the nucleus assembled by the machine's + * ROM, Hera born empty at place 0 and given it, her system and POST -- and every node steps + * together; there is no scheduler. A node Hera births has its room from the kernel's heap, a Stadium + * quota under its node key (stadium_node_key) granted from hers, and its opcodes charged as hers + * are; killed, its room and its quota are given back. */ +#define SK_V4_PLACES 16u +static v4_mesh sk_v4_mesh; +#define SK_V4_IDLE_STEPS 4096u /* fabric steps between looks at the keyboard, while a node runs */ static v4_place sk_v4_places[SK_V4_PLACES]; -static v4_fabric sk_v4_fabric; -static v4_manage sk_v4_manage; -static v4_manage_entry sk_v4_registry[SK_V4_PLACES]; -static unsigned sk_v4_hera_place; +static v4_wire_queue sk_v4_queues[4u * SK_V4_PLACES]; +static v4_manage_entry sk_v4_reg[SK_V4_PLACES]; +static v4_blocks sk_v4_blocks[SK_V4_PLACES]; +static v4_device sk_v4_kernel[SK_V4_PLACES]; +static v4_fabric_node sk_v4_nucleus, sk_v4_hera; static v4_image sk_v4_image; -#define sk_v4_heat (sk_v4_hera.h) +static v4_cell sk_v4_keyed[SK_V4_PLACES]; /* by place: the number its Stadium quota is keyed by; 0: none */ -/* What each place has open of a capsule (CAPSULE-OPEN, -LINE): a FORTH capsule's payload, read a - * block line at a time, as the boot reads one (v4/system/boot.c, load_capsule). */ -typedef struct { const uint8_t *p, *end; int in_block; } sk_v4_reader; -static sk_v4_reader sk_v4_reads[SK_V4_PLACES]; - -static int sk_v4_born(void *self) +static v4_fabric_node *sk_v4_room(void *self) +{ + v4_fabric_node *x = (v4_fabric_node *)kmalloc(sizeof *x); + unsigned char *b = (unsigned char *)x; + size_t i; + (void)self; + if (x) for (i = 0; i < sizeof *x; i++) b[i] = 0; + return x; +} +static void sk_v4_unroom(void *self, v4_fabric_node *x) { (void)self; kfree(x); } +static void sk_v4_gone(void *self, unsigned place) { (void)self; - return -1; /* the fabric has no room beyond Hera's place (Task 5b gives it room) */ -} -static void sk_v4_gone(void *self, unsigned place, v4_fabric_node *was) -{ - (void)self; (void)was; - if (place < SK_V4_PLACES) sk_v4_reads[place].p = sk_v4_reads[place].end = 0; -} -/* The capsule `name`: found in the kernel's directory, its content and signature checked as the - * boot checks them (a signature that does not verify refuses it), and opened for `asker`. Only a - * capsule of FORTH blocks can be opened: there is none of F18 code in the directory to read a cell - * at a time until Task 5b puts the nucleus there. */ -static int sk_v4_open(void *self, unsigned asker, const char *name, unsigned len) -{ - const CapsuleDirHeader *dir = capsule_get_directory(); - const CapsuleDesc *descs = capsule_get_descriptors(); - const CapsuleNameEntry *names = capsule_get_names(); - const uint8_t *arena = capsule_get_arena(); - const CapsuleDesc *cap; - const uint8_t *payload; - char cname[V4_MANAGE_LINE + 1u]; - unsigned i; - - (void)self; - if (asker >= SK_V4_PLACES || len == 0 || len > V4_MANAGE_LINE) return 0; - for (i = 0; i < len; i++) cname[i] = name[i]; - cname[len] = 0; - if (len < 4 || cname[len - 4] != '.' || cname[len - 3] != '4' || cname[len - 2] != 't' || cname[len - 1] != 'h') return 0; - cap = capsule_find_by_name(dir, descs, names, cname); - if (!cap || capsule_validate(cap, arena, dir->arena_size, 1) != CAPSULE_VALID) return 0; - if (capsule_verify_signature(descs, names, capsule_get_signatures(), arena, dir->desc_count, (int)(cap - descs)) - == CAPSULE_SIG_INVALID) return 0; - payload = capsule_get_payload(cap, arena); - if (!payload) return 0; - sk_v4_reads[asker].p = payload; - sk_v4_reads[asker].end = payload + cap->length; - sk_v4_reads[asker].in_block = 0; - return 1; -} -static int sk_v4_cell(void *self, unsigned asker, v4_cell *w) -{ - (void)self; (void)asker; (void)w; - return 0; /* no capsule of F18 code is open: none can be (sk_v4_open) */ -} -/* The next line of the open capsule that is part of a block, at most V4_MANAGE_LINE characters: - * its length, or -1 at its end (or a line too long, which no capsule mkcapsule passes has). */ -static int sk_v4_line(void *self, unsigned asker, char *text) -{ - sk_v4_reader *r; - (void)self; - if (asker >= SK_V4_PLACES) return -1; - r = &sk_v4_reads[asker]; - while (r->p && r->p < r->end) { - const uint8_t *after, *nl; - uint32_t num; - unsigned len, i; - if (capsule_block_header(r->p, r->end, &num, &after)) { r->in_block = 1; r->p = after; continue; } - for (nl = r->p; nl < r->end && *nl != '\n'; nl++) { } - len = (unsigned)(nl - r->p); - if (len && r->p[len - 1] == '\r') len--; - if (r->in_block && len > V4_MANAGE_LINE) { r->p = r->end; return -1; } - if (r->in_block) for (i = 0; i < len; i++) text[i] = (char)r->p[i]; - r->p = nl < r->end ? nl + 1 : r->end; - if (r->in_block) return (int)len; +#ifdef STARFORTH_ENABLE_VM + if (place < SK_V4_PLACES && sk_v4_keyed[place] != 0) { + VMUuid key = stadium_node_key((uint32_t)sk_v4_keyed[place]); + stadium_ops_forget(key); + (void)stadium_return_quota(key, stadium_node_key(SK_V4_HERA_NODE)); + stadium_words_print_boot_diagnostics(stadium_node_key(SK_V4_HERA_NODE)); /* conservation, after */ } - return -1; +#endif + if (place < SK_V4_PLACES) sk_v4_keyed[place] = 0; } /* A node the asker birthed has booted: its parity line, as the boot prints a capsule's. */ static void sk_v4_parity(void *self, unsigned place, v4_cell number, const v4_node *n) @@ -157,11 +112,6 @@ static void sk_v4_parity(void *self, unsigned place, v4_cell number, const v4_no line[at] = 0; console_println(line); } -static int sk_v4_request(void *self, v4_node *n, v4_cell request) -{ - (void)self; (void)n; - return v4_manage_serve(&sk_v4_manage, sk_v4_hera_place, request); -} static const char *const sk_v4_capsules[] = { "v4:centre.4th", "v4:hera.4th" }; static char sk_v4_text[SK_V4_LINE + 1u]; @@ -177,7 +127,22 @@ static uint8_t sk_v4_post_blk_ram[BLK_RAM_BLOCKS * BLK_FORTH_SIZE]; void sk_v4_charge_opcodes(void) { #ifdef STARFORTH_ENABLE_VM - stadium_ops_charge(stadium_node_key(SK_V4_HERA_NODE), sk_v4_heat.op, heartbeat_ticks()); + unsigned k; + uint64_t now = heartbeat_ticks(); + for (k = 0; k < SK_V4_PLACES; k++) { + v4_fabric_node *x = v4_fabric_node_at(&sk_v4_mesh.f, k); + v4_cell number; + if (!x) continue; + number = x->n.mem[sk_v4_image.me]; + if (number <= 0) continue; /* not told its number yet: charged when it is */ + if (sk_v4_keyed[k] != number) { /* a node first seen: its quota, granted from Hera's */ + VMUuid key = stadium_node_key((uint32_t)number); + if (stadium_quota_slot_for_vm(key) < 0 + && stadium_grant_quota(key, stadium_node_key(SK_V4_HERA_NODE)) != 0) continue; + sk_v4_keyed[k] = number; + } + stadium_ops_charge(stadium_node_key((uint32_t)number), x->h.op, now); + } #endif } @@ -253,6 +218,14 @@ static int sk_v4_readline(void) heartbeat_service(); } sk_v4_charge_opcodes(); + /* Nothing typed, and a node can run: the fabric steps on between lines, as during them + * -- every node steps together (JUSTIFICATION.md 4; #189, "Check for the edge cases"). + * A batch, then the keyboard is looked at again. */ + if (v4_mesh_busy(&sk_v4_mesh)) { + unsigned k; + for (k = 0; k < SK_V4_IDLE_STEPS; k++) (void)v4_mesh_step(&sk_v4_mesh); + continue; + } /* Nothing typed: wait for the next interrupt, not in a spin (#151 -- the spin kept a * CPU core 100% busy at the prompt, and the demo's host with it). The heartbeat's * timer wakes it at 100 Hz, so a key is seen within a tick, as the blink and the @@ -278,48 +251,34 @@ static int sk_v4_readline(void) } } -/* The node's boot record: sk_v4_boot() fills it, sk_v4_prompt() hands it lines. */ -static v4_boot boot; +/* a line to Hera, as a message from the console, and all that follows from it (v4/mesh.h) */ +static int sk_v4_tell(const char *text, unsigned len) { return v4_mesh_tell(&sk_v4_mesh, V4_MESH_HERA, text, len); } void sk_v4_boot(void (*chain)(void), const BootDisk *boot_disk) { - console_println("StarForth v4: one host node, the F18-derived engine"); + console_println("StarForth v4: Hera, born in the kernel's fabric"); sk_v4_image = v4_map_image; - /* the kernel's fabric, Hera at its one place, entered in its registry as node 10 (#189) */ - v4_fabric_init(&sk_v4_fabric, sk_v4_places, SK_V4_PLACES); - v4_fabric_gone_error(&sk_v4_fabric, V4_ERROR_NO_ONE); - { - int place = v4_fabric_add(&sk_v4_fabric, &sk_v4_hera, sk_v4_image.port); - if (place < 0) { console_println("StarForth v4: no place for Hera in the fabric"); sk_v4_stop(); } - sk_v4_hera_place = (unsigned)place; - } - sk_v4_manage.f = &sk_v4_fabric; sk_v4_manage.reg = sk_v4_registry; sk_v4_manage.reg_cap = SK_V4_PLACES; - sk_v4_manage.hera = sk_v4_hera_place; - sk_v4_manage.born = sk_v4_born; sk_v4_manage.gone = sk_v4_gone; sk_v4_manage.open = sk_v4_open; - sk_v4_manage.cell = sk_v4_cell; sk_v4_manage.line = sk_v4_line; sk_v4_manage.parity = sk_v4_parity; - sk_v4_manage.self = 0; - if (!v4_manage_enter(&sk_v4_manage, (v4_cell)SK_V4_HERA_NODE, sk_v4_hera_place, 0)) { - console_println("StarForth v4: Hera not entered in the registry"); sk_v4_stop(); - } - boot.n = &sk_v4_hera.n; boot.es = &sk_v4_hera.es; boot.h = &sk_v4_hera.h; boot.im = &sk_v4_image; - boot.request = sk_v4_request; boot.request_self = 0; - boot.capsules = sk_v4_capsules; boot.capsule_count = sizeof sk_v4_capsules / sizeof sk_v4_capsules[0]; - boot.out = sk_v4_out; boot.key = sk_v4_key; - boot.words = sk_v4_words; boot.word_count = sizeof sk_v4_words / sizeof sk_v4_words[0]; boot.serve = sk_v4_serve; + sk_v4_mesh.places = sk_v4_places; sk_v4_mesh.capacity = SK_V4_PLACES; + sk_v4_mesh.queues = sk_v4_queues; sk_v4_mesh.queue_count = sizeof sk_v4_queues / sizeof sk_v4_queues[0]; + sk_v4_mesh.reg = sk_v4_reg; sk_v4_mesh.blocks = sk_v4_blocks; sk_v4_mesh.kernel = sk_v4_kernel; + sk_v4_mesh.nucleus = &sk_v4_nucleus; sk_v4_mesh.hera = &sk_v4_hera; + sk_v4_mesh.room = sk_v4_room; sk_v4_mesh.unroom = sk_v4_unroom; + sk_v4_mesh.born = 0; sk_v4_mesh.gone = sk_v4_gone; sk_v4_mesh.parity = sk_v4_parity; + sk_v4_mesh.out = sk_v4_out; sk_v4_mesh.key = sk_v4_key; + sk_v4_mesh.words = sk_v4_words; sk_v4_mesh.word_count = sizeof sk_v4_words / sizeof sk_v4_words[0]; + sk_v4_mesh.serve = sk_v4_serve; + sk_v4_mesh.capsules = sk_v4_capsules; sk_v4_mesh.capsule_count = sizeof sk_v4_capsules / sizeof sk_v4_capsules[0]; + sk_v4_mesh.im = &sk_v4_image; sk_v4_mesh.self = 0; blk_subsys_init(sk_v4_post_blk_ram, sizeof(sk_v4_post_blk_ram)); - if (!v4_boot_run(&boot)) { + if (!v4_mesh_boot(&sk_v4_mesh)) { console_println("StarForth v4: not started"); sk_v4_stop(); /* #177: halts, not a 100% spin */ } - /* She is not told her number ((ME) stays 0): the boot's console addresses its lines to node 0, - * and a node takes a message as its own only when its first word is its number (quit.f18). The - * registry knows her as 10; NODE-ME answers her place. (Told 10, she let the console's next line - * go -- the boot stopped at "did not let go of POST's blocks", 2026-11-13.) */ if (chain) { /* the blocks are other blocks now: what the node has in its two * buffers is POST's, and is let go */ chain(); - if (v4_boot_line(&boot, "EMPTY-BUFFERS", 13) != V4_TEXT_COMPLETED) { + if (sk_v4_tell("EMPTY-BUFFERS", 13) != V4_TEXT_COMPLETED) { console_println("StarForth v4: the node did not let go of POST's blocks"); sk_v4_stop(); /* #177: halts, not a 100% spin */ } @@ -356,33 +315,29 @@ void sk_v4_prompt(void) console_puts(SK_V4_PROMPT); len = sk_v4_readline(); if (len < 0) continue; - how = v4_boot_line(&boot, sk_v4_text, (unsigned)len); + how = sk_v4_tell(sk_v4_text, (unsigned)len); /* The line is waiting for an answer no one here can send. The next - * line typed breaks the wait: the line that was waiting ends in an - * error, and then the line just typed is done (v4/boot.h). */ + * line typed breaks the wait (MESH.md 7b.6): it is a message to + * Hera, the line that was waiting ends in an error, and then the + * line just typed is done -- all in the one tell, which says how the + * last ended. */ while (how == V4_BOOT_LINE_WAITING) { len = sk_v4_readline(); if (len < 0) { /* Zuse's session ended while the line waited: its wait is * ended now, by an empty line, and not carried into the * next session (Captain Bob, 2026-10-26: "end the waiting - * line at logout"). How it ended is shown; the empty line - * is then done, and shows nothing. */ - how = v4_boot_line(&boot, "", 0u); + * line at logout"). How it ended is shown. */ + how = sk_v4_tell("", 0u); if (how == V4_TEXT_ERROR) console_puts(" ERROR\n"); else if (how == V4_TEXT_COMPLETED) console_puts(" ok\n"); - if (how == V4_TEXT_ERROR || how == V4_TEXT_COMPLETED) how = v4_boot_line(&boot, 0, 0); logged_out = 1; break; } - how = v4_boot_line(&boot, sk_v4_text, (unsigned)len); + how = sk_v4_tell(sk_v4_text, (unsigned)len); if (how == V4_BOOT_LINE_TOO_LONG) { console_puts("Line too long\n ERROR\n"); how = V4_BOOT_LINE_WAITING; continue; } - if (how == V4_TEXT_ERROR) console_puts(" ERROR\n" SK_V4_PROMPT); - else if (how == V4_TEXT_COMPLETED) console_puts(" ok\n" SK_V4_PROMPT); - else break; - how = v4_boot_line(&boot, 0, 0); } - if (logged_out && (how == V4_TEXT_ERROR || how == V4_TEXT_COMPLETED)) continue; + if (logged_out) continue; if (how == V4_TEXT_COMPLETED) console_puts(" ok\n"); else if (how == V4_TEXT_ERROR) console_puts(" ERROR\n"); else if (how == V4_BOOT_LINE_TOO_LONG) console_puts("Line too long\n ERROR\n"); diff --git a/kernel/src/vm/stadium.c b/kernel/src/vm/stadium.c index d1320c1b..4e23db5c 100644 --- a/kernel/src/vm/stadium.c +++ b/kernel/src/vm/stadium.c @@ -667,6 +667,42 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) { return 0; } +int stadium_return_quota(VMUuid vm_id, VMUuid to_vm_id) { + int slot, to_slot; + size_t idx, last = STADIUM_CELL_NONE, count = 0; + + if (!stadium_initialized) return -1; + slot = quota_slot_for_vm(vm_id); + to_slot = quota_slot_for_vm(to_vm_id); + if (slot <= 0 || to_slot < 0 || slot == to_slot) return -1; /* slot 0 is patron zero's */ + + /* its patrons leave, their heat to its reservoir */ + while (stadium_quotas[slot].resident_head != STADIUM_CELL_NONE) + if (stadium_evict(stadium_quotas[slot].resident_head) != 0) return -1; + + /* its free cells to `to`'s free list */ + for (idx = stadium_quotas[slot].free_head; idx != STADIUM_CELL_NONE; + idx = link_to_size(stadium_cell_array[idx].header.link)) { + stadium_owner[idx] = (uint16_t)to_slot; + last = idx; + count++; + } + if (last != STADIUM_CELL_NONE) { + stadium_cell_array[last].header.link = size_to_link(stadium_quotas[to_slot].free_head); + stadium_quotas[to_slot].free_head = stadium_quotas[slot].free_head; + stadium_quotas[to_slot].free_count += count; + } + + stadium_quotas[slot].vm_id = vm_uuid_none(); + stadium_quotas[slot].in_use = 0; + stadium_quotas[slot].free_head = STADIUM_CELL_NONE; + stadium_quotas[slot].free_count = 0; + stadium_quotas[slot].resident_head = STADIUM_CELL_NONE; + stadium_quotas[slot].reservoir = 0; + stadium_quotas[slot].consumed = 0; + return 0; +} + /* * stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: which currently * in-use VM has the most free (unclaimed) cells right now. Callers granting diff --git a/kernel/src/vm/stadium_ops.c b/kernel/src/vm/stadium_ops.c index c955b623..84bcc81e 100644 --- a/kernel/src/vm/stadium_ops.c +++ b/kernel/src/vm/stadium_ops.c @@ -136,6 +136,20 @@ void stadium_ops_charge(VMUuid vm_id, const uint64_t counts[STADIUM_OPS_COUNT], } } +void stadium_ops_forget(VMUuid vm_id) +{ + int slot; + unsigned k; + if (!ops_initialized) return; + slot = stadium_quota_slot_for_vm(vm_id); + if (slot < 0) return; + for (k = 0; k < STADIUM_OPS_COUNT; k++) { + op_slots[slot][k].cell_index = STADIUM_CELL_NONE; + op_slots[slot][k].last_tick = 0; + op_slots[slot][k].last_count = 0; + } +} + uint64_t stadium_ops_heat(VMUuid vm_id, unsigned op) { int slot; diff --git a/kernel/tests/test_stadium_ops.c b/kernel/tests/test_stadium_ops.c index b3ba87f2..2f113610 100644 --- a/kernel/tests/test_stadium_ops.c +++ b/kernel/tests/test_stadium_ops.c @@ -86,5 +86,23 @@ int main(void) CHECK(h5 > 2u * h0 && h5 < 4u * h0, "in proportion to their retirements, about 3 to 1"); CHECK(stadium_reservoir_peek(k2) >= Q48_ONE / 3 && stadium_conserved(k2), "the floor kept, conservation held"); } + + /* a node killed: its quota given back (#189, Phase 1 Task 5c) -- its patrons evicted, its cells + * to the node it came from, its slot free for the next birth; its opcode counts forgotten */ + { + VMUuid k2 = stadium_node_key(12), k3 = stadium_node_key(13); + uint64_t c3[32]; + stadium_ops_forget(k2); + CHECK(stadium_return_quota(k2, key) == 0, "node 12's quota is given back to node 10"); + CHECK(stadium_quota_slot_for_vm(k2) < 0 && stadium_ops_heat(k2, 5) == 0, "node 12 has no quota, and no opcode heat"); + CHECK(stadium_conserved(key), "node 10 conserved after taking its cells back"); + CHECK(stadium_return_quota(key, key) != 0 && stadium_return_quota(k2, key) != 0, "a quota is not given back to itself, or twice"); + CHECK(stadium_grant_quota(k3, key) == 0, "the slot is free: node 13 is granted one"); + memset(c3, 0, sizeof c3); + c3[7] = 2; + stadium_ops_charge(k3, c3, 30); + CHECK(stadium_ops_heat(k3, 7) == 2u * (uint64_t)STADIUM_WORD_HEAT_QUANTUM && stadium_ops_heat(k3, 5) == 0, + "node 13 starts with no counts of node 12's: two retirements, two quanta"); + } return DONE(); } diff --git a/logs/20261113-153617/amd64/README.md b/logs/20261113-153617/amd64/README.md new file mode 100644 index 00000000..6a6dc38f --- /dev/null +++ b/logs/20261113-153617/amd64/README.md @@ -0,0 +1 @@ +Development boot, not acceptance evidence (2026-11-13): an amd64 boot of the mesh boot under development (#189 Phase 1, Tasks 5b-5d), before the edge cases. POST passed (539/539). The session checks' results are not recorded in this serial log. The protocol run is the evidence. diff --git a/logs/20261113-153617/amd64/qemu-amd64-20261113-153617.log b/logs/20261113-153617/amd64/qemu-amd64-20261113-153617.log new file mode 100644 index 00000000..e25a09e2 --- /dev/null +++ b/logs/20261113-153617/amd64/qemu-amd64-20261113-153617.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:220471465794c8ae61228d78051913791b130a9eada4f5ec3dbe6921bd4abb6d +size 7601 diff --git a/logs/20261113-153958/amd64/README.md b/logs/20261113-153958/amd64/README.md new file mode 100644 index 00000000..6a6dc38f --- /dev/null +++ b/logs/20261113-153958/amd64/README.md @@ -0,0 +1 @@ +Development boot, not acceptance evidence (2026-11-13): an amd64 boot of the mesh boot under development (#189 Phase 1, Tasks 5b-5d), before the edge cases. POST passed (539/539). The session checks' results are not recorded in this serial log. The protocol run is the evidence. diff --git a/logs/20261113-153958/amd64/qemu-amd64-20261113-153958.log b/logs/20261113-153958/amd64/qemu-amd64-20261113-153958.log new file mode 100644 index 00000000..475e46cd --- /dev/null +++ b/logs/20261113-153958/amd64/qemu-amd64-20261113-153958.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68ff772d974a8047299a5b7f54466139235df4b0714c2f9fdfa651a223c95c1f +size 8405 diff --git a/logs/20261113-154853/amd64/README.md b/logs/20261113-154853/amd64/README.md new file mode 100644 index 00000000..4fc668c3 --- /dev/null +++ b/logs/20261113-154853/amd64/README.md @@ -0,0 +1 @@ +Development boot, not acceptance evidence (2026-11-13): the amd64 boot that checked the edge cases on bare metal (#189: "Check for the edge cases"). Every session check passed (RESULT: PASS), including a node running SPIN for ever, the prompt answering while it runs, and 11 KILL with the Stadium CONSERVED. The protocol run is the evidence. diff --git a/logs/20261113-154853/amd64/qemu-amd64-20261113-154853.log b/logs/20261113-154853/amd64/qemu-amd64-20261113-154853.log new file mode 100644 index 00000000..fd49790c --- /dev/null +++ b/logs/20261113-154853/amd64/qemu-amd64-20261113-154853.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bac158c4872f8b57ed2e2315d476aebf85666f908d85b9085cbf30860b0bbaa +size 9462 diff --git a/logs/20261113-161229/v4-checks/STOPPED.md b/logs/20261113-161229/v4-checks/STOPPED.md new file mode 100644 index 00000000..a2733580 --- /dev/null +++ b/logs/20261113-161229/v4-checks/STOPPED.md @@ -0,0 +1 @@ +STOPPED, not a result: this v4-checks run was stopped partway (2026-11-13) because v4/Makefile changed under it -- a sanitized mesh-boot test was added to `sanitize` (Captain Bob: "Yes, add it to v4-checks"). No failure had been seen. The run on the final tree is the evidence. diff --git a/logs/20261113-161229/v4-checks/v4-checks-20261113-161229.log b/logs/20261113-161229/v4-checks/v4-checks-20261113-161229.log new file mode 100644 index 00000000..febb8ecd --- /dev/null +++ b/logs/20261113-161229/v4-checks/v4-checks-20261113-161229.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15fcda316475c95fe1734c7abd8d225a55cd263ba106d2f835c3d10317488ff0 +size 22251 diff --git a/logs/20261113-161614/v4-checks/STOPPED.md b/logs/20261113-161614/v4-checks/STOPPED.md new file mode 100644 index 00000000..91525813 --- /dev/null +++ b/logs/20261113-161614/v4-checks/STOPPED.md @@ -0,0 +1 @@ +STOPPED, not a result: this v4-checks run (2026-11-13) was stopped by the 10-minute limit set on the command that ran it, partway through the sanitizer runs (the 32-bit depth test). No failure had been seen. The run with a longer limit is the evidence. diff --git a/logs/20261113-161614/v4-checks/v4-checks-20261113-161614.log b/logs/20261113-161614/v4-checks/v4-checks-20261113-161614.log new file mode 100644 index 00000000..f0245e30 --- /dev/null +++ b/logs/20261113-161614/v4-checks/v4-checks-20261113-161614.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52333b6cbbccfb3ccadbcde6f843ecdfd8589dfc0be16af5b16a2eeff9d7159e +size 32160 diff --git a/logs/20261113-162622/v4-checks/v4-checks-20261113-162622.log b/logs/20261113-162622/v4-checks/v4-checks-20261113-162622.log new file mode 100644 index 00000000..d84142a8 --- /dev/null +++ b/logs/20261113-162622/v4-checks/v4-checks-20261113-162622.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d638890abda94e9f43d845f5108b72a13240f51a9de0869e675de022d75a279 +size 134744 diff --git a/scripts/qemu_session.py b/scripts/qemu_session.py index 494c3e41..218e7a8e 100755 --- a/scripts/qemu_session.py +++ b/scripts/qemu_session.py @@ -495,6 +495,46 @@ def registry(): check("NODE-ME: Hera is at place 0 of the kernel's fabric", wait_for(PROMPT, 60, n) and re.search(r"(?:^|[^0-9])0\s+ok", text()[-200:]) is not None and "UNKNOWN WORD" not in text()[-200:]) + births() + + +def births(): + """A node born from Hera in the kernel's fabric, answering, and killed (#189, Phase 1 Task 5d): + 11 2 2 BIRTH gives it the nucleus and FORTH-79 (its parity line); Hera sends it a line and awaits + it; 11 KILL removes it, its Stadium quota given back, and conservation holds after.""" + n = count(PROMPT) + type_line("11 2 2 BIRTH .") + check("BIRTH: node 11 born from Hera, its parity recorded", + wait_for(PROMPT, 300, n) and re.search(r"PARITY:V4_BIRTH node=11 dict_hash=0x[0-9a-f]{16}", text()) is not None + and "ERROR" not in text()[-300:]) + n = count(PROMPT) + type_line('S" 6 7 * ." 11 SEND 11 AWAIT .') + check("node 11 answers Hera: 42, and its text completed", + wait_for(PROMPT, 120, n) and re.search(r"42\s", text()[-300:]) is not None and "ERROR" not in text()[-300:]) + c = count(r"Stadium conservation: CONSERVED") + n = count(PROMPT) + type_line("11 KILL") + check("KILL: node 11 removed, the Stadium CONSERVED after", + wait_for(PROMPT, 120, n) and count(r"Stadium conservation: CONSERVED") > c and "ERROR" not in text()[-300:]) + # the edges (Captain Bob, 2026-11-13: "Check for the edge cases"): a node set running for ever + # does not hold the console -- it runs on between lines, and is killed + n = count(PROMPT) + type_line("11 2 2 BIRTH .") + check("after a kill, 11 is born again", wait_for(PROMPT, 300, n) and "ERROR" not in text()[-300:]) + n = count(PROMPT) + type_line('S" VARIABLE N1 : SPIN BEGIN 1 N1 +! AGAIN ;" 11 SEND 11 AWAIT .') + check("node 11 is given SPIN", wait_for(PROMPT, 120, n) and "ERROR" not in text()[-300:]) + n = count(PROMPT) + type_line('S" SPIN" 11 SEND') + check("Hera's line ends, though node 11 runs for ever", wait_for(PROMPT, 120, n) and "ERROR" not in text()[-300:]) + n = count(PROMPT) + type_line("3 4 + .") + check("and the prompt answers while it runs: 7 ok", wait_for(PROMPT, 120, n) and re.search(r"7\s+ok", text()[-300:]) is not None) + c = count(r"Stadium conservation: CONSERVED") + n = count(PROMPT) + type_line("11 KILL") + check("a node running for ever is killed, the Stadium CONSERVED after", + wait_for(PROMPT, 120, n) and count(r"Stadium conservation: CONSERVED") > c and "ERROR" not in text()[-300:]) check("POST: PASSED", wait_for(r"POST: PASSED", 900)) diff --git a/v4/Makefile b/v4/Makefile index c243370e..d7e0ed89 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -29,7 +29,9 @@ CFLAGS ?= $(CSTD) $(WARN) $(OPT) HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) SRCS := $(wildcard $(HERE)/src/*.c) -TESTS := $(wildcard $(HERE)/tests/test_*.c) +# test_mesh_boot.c is the kernel's boot of the fabric, linked as the hosted product is (its own rule, +# `mesh-boot`, below), not one of the per-width tests. +TESTS := $(filter-out %/test_mesh_boot.c,$(wildcard $(HERE)/tests/test_*.c)) # Cell widths to build and verify. 32 is the F18/Zynq case, 64 the aarch64 # host case; see DECOMPOSITION.md D-5. @@ -344,12 +346,41 @@ $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call TEST_RULE,$(w),$(t))))) $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call RUN_RULE,$(w),$(t))))) $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call SAN_RULE,$(w),$(t))))) -sanitize: $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),san-$(w)-$(notdir $(t)))) +sanitize: $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),san-$(w)-$(notdir $(t)))) san-mesh-boot @echo "all v4 tests passed under ASan+UBSan" .PHONY: $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),run-$(w)-$(notdir $(t)) san-$(w)-$(notdir $(t)))) -test: $(addprefix test-,$(WIDTHS)) +# THE MESH BOOT (system/mesh.c; #189, ENGINE step 4 Phase 1, Task 5b): the kernel's boot of the +# fabric, tested on the host with tests/test_mesh_boot.c as its owner. Linked as the hosted product +# is, with the registry (src/manage.c) and a mesh node's ROM; 64-bit cells, as the capsules are. +MESH_BOOT_SRCS := $(HERE)/system/mesh.c $(HERE)/src/manage.c $(BINDIR)/mesh-rom-64.c +$(BINDIR)/test_mesh_boot: $(HERE)/tests/test_mesh_boot.c $(MESH_BOOT_SRCS) $(MACHINE_64) $(CAPSULE_DIR_C) $(ENGINE_SRCS) $(SYSTEM_SRCS) $(BLOCKS_SRC) $(V3_BLOCK_SRCS) $(wildcard $(HERE)/include/v4/*.h) $(HERE)/Makefile + $(CC) $(CFLAGS) -D_POSIX_C_SOURCE=200809L -I$(HERE)/include $(V3_INC) -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(HERE)/tests/test_mesh_boot.c -o $(BINDIR)/test_mesh_boot.o + $(CC) $(SYSTEM_CFLAGS) -c $(HERE)/system/mesh.c -o $(BINDIR)/mesh-64.o + @rm -rf $(BINDIR)/v3o-mesh && mkdir -p $(BINDIR)/v3o-mesh + cd $(BINDIR)/v3o-mesh && $(CC) $(V3_CFLAGS) -I$(HERE)/include -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(V3_BLOCK_SRCS) $(BLOCKS_SRC) + $(CC) $(SYSTEM_CFLAGS) $(BINDIR)/test_mesh_boot.o $(BINDIR)/mesh-64.o $(HERE)/src/manage.c $(BINDIR)/mesh-rom-64.c $(MACHINE_64) $(CAPSULE_DIR_C) $(ENGINE_SRCS) $(SYSTEM_SRCS) $(BINDIR)/v3o-mesh/*.o -o $@ + +.PHONY: mesh-boot +mesh-boot: $(BINDIR)/test_mesh_boot + @echo " [mesh boot, V4_CELL_BITS=64]" + @$(BINDIR)/test_mesh_boot + +# The same, under ASan+UBSan (Captain Bob, 2026-11-13: "Yes, add it to v4-checks"): run by `sanitize`. +MESH_SAN := -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-recover=all +$(BINDIR)/san-test_mesh_boot: $(BINDIR)/test_mesh_boot + $(CC) $(CFLAGS) $(MESH_SAN) -D_POSIX_C_SOURCE=200809L -I$(HERE)/include $(V3_INC) -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(HERE)/tests/test_mesh_boot.c -o $(BINDIR)/san-test_mesh_boot.o + @rm -rf $(BINDIR)/san-v3o-mesh && mkdir -p $(BINDIR)/san-v3o-mesh + cd $(BINDIR)/san-v3o-mesh && $(CC) $(V3_CFLAGS) $(MESH_SAN) -I$(HERE)/include -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(V3_BLOCK_SRCS) $(BLOCKS_SRC) + $(CC) $(SYSTEM_CFLAGS) $(MESH_SAN) $(BINDIR)/san-test_mesh_boot.o $(HERE)/system/mesh.c $(HERE)/src/manage.c $(BINDIR)/mesh-rom-64.c $(MACHINE_64) $(CAPSULE_DIR_C) $(ENGINE_SRCS) $(SYSTEM_SRCS) $(BINDIR)/san-v3o-mesh/*.o -o $@ + +.PHONY: san-mesh-boot +san-mesh-boot: $(BINDIR)/san-test_mesh_boot + @echo " [ASan+UBSan mesh boot, V4_CELL_BITS=64]" + @$(BINDIR)/san-test_mesh_boot + +test: $(addprefix test-,$(WIDTHS)) mesh-boot @echo "all v4 tests passed" clean: diff --git a/v4/include/v4/boot.h b/v4/include/v4/boot.h index 652ef678..19a06856 100644 --- a/v4/include/v4/boot.h +++ b/v4/include/v4/boot.h @@ -5,9 +5,13 @@ * the system, then its capsules are loaded, then the prompt. * docs/v4.0.0/MACHINE.md, NUCLEUS.md. * - * The hosted binary (tools/hosted.c) and the bare-metal kernel - * (kernel/src/v4/sk_v4.c) both call v4_boot_run and differ only in where - * the text goes. So the two start the same way and print the same lines: + * The hosted binary (tools/hosted.c) calls v4_boot_run: one node, which its + * ROM brings up and which is handed its lines. The bare-metal kernel + * (kernel/src/v4/sk_v4.c) did too, until 2026-11-13; it now boots the fabric + * (v4/mesh.h; #189, ENGINE step 4 Phase 1): the ROM assembles the nucleus by + * v4_boot_assemble, and Hera is born empty and given it. Corrected + * 2026-11-13 -- this said the two "differ only in where the text goes". + * Both print the same parity lines, in the same order: * * PARITY:V4_F18 name=v4:sys:map-64.f18 capsule_hash=0x... (one for each blob of the system's source) * PARITY:V4_ROM words=N rom_hash=0x... instructions=N (the ROM, and what assembling the system cost) @@ -117,4 +121,18 @@ int v4_boot_line(const v4_boot *b, const char *text, unsigned len); /* The dictionary hash of a node started from `im`. */ uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im); +/* WHAT THE MESH BOOT SHARES (v4/mesh.h; #189). v4_boot_assemble: the node + * `b->n` is born empty and its ROM assembles the system from its source, + * the PARITY:V4_F18 and PARITY:V4_ROM lines printed, `b->im` completed from + * the image record; 1, or 0 with the failure said -- what v4_boot_run does + * first. v4_boot_capsule: the capsule `name` found in the directory, its + * hash and signature checked as the boot checks them, a failure said; its + * payload, or 0. And the boot's own way of printing numbers, so the mesh + * prints the same parity lines. */ +int v4_boot_assemble(const v4_boot *b); +uint32_t v4_boot_word_count(const v4_node *n, const v4_image *im); +const uint8_t *v4_boot_capsule(const v4_boot *b, const char *name, uint32_t *length, uint64_t *id, uint64_t *hash); +void v4_boot_say_dec(const v4_boot *b, uint32_t v); +void v4_boot_say_hex(const v4_boot *b, uint64_t v); + #endif /* V4_BOOT_H */ diff --git a/v4/include/v4/image.h b/v4/include/v4/image.h index 5424dfdb..613f67c5 100644 --- a/v4/include/v4/image.h +++ b/v4/include/v4/image.h @@ -49,6 +49,7 @@ typedef struct { v4_cell dstack_reg, rstack_reg; v4_cell node_error; v4_cell heat_reg; /* HEAT-OP[0]; ANTICLOCK is 32 after it (node.h, #189) */ + v4_cell me; /* (ME): the node's number, which messages go by (v4/mesh.h, #189) */ /* the dictionary's two variables: DP holds HERE, a byte address, and * LATEST the newest word of FORTH. What lies below HERE, and LATEST, diff --git a/v4/include/v4/mesh.h b/v4/include/v4/mesh.h new file mode 100644 index 00000000..6ca532b1 --- /dev/null +++ b/v4/include/v4/mesh.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LicenseRef-Starship-1.0 + * Copyright (c) 2023-2026 Robert A. James + */ +/* mesh.h -- the kernel's boot of the fabric: Hera born empty, given the + * system, and every node stepped together. docs/v4.0.0/MESH.md 9, + * MACHINE.md 3; #189, ENGINE step 4 Phase 1, Task 5b (Captain Bob, + * 2026-11-13: "Build it to proper spec"). + * + * There is no scheduler (JUSTIFICATION.md 4: "There is no scheduler to + * write"): every awake node executes an instruction word at each step of + * the fabric (fabric.h), and a node with nothing to do waits at a port. + * + * THE BOOT, as v4/tests/test_host_unit.c does it on the host: + * 1. the nucleus: the machine's ROM assembles the system's source on a + * node of its own (v4/boot.h, v4_boot_assemble), and the system, as + * assembled, is a capsule of F18 cells -- a node that is born is given + * assembled code, never the source (Captain Bob, 2026-10-15); + * 2. Hera is born empty at place 0, with the machine's ROM: the kernel is + * on her port 0 -- it gives her the nucleus a cell at a time, and takes + * her requests: blocks, the kernel's words, the registry (manage.h) -- + * and the console on her port 1, which speaks whole messages; + * 3. she is told she is node 10 and where the console is, entered in the + * registry, made the kernel's words, given FORTH-79, sealed, POSTed, + * given the kernel's capsules, and sealed; + * printing the same parity lines as v4_boot_run (boot.h). + * + * A NODE IS BORN (NODE-BORN): in storage the owner gives, with a mesh + * node's ROM, the kernel on its port 0 for its blocks; it is given the + * nucleus by whoever birthed it (centre.4th). NODE-KILL gives the storage + * back. + * + * Nothing here uses the C library. The owner supplies all the memory. + */ +#ifndef V4_MESH_H +#define V4_MESH_H + +#include "v4/boot.h" +#include "v4/blocks.h" +#include "v4/fabric.h" +#include "v4/manage.h" + +#define V4_MESH_HERA 10 /* Hera's number, as the host mesh numbers her */ +#define V4_MESH_CONSOLE 1 /* the console's number as a sender, and her port to it */ +#define V4_MESH_NUCLEUS_CELLS (2u * V4_NODE_WORDS + 64u) + +typedef struct v4_mesh v4_mesh; +struct v4_mesh { + /* ---- the owner's: set before v4_mesh_boot ---- */ + v4_place *places; /* the fabric's room: `capacity` places */ + unsigned capacity; + v4_wire_queue *queues; /* the wires' memory (fabric.h): two for every wire at once */ + unsigned queue_count; + v4_manage_entry *reg; /* the registry: `capacity` entries */ + v4_blocks *blocks; /* each place's block window, kept by the kernel: `capacity` of them */ + v4_fabric_node *nucleus; /* where the ROM assembles the system: a node of its own */ + v4_fabric_node *hera; /* Hera's storage */ + v4_device *kernel; /* the kernel on each place's port 0: `capacity` of them */ + /* storage for a node to be born, or 0: none; and given back */ + v4_fabric_node *(*room)(void *self); + void (*unroom)(void *self, v4_fabric_node *x); + /* a node has been born at `place`, numbered `number` once it is told; is gone (may be 0) */ + void (*born)(void *self, unsigned place); + void (*gone)(void *self, unsigned place); + /* a node the asker birthed has its system: its parity (may be 0) */ + void (*parity)(void *self, unsigned place, v4_cell number, const v4_node *n); + /* the console, the keyboard, the kernel's words and capsules: as v4_boot's (boot.h) */ + void (*out)(const char *text, unsigned len); + int (*key)(void); + const char *const *words; + unsigned word_count; + void (*serve)(v4_node *n, unsigned request); + const char *const *capsules; + unsigned capsule_count; + v4_image *im; /* the map's description (v4_map_image), which the boot completes */ + void *self; + + /* ---- the mesh's own ---- */ + v4_fabric f; + v4_manage m; + unsigned hera_place; + v4_cell cells[V4_MESH_NUCLEUS_CELLS]; /* the nucleus as a capsule of F18 cells */ + unsigned cell_count, given; /* how many; how many given to Hera */ + unsigned reading_nucleus[64]; /* by place: the next nucleus cell a CAPSULE-CELL gives; 0: not open */ + unsigned dones; /* how many texts have said how they ended, ever */ + v4_cell done_from, done_how; /* the last of them: from which node, and how */ + v4_cell owed_from; /* a node told texts that were left "waiting" ... */ + unsigned owed; /* ... and how many of their ends are still to come */ +}; + +/* The boot: the nucleus, Hera, her system, POST. 1, Hera asleep at her + * wires waiting for a line; or 0, the failure printed. */ +int v4_mesh_boot(v4_mesh *ms); + +/* Text for node `node`, from the console, and the fabric stepped until that node says how the text + * ended: what any node prints goes to `out`; returns how it ended (V4_TEXT_*), or + * V4_BOOT_LINE_STOPPED / _TOO_LONG / _REFUSED / _NO_INPUT (boot.h). Other nodes may run on after it + * returns (v4_mesh_step). V4_BOOT_LINE_WAITING: Hera is asleep awaiting an answer that has not come + * -- nothing else can run, or other nodes have run a long while and not given it; the next text from + * the console breaks the wait (MESH.md 7b.6), and if the answer comes first, `dones` says so. */ +int v4_mesh_tell(v4_mesh *ms, v4_cell node, const char *text, unsigned len); + +/* One step of the fabric, as the host's idle: every awake node executes an instruction word, and what + * comes back to the console is printed (or counted, for how a text ended: `dones`). Returns how many + * instructions were retired. The host calls it while it waits, whenever v4_mesh_busy says a node can + * run: every node steps together, between lines as during them. */ +unsigned v4_mesh_step(v4_mesh *ms); +int v4_mesh_busy(const v4_mesh *ms); + +/* The node numbered `number`, or 0. */ +v4_fabric_node *v4_mesh_node(const v4_mesh *ms, v4_cell number); + +#endif diff --git a/v4/system/boot.c b/v4/system/boot.c index a5b2b8f7..f1dd4414 100644 --- a/v4/system/boot.c +++ b/v4/system/boot.c @@ -568,19 +568,41 @@ static int load_system(const v4_boot *b) /* ---- the whole boot ------------------------------------------------------ */ -int v4_boot_run(const v4_boot *b) -{ - unsigned i; +/* ---- what the mesh boot shares (v4/mesh.h; #189) ---------------------------- */ +int v4_boot_assemble(const v4_boot *b) +{ /* a node is born with nothing in it, and its ROM assembles the system */ if (!v4_image_born(b->n, b->es, b->h, b->im)) { - say(b, "V4: the memory map is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n"); + say(b, "V4: the memory map is not for this build of the engine\n"); return 0; } /* its console's wire, with nothing on it */ v4_wire_reset(TO_NODE); v4_wire_reset(FROM_NODE); v4_blocks_init(&boot_blocks, b->im->block_window); - if (!load_system(b)) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; } + return load_system(b); +} + +uint32_t v4_boot_word_count(const v4_node *n, const v4_image *im) { return word_count(n, im); } +void v4_boot_say_dec(const v4_boot *b, uint32_t v) { say_dec(b, v); } +void v4_boot_say_hex(const v4_boot *b, uint64_t v) { say_hex(b, v); } + +const uint8_t *v4_boot_capsule(const v4_boot *b, const char *name, uint32_t *length, uint64_t *id, uint64_t *hash) +{ + const CapsuleDesc *cap; + const uint8_t *p = open_capsule(b, name, &cap); + if (!p) return 0; + *length = (uint32_t)cap->length; + *id = cap->capsule_id; + *hash = cap->content_hash; + return p; +} + +int v4_boot_run(const v4_boot *b) +{ + unsigned i; + + if (!v4_boot_assemble(b)) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; } /* the kernel's words, before any capsule that might use one */ for (i = 0; i < b->word_count; i++) { diff --git a/v4/system/mesh.c b/v4/system/mesh.c new file mode 100644 index 00000000..53e51146 --- /dev/null +++ b/v4/system/mesh.c @@ -0,0 +1,466 @@ +/* SPDX-License-Identifier: LicenseRef-Starship-1.0 + * Copyright (c) 2023-2026 Robert A. James + */ +/* mesh.c -- the kernel's boot of the fabric. See v4/mesh.h (#189). + * + * What v4/tests/test_host_unit.c does on the host, made the kernel's: the + * nucleus, Hera born empty and given it, her system, POST, and every node + * stepped together by the fabric. No scheduler. + * + * Nothing here uses the C library: the bare-metal kernel links this file. + */ +#include "v4/mesh.h" +#include "v4/capsule.h" +#include "v4/image.h" +#include "v4/message.h" +#include "v4/post.h" +#include "starkernel/capsule_blocks.h" + +#if V4_CELL_BITS == 64 +extern const v4_cell v4_rom_64[]; +extern const unsigned v4_rom_64_words; +extern const v4_cell v4_mesh_rom_64[]; +extern const unsigned v4_mesh_rom_64_words; +#define HERA_ROM v4_rom_64 +#define HERA_ROM_WORDS v4_rom_64_words +#define NODE_ROM v4_mesh_rom_64 +#define NODE_ROM_WORDS v4_mesh_rom_64_words +#define NUCLEUS_NAME "v4:nucleus-64.f18" +#else +extern const v4_cell v4_rom_32[]; +extern const unsigned v4_rom_32_words; +extern const v4_cell v4_mesh_rom_32[]; +extern const unsigned v4_mesh_rom_32_words; +#define HERA_ROM v4_rom_32 +#define HERA_ROM_WORDS v4_rom_32_words +#define NODE_ROM v4_mesh_rom_32 +#define NODE_ROM_WORDS v4_mesh_rom_32_words +#define NUCLEUS_NAME "v4:nucleus-32.f18" +#endif + +#define KERNEL_PORT 0u +#define MAX_PLACES 64u /* mesh.h: reading_nucleus[64] */ +#define STEP_LIMIT 4000000000ULL /* fabric steps one line may take */ +#define WAIT_STEPS 2000000ULL /* steps Hera may sleep awaiting a node that runs on, before the line is "waiting" */ +#define TOLD_FIRST "10 (ME) ! 1 (CONSOLE) ! 1 1 ROUTE" + +static v4_mesh *the; /* one mesh: the devices' callbacks find it here */ +static v4_boot bt; /* for saying, and for the ROM's assembly of the nucleus */ + +/* ---- saying ---------------------------------------------------------------- */ + +static void say(const char *s) { unsigned len = 0; while (s[len]) len++; the->out(s, len); } +static int same(const char *a, unsigned alen, const char *b) { unsigned i; for (i = 0; i < alen && b[i] && a[i] == b[i]; i++) { } return i == alen && !b[i]; } + +/* ---- the nucleus, as a capsule of F18 cells -------------------------------- */ + +static void put_cell(void *self, v4_cell w) +{ + v4_mesh *ms = (v4_mesh *)self; + if (ms->cell_count < V4_MESH_NUCLEUS_CELLS) ms->cells[ms->cell_count++] = w; +} + +/* ---- each place's capsule reader (CAPSULE-OPEN, -CELL, -LINE) -------------- */ + +typedef struct { const uint8_t *p, *end; int in_block; } reader; +static reader reads[MAX_PLACES]; + +static int open_cb(void *self, unsigned asker, const char *name, unsigned len) +{ + char cname[V4_MANAGE_LINE + 1u]; + uint32_t length; + uint64_t id, hash; + const uint8_t *p; + unsigned i; + (void)self; + if (asker >= MAX_PLACES || len == 0 || len > V4_MANAGE_LINE) return 0; + if (same(name, len, NUCLEUS_NAME)) { /* the system, assembled: a cell at a time */ + the->reading_nucleus[asker] = 1; + reads[asker].p = reads[asker].end = 0; + return 1; + } + for (i = 0; i < len; i++) cname[i] = name[i]; + cname[len] = 0; + p = v4_boot_capsule(&bt, cname, &length, &id, &hash); + if (!p) return 0; + the->reading_nucleus[asker] = 0; + reads[asker].p = p; reads[asker].end = p + length; reads[asker].in_block = 0; + return 1; +} +static int cell_cb(void *self, unsigned asker, v4_cell *w) +{ + unsigned k; + (void)self; + if (asker >= MAX_PLACES || !the->reading_nucleus[asker]) return 0; + k = the->reading_nucleus[asker] - 1u; + if (k >= the->cell_count) return 0; + *w = the->cells[k]; + the->reading_nucleus[asker]++; + return 1; +} +/* The next line of a block of the open capsule, at most V4_MANAGE_LINE characters: its length, or -1. */ +static int next_line(reader *r, char *text, unsigned cap) +{ + while (r->p && r->p < r->end) { + const uint8_t *after, *nl; + uint32_t num; + unsigned len, i; + if (capsule_block_header(r->p, r->end, &num, &after)) { r->in_block = 1; r->p = after; continue; } + for (nl = r->p; nl < r->end && *nl != '\n'; nl++) { } + len = (unsigned)(nl - r->p); + if (len && r->p[len - 1] == '\r') len--; + if (r->in_block && len > cap) { r->p = r->end; return -1; } + if (r->in_block) for (i = 0; i < len; i++) text[i] = (char)r->p[i]; + r->p = nl < r->end ? nl + 1 : r->end; + if (r->in_block && len) return (int)len; + } + return -1; +} +static int line_cb(void *self, unsigned asker, char *text) +{ + (void)self; + if (asker >= MAX_PLACES || the->reading_nucleus[asker]) return -1; + return next_line(&reads[asker], text, V4_MANAGE_LINE); +} +static void parity_cb(void *self, unsigned place, v4_cell number, const v4_node *n) +{ + (void)self; + if (the->parity) the->parity(the->self, place, number, n); +} + +/* ---- the kernel on a place's port 0 ---------------------------------------- */ + +static unsigned place_of_device(const void *self) { return (unsigned)((const v4_device *)self - the->kernel); } + +static int kernel_take(void *self, v4_cell value) /* a request */ +{ + unsigned place = place_of_device(self); + v4_fabric_node *x = v4_fabric_node_at(&the->f, place); + if (!x) return 1; + if (v4_blocks_serve(&the->blocks[place], &x->n, value)) return 1; + if (place == the->hera_place && the->serve && value >= 1 && value <= (v4_cell)the->word_count) { + the->serve(&x->n, (unsigned)value); + return 1; + } + if (v4_manage_serve(&the->m, place, value)) return 1; /* the registry's: any node may ask, the kernel checks who */ + v4_node_store(&x->n, x->n.error_reg, 12); /* a request no one serves: 12, Argument out of range */ + return 1; +} +static int kernel_give(void *self, v4_cell *value) /* the nucleus, to Hera, who has nothing in her */ +{ + if (place_of_device(self) != the->hera_place || the->given >= the->cell_count) return 0; + *value = the->cells[the->given++]; + return 1; +} +static const v4_device console = { 0, 0, 0 }; /* it speaks whole messages only */ + +/* What a node's hardware gives it, in place `place`: its registers, its ROM, the kernel on port 0. */ +static int fit(unsigned place, v4_fabric_node *x, const v4_cell *rom, unsigned rom_words, int gives) +{ + const v4_image *im = the->im; + v4_node_stack_regs_attach(&x->n, im->dstack_reg, im->rstack_reg); + v4_node_error_attach(&x->n, im->node_error); + v4_node_fault_attach(&x->n, im->fault_table); + v4_node_heat_regs_attach(&x->n, im->heat_reg, x->h.op, &x->es.anticlock); + v4_node_rom_attach(&x->n, rom, rom_words); + v4_blocks_init(&the->blocks[place], im->block_window); + the->kernel[place].take = kernel_take; + the->kernel[place].give = gives ? kernel_give : 0; + the->kernel[place].self = &the->kernel[place]; + return v4_fabric_wire_kernel(&the->f, place, KERNEL_PORT, &the->kernel[place]); +} + +/* ---- births ------------------------------------------------------------------ */ + +static int born_cb(void *self) +{ + v4_fabric_node *x; + int place; + (void)self; + x = the->room ? the->room(the->self) : 0; + if (!x) return -1; + place = v4_fabric_add(&the->f, x, the->im->port); + if (place < 0 || (unsigned)place >= MAX_PLACES) { + if (place >= 0) (void)v4_fabric_remove(&the->f, (unsigned)place); + the->unroom(the->self, x); + return -1; + } + if (!fit((unsigned)place, x, NODE_ROM, NODE_ROM_WORDS, 0)) { + (void)v4_fabric_remove(&the->f, (unsigned)place); + the->unroom(the->self, x); + return -1; + } + the->reading_nucleus[place] = 0; + if (the->born) the->born(the->self, (unsigned)place); + return place; +} +static void gone_cb(void *self, unsigned place, v4_fabric_node *was) +{ + (void)self; + if (place < MAX_PLACES) { the->reading_nucleus[place] = 0; reads[place].p = reads[place].end = 0; } + if (the->gone) the->gone(the->self, place); + if (the->unroom) the->unroom(the->self, was); +} + +v4_fabric_node *v4_mesh_node(const v4_mesh *ms, v4_cell number) +{ + unsigned k; + for (k = 0; k < ms->capacity; k++) { + v4_fabric_node *x = v4_fabric_node_at(&ms->f, k); + if (x && x->n.mem[ms->im->me] == number) return x; + } + return 0; +} + +/* ---- the console: text to a node, and what comes back -------------------- */ + +/* Does any node execute at the next step? One asleep at a wire, blocked, or stopped does not. */ +static int any_running(const v4_mesh *ms) +{ + unsigned k; + for (k = 0; k < ms->capacity; k++) { + const v4_fabric_node *x = v4_fabric_node_at(&ms->f, k); + if (!x || ms->places[k].asleep || x->n.stopped || x->n.asking || x->n.doing || (x->n.reading && !x->n.given)) continue; + return 1; + } + return 0; +} + +/* What has come back to the console: printing goes to `out`, and how a text ended is kept. */ +static int takes(v4_mesh *ms, v4_cell *ended_from, v4_cell *ended_how, unsigned *ended) +{ + static v4_cell hd[V4_WIRE_HEADER], text[V4_MSG_MAX_CHARS / 4u]; + int took = 0; + while (v4_fabric_device_take(&ms->f, ms->hera_place, V4_MESH_CONSOLE, hd, text, V4_MSG_MAX_CHARS / 4u) == V4_WIRE_DONE) { + took = 1; + if (hd[2] == V4_MSG_OUTPUT) { + char buf[64]; + unsigned i, at = 0; + for (i = 0; i < (unsigned)hd[6]; i++) { + buf[at++] = (char)(((v4_ucell)text[i / 4u] >> (8u * (i % 4u))) & 0xFFu); + if (at == sizeof buf) { ms->out(buf, at); at = 0; } + } + if (at) ms->out(buf, at); + } else if (hd[2] == V4_MSG_DONE) { + /* the end of a text left "waiting" (7b.6: it ends "Interrupted" when the next comes, or + * with its answer): owed, not the end of the text being told now */ + if (ms->owed && hd[1] == ms->owed_from) ms->owed--; + else (*ended)++; + *ended_from = hd[1]; + *ended_how = text[0]; + ms->dones++; + ms->done_from = hd[1]; + ms->done_how = text[0]; + } + } + return took; +} + +unsigned v4_mesh_step(v4_mesh *ms) +{ + v4_cell f = -1, h = -1; + unsigned e = 0, did; + the = ms; + did = v4_fabric_step(&ms->f); + (void)takes(ms, &f, &h, &e); + return did; +} + +int v4_mesh_busy(const v4_mesh *ms) { return any_running(ms); } + +int v4_mesh_tell(v4_mesh *ms, v4_cell node, const char *text, unsigned len) +{ + static v4_message msg; + v4_cell ended_from = -1, ended_how = -1; + unsigned ended = 0; + unsigned long long steps = 0, asleep = 0; + v4_fabric_node *h = v4_fabric_node_at(&ms->f, ms->hera_place); + + the = ms; + if (len > V4_MSG_MAX_CHARS) return V4_BOOT_LINE_TOO_LONG; + if (!v4_message_text(&msg, node, V4_MESH_CONSOLE, V4_MSG_TEXT, text, len)) return V4_BOOT_LINE_TOO_LONG; + if (v4_fabric_device_put(&ms->f, ms->hera_place, V4_MESH_CONSOLE, msg.word, msg.word + V4_MSG_HEADER) != V4_WIRE_DONE) + return V4_BOOT_LINE_REFUSED; + while (steps++ < STEP_LIMIT) { + unsigned did = v4_fabric_step(&ms->f); + int took = takes(ms, &ended_from, &ended_how, &ended); + int running; + if (h && h->n.stopped) return V4_BOOT_LINE_STOPPED; + /* the node it was for has said how its text ended -- or, for node 0, one not yet told its + * number, whoever it became: the first line Hera is told makes her 10 */ + if (ended && (node == 0 || ended_from == node)) return (int)ended_how; + /* the text reads the keyboard: Hera's console registers, fed from the host's keys */ + if (h && v4_image_waiting(&h->n, ms->im)) { + int c = ms->key ? ms->key() : V4_BOOT_KEY_END; + unsigned char ch = (unsigned char)c; + if (c == V4_BOOT_KEY_END) return V4_BOOT_LINE_NO_INPUT; + if (c != V4_BOOT_KEY_NONE) (void)v4_node_console_feed(&h->n, &ch, 1); + continue; + } + running = any_running(ms); + /* Hera asleep at her wires, her text not ended: awaiting an answer -- "waiting" when nothing + * else can give it, or when other nodes have run a long while without giving it */ + if (h && h->n.doing && h->n.do_op == 8) { + if ((did == 0 && !took && !running) || ++asleep > WAIT_STEPS) { + if (ms->owed && ms->owed_from == node) ms->owed++; + else { ms->owed_from = node == 0 ? V4_MESH_HERA : node; ms->owed = 1; } + return V4_BOOT_LINE_WAITING; + } + } else asleep = 0; + if (did == 0 && !took && !running) return V4_BOOT_LINE_STOPPED; + } + return V4_BOOT_LINE_STOPPED; +} + +/* ---- POST's host: a line is told to Hera, what she prints handed to the runner ---- */ + +static char post_out[V4_POST_OUT]; +static unsigned post_len, post_cap; +static void post_capture(const char *text, unsigned len) +{ + unsigned i; + for (i = 0; i < len; i++) { if (post_len < post_cap) post_out[post_len] = text[i]; post_len++; } +} +static int post_line(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len) +{ + v4_mesh *ms = (v4_mesh *)self; + void (*was)(const char *, unsigned) = ms->out; + int how; + unsigned i; + post_len = 0; post_cap = cap < sizeof post_out ? cap : (unsigned)sizeof post_out; + ms->out = post_capture; + how = v4_mesh_tell(ms, V4_MESH_HERA, text, text_len); + ms->out = was; + for (i = 0; i < post_len && i < post_cap; i++) out[i] = post_out[i]; + *len = post_len; + return how == V4_TEXT_QUIT || how == V4_TEXT_COMPLETED || how == V4_TEXT_ERROR ? how : -1; +} +static void post_say(void *self, const char *text, unsigned len) { ((v4_mesh *)self)->out(text, len); } + +/* ---- the boot ----------------------------------------------------------------- */ + +/* Every line of a capsule of FORTH blocks, told to Hera; then its parity line, as v4_boot_run prints it. */ +static int load(v4_mesh *ms, const char *name) +{ + static reader r; + char text[V4_MSG_MAX_CHARS + 1u]; + uint32_t length; + uint64_t id, hash; + const uint8_t *p = v4_boot_capsule(&bt, name, &length, &id, &hash); + int n; + v4_fabric_node *h = v4_fabric_node_at(&ms->f, ms->hera_place); + if (!p) return 0; + r.p = p; r.end = p + length; r.in_block = 0; + while ((n = next_line(&r, text, V4_MSG_MAX_CHARS)) >= 0) { + int how = v4_mesh_tell(ms, V4_MESH_HERA, text, (unsigned)n); + if (how != V4_TEXT_COMPLETED) { + say("V4: "); say(name); say(" did not complete: "); ms->out(text, (unsigned)n); say("\n"); + return 0; + } + } + say("PARITY:V4_CAPSULE name="); say(name); + say(" capsule_id="); v4_boot_say_hex(&bt, id); + say(" capsule_hash="); v4_boot_say_hex(&bt, hash); + say(" dict_hash="); v4_boot_say_hex(&bt, v4_image_dict_hash(&h->n, ms->im)); + say("\n"); + return 1; +} + +static int boot(v4_mesh *ms) +{ + v4_fabric_node *h = ms->hera; + unsigned long long steps = 0; + unsigned i; + int place; + + if (ms->capacity == 0 || ms->capacity > MAX_PLACES) { say("V4: the mesh has no room, or more than it can keep\n"); return 0; } + + /* 1. the nucleus: the ROM assembles the system on a node of its own, written as F18 cells */ + bt.n = &ms->nucleus->n; bt.es = &ms->nucleus->es; bt.h = &ms->nucleus->h; bt.im = ms->im; + bt.out = ms->out; bt.key = 0; bt.words = 0; bt.word_count = 0; bt.serve = 0; + bt.request = 0; bt.request_self = 0; bt.capsules = 0; bt.capsule_count = 0; + if (!v4_boot_assemble(&bt)) return 0; + ms->cell_count = 0; ms->given = 0; + (void)v4_capsule_write(&ms->nucleus->n, ms->im->idle, put_cell, ms); + if (ms->cell_count == 0 || ms->cell_count >= V4_MESH_NUCLEUS_CELLS) { say("V4: the nucleus did not fit\n"); return 0; } + + /* 2. the fabric, and Hera born empty at place 0: the kernel on port 0, the console on port 1 */ + v4_fabric_init(&ms->f, ms->places, ms->capacity); + v4_fabric_queues(&ms->f, ms->queues, ms->queue_count); + v4_fabric_gone_error(&ms->f, V4_ERROR_NO_ONE); + for (i = 0; i < MAX_PLACES; i++) { ms->reading_nucleus[i] = 0; reads[i].p = reads[i].end = 0; } + place = v4_fabric_add(&ms->f, h, ms->im->port); + if (place != 0) { say("V4: Hera is not at place 0\n"); return 0; } + ms->hera_place = (unsigned)place; + v4_node_console_input_attach(&h->n, ms->im->console_rx, ms->im->console_status); /* KEY reads these */ + if (!fit(ms->hera_place, h, HERA_ROM, HERA_ROM_WORDS, 1) + || !v4_fabric_wire_device(&ms->f, ms->hera_place, V4_MESH_CONSOLE, &console)) { + say("V4: Hera's ports could not be wired\n"); + return 0; + } + while (!(h->n.doing && h->n.do_op == 8 && ms->given == ms->cell_count)) { /* she takes it in, and sleeps */ + (void)v4_fabric_step(&ms->f); + if (h->n.stopped || ++steps > STEP_LIMIT) { say("V4: Hera did not take in the nucleus\n"); return 0; } + } + + /* 3. who she is; the registry; the kernel's words; FORTH-79 and the kernel's capsules; sealed */ + ms->m.f = &ms->f; ms->m.reg = ms->reg; ms->m.reg_cap = ms->capacity; ms->m.hera = ms->hera_place; + ms->m.born = born_cb; ms->m.gone = gone_cb; ms->m.open = open_cb; + ms->m.cell = cell_cb; ms->m.line = line_cb; ms->m.parity = parity_cb; ms->m.self = ms; + { + int how = v4_mesh_tell(ms, 0, TOLD_FIRST, (unsigned)(sizeof TOLD_FIRST - 1u)); + if (how != V4_TEXT_COMPLETED) { + say("V4: Hera was not told who she is: it ended "); + if (how < 0) { say("-"); v4_boot_say_dec(&bt, (uint32_t)-how); } else v4_boot_say_dec(&bt, (uint32_t)how); + say("\n"); + return 0; + } + } + if (!v4_manage_enter(&ms->m, (v4_cell)V4_MESH_HERA, ms->hera_place, 0)) { say("V4: Hera not entered in the registry\n"); return 0; } + for (i = 0; i < ms->word_count; i++) { + char text[64]; + unsigned at = 0, k, num = i + 1, div = 1; + while (num / div >= 10) div *= 10; + for (; div; div /= 10) text[at++] = (char)('0' + num / div % 10); + for (k = 0; " KERNEL-WORD "[k]; k++) text[at++] = " KERNEL-WORD "[k]; + for (k = 0; ms->words[i][k] && at < sizeof text; k++) text[at++] = ms->words[i][k]; + if (ms->words[i][k] || v4_mesh_tell(ms, V4_MESH_HERA, text, at) != V4_TEXT_COMPLETED) { + say("V4: kernel word "); say(ms->words[i]); say(" could not be made\n"); + return 0; + } + } + if (!load(ms, "v4:forth79.4th")) return 0; + for (i = 0; i < ms->capsule_count; i++) if (!load(ms, ms->capsules[i])) return 0; + v4_image_seal(&h->n, ms->im); + + /* POST, run on her as the kernel's, through her console; then she is put back as sealed */ + { + v4_post_host host; + v4_post_tally tally; + host.self = ms; host.n = &h->n; host.line = post_line; host.say = post_say; + if (!v4_post_run(&host, v4_post_cases, v4_post_case_count, &tally)) { say("V4: POST reported failures\n"); return 0; } + v4_dstack_reset(&h->n.ds); + { + void (*was)(const char *, unsigned) = ms->out; + int how; + post_len = 0; post_cap = 0; + ms->out = post_capture; /* what COLD prints is not shown */ + how = v4_mesh_tell(ms, V4_MESH_HERA, "COLD", 4); + ms->out = was; + if (how != V4_TEXT_COMPLETED && how != V4_TEXT_QUIT) { say("V4: Hera did not come back to the system after POST\n"); return 0; } + } + } + + say("PARITY:V4_SYSTEM word_count="); v4_boot_say_dec(&bt, v4_boot_word_count(&h->n, ms->im)); + say(" dict_hash="); v4_boot_say_hex(&bt, v4_image_dict_hash(&h->n, ms->im)); + say("\n"); + return 1; +} + +int v4_mesh_boot(v4_mesh *ms) +{ + the = ms; + if (!boot(ms)) { say("PARITY:FAIL\nPOST: FAILED\n"); return 0; } + say("PARITY:OK\nPOST: PASSED\n"); + return 1; +} diff --git a/v4/tests/test_mesh_boot.c b/v4/tests/test_mesh_boot.c new file mode 100644 index 00000000..78e0bb11 --- /dev/null +++ b/v4/tests/test_mesh_boot.c @@ -0,0 +1,170 @@ +/* SPDX-License-Identifier: LicenseRef-Starship-1.0 + * Copyright (c) 2023-2026 Robert A. James + */ +/* test_mesh_boot.c -- the kernel's boot of the fabric (v4/mesh.h), on the host, with this program as + * its owner, as the kernel will be (#189, ENGINE step 4 Phase 1, Task 5b; Captain Bob, 2026-11-13: + * "Build it to proper spec"). No scheduler: every node steps together. + * + * The boot: the nucleus assembled by the ROM, Hera born empty and given it, FORTH-79, POST, the + * kernel's capsules -- and the same parity lines v4_boot_run prints. Then: a line; NODE-ME; a node + * born (11 2 2 BIRTH), given the nucleus and FORTH-79 by Hera, answering a line of its own; and + * killed, its room given back. + * + * Linked as the hosted product is (v4/Makefile), with mesh.c, manage.c and a mesh node's ROM. + */ +#include "v4/mesh.h" +#include "v4/message.h" +#include "block_subsystem.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) + +#define PLACES 8u +static v4_place places[PLACES]; +static v4_wire_queue queues[32]; +static v4_manage_entry reg[PLACES]; +static v4_blocks blocks[PLACES]; +static v4_device kernel[PLACES]; +static v4_fabric_node nucleus, hera, pool[PLACES]; +static int used[PLACES]; +static unsigned roomed, given_back, borns, gones, parities; +static v4_image im; +static v4_mesh ms; + +static int no_room; /* the owner has no room to give */ +static v4_fabric_node *room(void *self) { unsigned k; (void)self; if (no_room) return 0; for (k = 0; k < PLACES; k++) if (!used[k]) { used[k] = 1; roomed++; return &pool[k]; } return 0; } +static void unroom(void *self, v4_fabric_node *x) { (void)self; used[x - pool] = 0; given_back++; } +static void born(void *self, unsigned place) { (void)self; (void)place; borns++; } +static void gone(void *self, unsigned place) { (void)self; (void)place; gones++; } +static void parity(void *self, unsigned place, v4_cell number, const v4_node *n) { (void)self; (void)place; (void)number; (void)n; parities++; } + +static char said[1u << 16]; +static unsigned said_len; +static void out(const char *text, unsigned len) +{ + if (said_len + len + 1 < sizeof said) { memcpy(said + said_len, text, len); said_len += len; said[said_len] = 0; } +} +static int key(void) { return V4_BOOT_KEY_END; } +static const char *const words[] = { "BYE" }; +static unsigned served; +static void serve(v4_node *n, unsigned request) { (void)n; if (request == 1) served++; } +static const char *const capsules[] = { "v4:centre.4th", "v4:hera.4th" }; + +static unsigned char ram[(size_t)BLK_RAM_BLOCKS * BLK_FORTH_SIZE]; +extern void sf_time_init(void); +extern void log_set_level(int level); + +/* what a line printed, and how it ended */ +static const char *line(v4_cell node, const char *text, int *how) +{ + said_len = 0; said[0] = 0; + *how = v4_mesh_tell(&ms, node, text, (unsigned)strlen(text)); + return said; +} + +int main(void) +{ + int how; + const char *r; + + printf("v4 mesh boot: V4_CELL_BITS=%d\n", V4_CELL_BITS); + sf_time_init(); + log_set_level(-1); + CHECK(blk_subsys_init(ram, sizeof ram) == BLK_OK, "the kernel's blocks"); + + im = v4_map_image; + ms.places = places; ms.capacity = PLACES; ms.queues = queues; ms.queue_count = sizeof queues / sizeof queues[0]; + ms.reg = reg; ms.blocks = blocks; ms.nucleus = &nucleus; ms.hera = &hera; ms.kernel = kernel; + ms.room = room; ms.unroom = unroom; ms.born = born; ms.gone = gone; ms.parity = parity; + ms.out = out; ms.key = key; ms.words = words; ms.word_count = 1; ms.serve = serve; + ms.capsules = capsules; ms.capsule_count = 2; ms.im = &im; ms.self = 0; + + /* ---- the boot ---- */ + CHECK(v4_mesh_boot(&ms) == 1, "the boot: %s", said); + CHECK(strstr(said, "PARITY:V4_ROM ") != 0, "the ROM assembled the nucleus, and said so"); + CHECK(strstr(said, "PARITY:V4_CAPSULE name=v4:forth79.4th ") && strstr(said, "PARITY:V4_CAPSULE name=v4:centre.4th ") + && strstr(said, "PARITY:V4_CAPSULE name=v4:hera.4th "), "FORTH-79 and the kernel's capsules, each with its parity line"); + CHECK(strstr(said, "PARITY:V4_POST tests=539 pass=539 fail=0") != 0, "POST passes on Hera, born in the fabric"); + CHECK(strstr(said, "PARITY:V4_SYSTEM word_count=") && strstr(said, "PARITY:OK\nPOST: PASSED\n"), "the system line, and POST: PASSED"); + if (failures) { printf(" %d checks, %d failures\n", checks, failures); return 1; } + + /* ---- lines, as messages ---- */ + r = line(V4_MESH_HERA, "3 4 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "7 ") == 0, "a line to Hera: \"%s\" (%d)", r, how); + r = line(V4_MESH_HERA, "NODE-ME .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "0 ") == 0, "NODE-ME: Hera is at place 0: \"%s\"", r); + r = line(V4_MESH_HERA, "NOSUCH", &how); + CHECK(how == V4_TEXT_ERROR && strstr(r, "UNKNOWN WORD") != 0, "an unknown word ends in an error: \"%s\"", r); + r = line(V4_MESH_HERA, "BYE", &how); + CHECK(served == 1, "a kernel word is served by the owner"); + + /* ---- a birth, and a kill ---- */ + r = line(V4_MESH_HERA, "11 2 2 BIRTH .", &how); + CHECK(how == V4_TEXT_COMPLETED && roomed == 1 && borns == 1 && parities == 1, + "11 2 2 BIRTH: a node is born in the owner's room, given the system, and its parity recorded: \"%s\" (%d)", r, how); + CHECK(v4_mesh_node(&ms, 11) != 0, "node 11 is in the fabric"); + r = line(11, "6 7 * .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "42 ") == 0, "node 11 answers a line of its own: \"%s\" (%d)", r, how); + r = line(V4_MESH_HERA, "S\" 6 7 * .\" 11 SEND 11 AWAIT .", &how); + CHECK(how == V4_TEXT_COMPLETED && strstr(r, "42 ") && strstr(r, "1 "), + "and through Hera, as the prompt reaches it: she sends node 11 a line and awaits it: \"%s\" (%d)", r, how); + r = line(V4_MESH_HERA, "11 KILL", &how); + CHECK(how == V4_TEXT_COMPLETED && v4_mesh_node(&ms, 11) == 0 && given_back == 1 && gones == 1, + "11 KILL: the node is gone, and its room given back: \"%s\" (%d)", r, how); + r = line(V4_MESH_HERA, "3 4 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "7 ") == 0, "and Hera goes on: \"%s\"", r); + + /* ---- the edges (Captain Bob, 2026-11-13: "Check for the edge cases") ---- */ + r = line(V4_MESH_HERA, "11 2 2 BIRTH .", &how); + CHECK(how == V4_TEXT_COMPLETED && v4_mesh_node(&ms, 11) != 0, "after a kill, 11 is born again, in the room given back (%d)", how); + r = line(V4_MESH_HERA, "11 3 2 BIRTH .", &how); + CHECK(how == V4_TEXT_ERROR, "a number that is live is not born again: an error (%d) \"%s\"", how, r); + r = line(11, "1 0 /", &how); + CHECK(how == V4_TEXT_ERROR, "a fault on node 11 ends its line in an error (%d) \"%s\"", how, r); + r = line(11, "2 3 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "5 ") == 0, "and node 11 goes on: \"%s\"", r); + r = line(V4_MESH_HERA, "99 KILL", &how); + CHECK(how == V4_TEXT_ERROR && strstr(r, "no such node"), "KILL of a node there is not: an error, \"%s\"", r); + r = line(V4_MESH_HERA, "10 KILL", &how); + CHECK(how == V4_TEXT_ERROR && v4_mesh_node(&ms, 10) != 0, "Hera does not kill herself: \"%s\"", r); + no_room = 1; + r = line(V4_MESH_HERA, "12 3 2 BIRTH .", &how); + CHECK(how == V4_TEXT_ERROR && v4_mesh_node(&ms, 12) == 0, "no room: the birth fails in an error, \"%s\"", r); + no_room = 0; + r = line(V4_MESH_HERA, "3 4 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "7 ") == 0, "and Hera goes on: \"%s\"", r); + { + char big[V4_MSG_MAX_CHARS + 8]; + memset(big, ' ', sizeof big - 1); big[sizeof big - 1] = 0; + r = line(V4_MESH_HERA, big, &how); + CHECK(how == V4_BOOT_LINE_TOO_LONG, "a line too long for a message is not run (%d)", how); + } + /* a node set going for ever does not hold the console: Hera's line ends, and node 11 goes on + * running between lines -- every node steps together, not only while a line runs */ + r = line(V4_MESH_HERA, "S\" VARIABLE N1 : SPIN BEGIN 1 N1 +! AGAIN ;\" 11 SEND 11 AWAIT .", &how); + CHECK(how == V4_TEXT_COMPLETED && strstr(r, "1 "), "node 11 is given SPIN, which runs for ever (%d) \"%s\"", how, r); + r = line(V4_MESH_HERA, "S\" SPIN\" 11 SEND", &how); + CHECK(how == V4_TEXT_COMPLETED, "Hera's line ends, though node 11 now runs for ever (%d) \"%s\"", how, r); + { + v4_fabric_node *x = v4_mesh_node(&ms, 11); + v4_uheat_t before = x ? x->es.anticlock : 0; + unsigned k; + for (k = 0; k < 1000; k++) v4_mesh_step(&ms); /* the host's idle: the fabric steps on */ + CHECK(x && x->es.anticlock > before + 1000u, "between lines node 11 runs on: every node steps together"); + } + r = line(V4_MESH_HERA, "11 KILL", &how); + CHECK(how == V4_TEXT_COMPLETED && v4_mesh_node(&ms, 11) == 0, "and a node running for ever is killed (%d) \"%s\"", how, r); + /* a wait no one will answer: the next line breaks it */ + r = line(V4_MESH_HERA, "11 2 2 BIRTH DROP 12 AWAIT", &how); + CHECK(how == V4_BOOT_LINE_WAITING, "AWAIT on a node that will never say: the line waits (%d)", how); + r = line(V4_MESH_HERA, "3 4 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strstr(r, "Interrupted") && strstr(r, "7 ") > strstr(r, "Interrupted"), + "and the next line ends the wait \"Interrupted\", and is then run (MESH.md 7b.6): \"%s\" (%d)", r, how); + r = line(V4_MESH_HERA, "3 4 + .", &how); + CHECK(how == V4_TEXT_COMPLETED && strcmp(r, "7 ") == 0 && ms.owed == 0, "and nothing more is owed: \"%s\"", r); + + printf(" %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} diff --git a/v4/tools/mkmap.c b/v4/tools/mkmap.c index 2274eabc..c70bf6b2 100644 --- a/v4/tools/mkmap.c +++ b/v4/tools/mkmap.c @@ -53,9 +53,9 @@ static int image_description(void) printf("#include \"v4/image.h\"\n\nconst v4_image v4_map_image = {\n"); printf(" %uu, %uu, %uu, %uu,\n", (unsigned)V4_CELL_BITS, (unsigned)V4_NODE_WORDS, (unsigned)V4_DATA_RING, (unsigned)V4_RET_RING); printf(" -1,\n -1, %ld,\n -1, -1,\n", (long)PORT); - printf(" %ld, %ld, %ld,\n %ld, %ld,\n %ld,\n %ld,\n %ld, %ld,\n %ld, %ld,\n %ld,\n", + printf(" %ld, %ld, %ld,\n %ld, %ld,\n %ld,\n %ld, %ld,\n %ld, %ld,\n %ld, %ld,\n %ld,\n", (long)CONSOLE_TX, (long)CONSOLE_RX, (long)CONSOLE_ST, (long)DSTACK_REG, (long)RSTACK_REG, - (long)NODE_ERROR, (long)HEAT_REG, (long)DP, (long)LATEST, (long)BOOT_CELLS, (long)FENCE, (long)BUF0_W); + (long)NODE_ERROR, (long)HEAT_REG, (long)ME, (long)DP, (long)LATEST, (long)BOOT_CELLS, (long)FENCE, (long)BUF0_W); printf(" %ld, %ld, %ld\n};\n", (long)ROM_IMG, (long)ROM_FAILED, (long)ROM_ERRLINE); return 0; }