Star forth v4.0.0 #3

Closed
admin wants to merge 136 commits from StarForth-v4.0.0 into master
806 changed files with 122425 additions and 2257 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ Checks: >
WarningsAsErrors: [ ]
HeaderFilterRegex: '^src/.*|^include/.*'
HeaderFilterRegex: '^v3/(src|include)/.*|^kernel/(src|include)/.*'
AnalyzeTemporaryDtors: false
FormatStyle: none
+28 -28
View File
@@ -6,8 +6,8 @@
# clang-tidy will ALWAYS false-positive them. Always.
# ---------------------------------------------------------
src/platform/**
src/platform/*
v3/src/platform/**
v3/src/platform/*
# ---------------------------------------------------------
# Skip test infrastructure
@@ -15,8 +15,8 @@ src/platform/*
# and stress edge conditions clang-tidy will never grok.
# ---------------------------------------------------------
src/test_runner/**
src/test_runner/*
v3/src/test_runner/**
v3/src/test_runner/*
# ---------------------------------------------------------
# Skip word definitions generated or "mechanically patterned"
@@ -24,30 +24,30 @@ src/test_runner/*
# clang-tidy gives ZERO useful feedback on them.
# ---------------------------------------------------------
src/word_source/string_words.c
src/word_source/block_words.c
src/word_source/arithmetic_words.c
src/word_source/mixed_arithmetic_words.c
src/word_source/return_stack_words.c
src/word_source/defining_words.c
src/word_source/stack_words.c
src/word_source/logical_words.c
src/word_source/memory_words.c
src/word_source/io_words.c
src/word_source/dictionary_words.c
src/word_source/dictionary_manipulation_words.c
src/word_source/vocabulary_words.c
src/word_source/system_words.c
src/word_source/starforth_words.c
src/word_source/editor_words.c
src/word_source/format_words.c
v3/src/word_source/string_words.c
v3/src/word_source/block_words.c
v3/src/word_source/arithmetic_words.c
v3/src/word_source/mixed_arithmetic_words.c
v3/src/word_source/return_stack_words.c
v3/src/word_source/defining_words.c
v3/src/word_source/stack_words.c
v3/src/word_source/logical_words.c
v3/src/word_source/memory_words.c
v3/src/word_source/io_words.c
v3/src/word_source/dictionary_words.c
v3/src/word_source/dictionary_manipulation_words.c
v3/src/word_source/vocabulary_words.c
v3/src/word_source/system_words.c
v3/src/word_source/starforth_words.c
v3/src/word_source/editor_words.c
v3/src/word_source/format_words.c
# ---------------------------------------------------------
# Skip headers belonging to the word system
# Same reason: no signal, infinite false positives.
# ---------------------------------------------------------
src/word_source/include/*
v3/src/word_source/include/*
# ---------------------------------------------------------
# Skip anything that implements performance knobs,
@@ -55,15 +55,15 @@ src/word_source/include/*
# These are meant to be weird, and tidy can't reason about them.
# ---------------------------------------------------------
src/stack_management.c
src/profiler.c
src/vm_debug.c
src/physics_runtime.c
v3/src/stack_management.c
v3/src/profiler.c
v3/src/vm_debug.c
v3/src/physics_runtime.c
# ---------------------------------------------------------
# Skip integration + stress torture-tests.
# These *intentionally* do UB-like bullshit.
# ---------------------------------------------------------
src/test_runner/modules/*stress*
src/test_runner/modules/*integration*
v3/src/test_runner/modules/*stress*
v3/src/test_runner/modules/*integration*
+23 -21
View File
@@ -176,7 +176,9 @@ capsule files — accurate):
previously described a 1024-byte-per-block budget — right by arithmetic, wrong as a rule):
`validate_forth_blocks` enforces a **64-char × 16-line** format —
**line length ≤ 64 chars, ≤ 16 content lines per block** (`mkcapsule.c:344-345`, `:430`) — and
a block number in **`[2048, 5120)`** (`:408-409`). 64 × 16 = 1024, which is where the old
a block number of **2048 or more, with no upper bound** (`:408-411`; ruled 2026-10-05,
`docs/v4.0.0/NUCLEUS.md` §5.2 — blocks 0–2047, the VM's fast RAM, are the only ones a capsule
may not claim; the check was `[2048, 5120)` before). 64 × 16 = 1024, which is where the old
figure came from, but the enforcement is per-line and per-line-count: **8 lines of 128 chars
is 1024 bytes and still fails.** Verify with `mkcapsule --lint capsules/`, not `wc -c`.
@@ -224,43 +226,43 @@ changes with any new C word registration).
```bash
# Build kernel for a given architecture (amd64 default)
make -f Makefile.starkernel ARCH=amd64
make -f Makefile.starkernel ARCH=aarch64
make -f Makefile.starkernel ARCH=riscv64
make -f kernel/Makefile ARCH=amd64
make -f kernel/Makefile ARCH=aarch64
make -f kernel/Makefile ARCH=riscv64
# Run in QEMU with OVMF
make -f Makefile.starkernel qemu
make -f Makefile.starkernel ARCH=aarch64 qemu
make -f Makefile.starkernel ARCH=riscv64 qemu
make -f kernel/Makefile qemu
make -f kernel/Makefile ARCH=aarch64 qemu
make -f kernel/Makefile ARCH=riscv64 qemu
# Clean
make -f Makefile.starkernel clean
make -f kernel/Makefile clean
```
Output: `build/<arch>/kernel/starkernel_loader.efi` + `build/<arch>/kernel/starkernel_kernel.elf`.
Two independently tracked version strings flow into the generated `include/version.h`:
`VERSION` (`Makefile.starkernel` — the embedded StarForth engine version, currently `3.1.0`;
`VERSION` (`kernel/Makefile` — the embedded StarForth engine version, currently `3.1.0`;
note this does **not** auto-sync with the standalone StarForth repo's own version) and
`LITHOS_VERSION` (`Makefile.starkernel` — the kernel version, currently **`2.0.0`**;
`LITHOS_VERSION` (`kernel/Makefile` — the kernel version, currently **`2.0.0`**;
corrected 2026-09-19 — this file said `2.0.1`, but `FABRIC-3.md` §I.2 rolled it back to
`2.0.0` on 2026-09-04 because `2.0.1` names the SER5 hardware-track line and claims hardware
progress not yet verified. The versioning policy is **semantic, not sequential** — see the
roadmap table in `Makefile.starkernel` before choosing any version).
roadmap table in `kernel/Makefile` before choosing any version).
### Build configuration (Kconfig — real, wired, not vestigial)
Every kernel-only knob (`STARFORTH_ENABLE_VM`, `PARITY_MODE`, the shared physics/heartbeat
family, etc.) is an optional Kconfig symbol defined across `Kconfig`, `Kconfig.arch`,
`Kconfig.heartbeat`, `Kconfig.kernel`, `Kconfig.physics`, `Kconfig.variant` (~40 symbols
total). `Makefile.starkernel` pulls its defaults from this system via a `kconfig_bool(...)`
total). `kernel/Makefile` pulls its defaults from this system via a `kconfig_bool(...)`
mechanism — e.g. `STARFORTH_ENABLE_VM` defaults to **1** (confirmed at
`Makefile.starkernel:61`), meaning a plain `make -f Makefile.starkernel` already builds with
`kernel/Makefile:61`), meaning a plain `make -f kernel/Makefile` already builds with
VM + capsule-birth + ACL active. A bare invocation uses the defaults it always has:
```bash
make -f Makefile.starkernel ARCH=amd64 menuconfig
make -f Makefile.starkernel ARCH=amd64 kernel_amd64_defconfig
make -f kernel/Makefile ARCH=amd64 menuconfig
make -f kernel/Makefile ARCH=amd64 kernel_amd64_defconfig
```
### Hosted VM (vendored, for local sanity only)
@@ -277,7 +279,7 @@ StarForth repo) were removed 2026-08-15 — they referenced
in the actual generated `include/version.h` (which only has `STARFORTH_VERSION`,
`STARFORTH_ARCH`, `STARFORTH_TARGET`, `STARFORTH_TIMESTAMP`, `STARFORTH_VERSION_FULL`,
`LITHOS_VERSION`, `LITHOS_VERSION_STR`), so they could never have worked. Bump versions by hand-editing the `VERSION`/
`LITHOS_VERSION` variables in `Makefile.starkernel` instead. Report the broken targets if
`LITHOS_VERSION` variables in `kernel/Makefile` instead. Report the broken targets if
asked, don't silently fix them.
### Important: Linker Configuration
@@ -298,9 +300,9 @@ The vendored hosted `make` build (above) is NEVER used to validate kernel change
```bash
# Run in this exact order for every kernel change:
make -f Makefile.starkernel ARCH=amd64 clean qemu
make -f Makefile.starkernel ARCH=aarch64 clean qemu
make -f Makefile.starkernel ARCH=riscv64 clean qemu
make -f kernel/Makefile ARCH=amd64 clean qemu
make -f kernel/Makefile ARCH=aarch64 clean qemu
make -f kernel/Makefile ARCH=riscv64 clean qemu
```
**QEMU rule — non-negotiable:** Only ONE QEMU instance may run at a time, always in the
@@ -475,7 +477,7 @@ listed in `proof/ROOT` (corrected 2026-09-19; this file said 23, and `proof/COVE
said 52 all along). **Read `proof/COVERAGE.md` first**: its stated goal is not a green build but
"identify precisely what cannot be proven and why — the boundary between 'formally verified'
and 'not, for this specific reason'." Run `isabelle build -D proof/`
directly; neither `Makefile` nor `Makefile.starkernel` in this repo defines an
directly; neither `Makefile` nor `kernel/Makefile` in this repo defines an
`isabelle-build`/`isabelle-check` target (unlike the standalone StarForth repo, which has a
broken one — this repo simply doesn't have the target at all, so there's nothing to
mistakenly invoke).
@@ -495,7 +497,7 @@ status for kernel work.
- **Strict ANSI C99** — No GNU extensions, no C++ features
- **Zero warnings target, with four explicit exceptions — corrected 2026-08-18, previous
claim was wrong.** Build with `-Wall -Wextra -Werror`, but `Makefile.starkernel` carries
claim was wrong.** Build with `-Wall -Wextra -Werror`, but `kernel/Makefile` carries
`-Wno-error=unused-parameter -Wno-error=shift-negative-value -Wno-error=sign-compare
-Wno-error=missing-field-initializers` — those four classes are enabled (still visible as
warnings) but deliberately downgraded from fatal, everything else is. The previous version
+10 -7
View File
@@ -933,10 +933,12 @@ WARN_LOGFILE = docs/api/doxygen_warnings.log
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
INPUT = include \
src \
src/word_source \
src/test_runner \
INPUT = v3/include \
v3/src \
v3/src/word_source \
v3/src/test_runner \
kernel/include \
kernel/src \
README.md
# This tag can be used to specify the character encoding of the source files
@@ -2366,9 +2368,10 @@ SEARCH_INCLUDES = YES
# RECURSIVE has no effect here.
# This tag requires that the tag SEARCH_INCLUDES is set to YES.
INCLUDE_PATH = include \
src/word_source/include \
src/test_runner/include
INCLUDE_PATH = v3/include \
kernel/include \
v3/src/word_source/include \
v3/src/test_runner/include
# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
# patterns (like *.h and *.hpp) to filter out the header-files in the
+1 -1
View File
@@ -5,7 +5,7 @@ choice
default ARCH_AMD64
help
Selects the value passed as ARCH= to whichever Makefile
(Makefile or Makefile.starkernel) is driven from this config.
(Makefile or kernel/Makefile) is driven from this config.
Both Makefiles already accept "amd64"/"aarch64"/"riscv64" as
canonical spellings (each has its own alias-normalizing logic
for other spellings like x86_64/arm64/riscv), so this choice
+1 -1
View File
@@ -17,7 +17,7 @@ config HEARTBEAT_THREAD_ENABLED
builds without needing a special case. This is UI/model
correctness (menuconfig can't offer a choice LithosAnanke has no
way to honor -- there are no pthreads in a freestanding kernel),
not the sole enforcement mechanism: Makefile.starkernel additionally
not the sole enforcement mechanism: kernel/Makefile additionally
keeps its own unconditional `VM_FEATURE_OVERRIDES +=
-DHEARTBEAT_THREAD_ENABLED=0` post-override exactly as it was
before this migration. Deliberately redundant with the `depends
+12 -1
View File
@@ -11,7 +11,18 @@ config STARFORTH_ENABLE_VM
kernel that only reaches the M0-M6 hardware milestones (console,
PMM, VMM, interrupts, timers, kmalloc) with no FORTH interpreter,
no capsules, no "ok" REPL. Gates a large source-file selection
block in Makefile.starkernel, not just a handful of -D flags.
block in kernel/Makefile, not just a handful of -D flags.
config STARFORTH_V4
bool "Boot StarForth v4, the F18-derived engine, at a single prompt (STARFORTH_V4)"
default n
help
Instead of the v3 VM and its fleet, the kernel starts one StarForth
v4 host node after the M0-M6 hardware milestones: the golden model
of the 32-opcode engine (v4/src) running the capsule image built
from v4/capsule, with its console on the kernel's serial console.
It reaches v4's "ok> " prompt and stays there. The v3 VM is still
compiled in but is not started. See docs/v4.0.0/DECOMPOSITION.md.
config PARITY_MODE
bool "Deterministic parity harness mode (PARITY_MODE)"
+133 -1314
View File
File diff suppressed because it is too large Load Diff
+46 -7
View File
@@ -6,6 +6,25 @@
---
## Products
StarForth v4 moves the engine onto a 32-instruction core derived from Chuck Moore's F18
(`docs/v4.0.0/`). With it, this repository carries three products, not one:
| Product | What it is |
|---|---|
| **Hosted StarForth F18** | The v4 engine as a native Linux build, for amd64, arm64 and riscv64, on real hardware. |
| **FPGA StarForth F18** | A 32-bit build of the same engine loaded into the FPGA. It is the gateway to the rest: the foundation everything else is built up from. |
| **StarshipOS** | The bare-metal product: LithosAnanke running the v4 F18 engine. |
FORTH-79 is recomposed on the F18 engine and stored as a capsule; the StarshipOS-specific
vocabulary gets the same treatment. The source tree will be reorganised around this split.
v3 remains the reference system until v4 meets its acceptance criteria
(`docs/v4.0.0/JUSTIFICATION.md` §16). Everything below describes v3 as it stands today.
---
## Status — M7.1 (Capsule System · Multi-VM Fleet)
| Milestone | | Status |
@@ -48,19 +67,39 @@ POST at boot: **parity hash verified across amd64/aarch64/riscv64 · Mama capsul
## Quick Start
```bash
# One boot image per physical target (output: build/boards/<board>/)
make boot_image TARGET=SER5 # Beelink SER5 -- GPT/ESP, BOOTX64.EFI
make boot_image TARGET=RASPI # Raspberry Pi 5 -- FAT, config.txt + kernel_2712.img + DTB
make boot_image TARGET=MILKV # Milk-V Mars -- GPT/ESP, BOOTRISCV64.EFI (U-Boot bootefi)
make boot_image TARGET=ZYNQ7020 # Zynq-7020 -- ARMv7 port in progress
make boards # list boards
# The same TARGET drives all, docs and clean (make help)
make # every board with a kernel port + hosted v3 + the book
make TARGET=RASPI # one board's image + build/docs/LithosAnanke-raspi.pdf
make docs # build/docs/LithosAnanke.pdf (LaTeX master: docs/book/main.tex)
make clean TARGET=RASPI # that board's outputs and build/aarch64/kernel
make clean # all of build/ except build/cache/
# Build kernel (requires cross-compilation toolchain, or native gcc)
make -f Makefile.starkernel ARCH=amd64
make -f kernel/Makefile ARCH=amd64
# Run in QEMU with OVMF
make -f Makefile.starkernel qemu
make -f kernel/Makefile qemu
# Other architectures
make -f Makefile.starkernel ARCH=aarch64 qemu
make -f Makefile.starkernel ARCH=riscv64 qemu
make -f kernel/Makefile ARCH=aarch64 qemu
make -f kernel/Makefile ARCH=riscv64 qemu
```
Artifacts: `build/amd64/kernel/starkernel_loader.efi` · `build/amd64/kernel/starkernel_kernel.elf`
Tree: `kernel/` (LithosAnanke: `Makefile`, `src/`, `include/starkernel/`, `linker/`),
`v3/` (StarForth v3 engine and hosted build: `Makefile`, `src/`, `include/`),
`v4/` (StarForth v4, see `docs/v4.0.0/`), `boards/<board>/` (`board.mk` + boot files).
Every makefile runs from the repo root; the root `Makefile` forwards any other goal to
`v3/Makefile` (hosted build).
For the hosted VM by itself (Linux, no cross-compiler needed, no bare-metal tooling): see
the separate **StarForth** repository — LithosAnanke used to be a branch inside that repo,
now it's its own project with its own `master`.
@@ -69,12 +108,12 @@ now it's its own project with its own `master`.
Every kernel-only knob (`STARFORTH_ENABLE_VM`, `PARITY_MODE`, the shared
physics/heartbeat family, etc.) is an optional Kconfig symbol — a plain
`make -f Makefile.starkernel` uses the same defaults it always has unless
`make -f kernel/Makefile` uses the same defaults it always has unless
you opt in:
```bash
make -f Makefile.starkernel ARCH=amd64 menuconfig
make -f Makefile.starkernel ARCH=amd64 kernel_amd64_defconfig
make -f kernel/Makefile ARCH=amd64 menuconfig
make -f kernel/Makefile ARCH=amd64 kernel_amd64_defconfig
```
---
+29
View File
@@ -0,0 +1,29 @@
# boards/
One directory per physical target. `make boot_image TARGET=<NAME>` (repo root) maps the
name to a directory here, reads its `board.mk`, and builds the single file that goes on
that board's boot medium into `build/boards/<board>/`.
| TARGET | Directory | ISA | Boot path | Image |
|---|---|---|---|---|
| `SER5` | `ser5/` | amd64 | UEFI firmware, removable-media path | GPT + FAT32 ESP: `EFI/BOOT/BOOTX64.EFI` |
| `RASPI` | `raspi/` | aarch64 | Pi 5 EEPROM firmware, native | MBR + FAT32: `config.txt`, `kernel_2712.img`, `bcm2712-rpi-5-b.dtb` |
| `MILKV` | `milkv/` | riscv64 | SPI-flash U-Boot + OpenSBI, `bootefi` | GPT + FAT32 ESP: `EFI/BOOT/BOOTRISCV64.EFI` |
| `ZYNQ7020` | `zynq7020/` | armv7 | BootROM, `BOOT.BIN`, U-Boot | not yet: needs `kernel/src/arch/armv7` |
`board.mk` sets:
- `BOARD_DESC`: one-line description (`make boards`)
- `BOARD_ARCH`: kernel ISA (`kernel/src/arch/<arch>`)
- `BOARD_BOOT`: image recipe in `kernel/Makefile` (`uefi-esp`, `rpi-native`, ...)
- `BOARD_IMAGE`: output file name
Disk images are assembled by `scripts/mkdiskimage.sh`, which needs no root access and no loop
devices. `KERNEL_ARGS="..."` is written as `starforth.cfg` (UEFI boards) or `cmdline.txt`
(Pi 5, where the firmware copies it into `/chosen/bootargs`). `BOOT_IMAGE_SIZE_MIB` sets the
image size; the default is 128.
Hardware status: none of these images has been booted on real silicon from this build. The
SER5 image has been booted in QEMU (q35 + OVMF, from its own disk) through POST. The Pi 5
native path (`raspi/README.md`) and the JH7110 peripherals have still only been checked by
compiling.
+30
View File
@@ -0,0 +1,30 @@
# Milk-V Mars boot media
`make boot_image TARGET=MILKV` builds `build/boards/milkv/lithos-milkv.img`: a GPT disk with one
FAT32 EFI System Partition. Write it to an SD card with `dd`. The partition holds:
- `EFI/BOOT/BOOTRISCV64.EFI`: the riscv64 monolithic loader, the same one QEMU boots.
- `startup.nsh`: for a UEFI shell, if one is used.
- `starforth.cfg`: only present when `KERNEL_ARGS="..."` is given.
The board's own SPI-flash U-Boot, with OpenSBI underneath, finds the loader through its standard
`bootefi` scan of the removable-media path. U-Boot passes its devicetree to the loader through the
EFI configuration table.
Status: booted in QEMU through the same chain the board uses: OpenSBI, then U-Boot
`qemu-riscv64_smode`, then `bootefi` from this image's ESP, then the loader and the kernel. POST
passed, Stadium conservation was exact (resident 43691 + reservoir 21845 = 65536 = `Q48_ONE`), and
Hestia and Artemis came up. Not yet booted on the Mars itself. The JH7110 peripherals
(framebuffer, RNG) have only been checked by compiling.
To repeat the QEMU boot (`qemu-system-riscv64` is in Ubuntu 26.04's `qemu-system-riscv` package;
OpenSBI and U-Boot come from `opensbi` and `u-boot-qemu`). Use a copy, because the kernel writes
to its disk:
```
cp build/boards/milkv/lithos-milkv.img /tmp/mv.img
qemu-system-riscv64 -machine virt -cpu rv64 -m 2048 -nographic \
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_dynamic.bin \
-kernel /usr/lib/u-boot/qemu-riscv64_smode/u-boot.bin \
-drive if=none,format=raw,file=/tmp/mv.img,id=d0 -device virtio-blk-device,drive=d0
```
+11
View File
@@ -0,0 +1,11 @@
# boards/milkv/board.mk -- Milk-V Mars (StarFive JH7110, SiFive U74, RV64GC).
#
# The board's own SPI-flash U-Boot (OpenSBI underneath) runs the EFI
# application through its standard distro/bootefi scan: the SD card is a GPT
# disk with one FAT32 EFI System Partition holding EFI/BOOT/BOOTRISCV64.EFI,
# the same riscv64 monolithic loader QEMU boots. U-Boot passes its own
# devicetree through the EFI configuration table.
BOARD_DESC := Milk-V Mars (StarFive JH7110, RV64GC, U-Boot bootefi)
BOARD_ARCH := riscv64
BOARD_BOOT := uefi-esp
BOARD_IMAGE := lithos-milkv.img
+20
View File
@@ -0,0 +1,20 @@
# Raspberry Pi 5 native boot media
`make boot_image TARGET=RASPI` builds `build/boards/raspi/lithos-raspi.img`: an MBR disk
with one FAT32 (LBA) partition. Write it to an SD card with `dd`. The partition holds:
- `config.txt`: checked in here (FABRIC-3.md §IV.3 item 6). It pins `kernel_address=0x80000`
because Pi 5 firmware loads `kernel_2712.img` at 0x200000 by default.
- `kernel_2712.img`: the aarch64 kernel objects relinked at 0x80000 by
`kernel/linker/starkernel-native-rpi5.ld`, entered at `rpi5_native_start`
(`kernel/src/arch/aarch64/native_rpi5_entry.S`, which zeroes `.bss` first), and flattened
with `objcopy -O binary`.
- `bcm2712-rpi-5-b.dtb`: the Raspberry Pi firmware's own stock DTB. It is not built by this
project; it is fetched once from the firmware release pinned in `board.mk`
(`RPI_FIRMWARE_TAG`) into `build/cache/`. To use a local copy, pass `RPI5_DTB=/path/to/dtb`.
- `cmdline.txt`: only present when `KERNEL_ARGS="..."` is given. The firmware copies it into
`/chosen/bootargs`, which `rpi5_native_boot()` parses.
Not yet verified on hardware. The native path still assumes everything `kernel_main()`
needs after a UEFI handoff. The firmware enters at EL2 with the MMU off, which is not what
UEFI hands over, and that difference has not been exercised.
+18
View File
@@ -0,0 +1,18 @@
# boards/raspi/board.mk -- Raspberry Pi 5 (BCM2712, Cortex-A76, aarch64).
#
# Native firmware boot, no UEFI (FABRIC-3.md §IV): the Pi 5 bootloader
# lives in EEPROM and reads config.txt, kernel_2712.img and the board DTB
# from the first FAT partition of the SD card. kernel_2712.img is the
# aarch64 kernel relinked at 0x80000 (kernel/linker/starkernel-native-rpi5.ld)
# and flattened to a raw binary.
#
# The DTB is the Raspberry Pi firmware's own stock file, not built here. It
# is fetched once from the pinned firmware release below into build/cache/
# unless RPI5_DTB points at a local copy.
BOARD_DESC := Raspberry Pi 5 (BCM2712, Cortex-A76, native firmware boot)
BOARD_ARCH := aarch64
BOARD_BOOT := rpi-native
BOARD_IMAGE := lithos-raspi.img
RPI_FIRMWARE_TAG ?= 1.20260915
RPI5_DTB_URL ?= https://raw.githubusercontent.com/raspberrypi/firmware/$(RPI_FIRMWARE_TAG)/boot/bcm2712-rpi-5-b.dtb
RPI5_DTB ?= build/cache/raspi-$(RPI_FIRMWARE_TAG)/bcm2712-rpi-5-b.dtb
@@ -12,6 +12,13 @@
# firmware picks up.
kernel=kernel_2712.img
# Pin the load address to the one the image is linked at
# (kernel/linker/starkernel-native-rpi5.ld). Pi 5 firmware's own default
# for kernel_2712.img is 0x200000, not the classic 0x80000 -- leaving it
# implicit would load a 0x80000-linked flat image 1.5 MiB away from where
# every absolute address in it points.
kernel_address=0x80000
# Disable the firmware's Linux-compatible-image sanity check. Without
# this, official docs describe it as checking for "a compatible Device
# Tree file before attempting to boot" and warn that "older non-compatible
+14
View File
@@ -0,0 +1,14 @@
# Beelink SER5 boot media
`make boot_image TARGET=SER5` builds `build/boards/ser5/lithos-ser5.img`: a GPT disk with one
FAT32 EFI System Partition. Write it to a USB stick with `dd`. The partition holds:
- `EFI/BOOT/BOOTX64.EFI`: the amd64 monolithic loader, with the whole kernel embedded. Firmware
finds it through the UEFI removable-media path, so no boot entry has to be created.
- `startup.nsh`: runs the loader if the firmware drops into the UEFI shell.
- `starforth.cfg`: only present when `KERNEL_ARGS="..."` is given.
Nothing in the image is SER5-specific. It follows the generic UEFI/ACPI path in FABRIC-3.md §III.
Status: booted in QEMU (q35 + OVMF, from this image as its own disk) through POST, with Stadium
conservation exact (sum 65536 = `Q48_ONE`). Not yet booted on the SER5 itself.
+10
View File
@@ -0,0 +1,10 @@
# boards/ser5/board.mk -- Beelink SER5 (AMD Ryzen, x86-64, UEFI/ACPI).
#
# Generic standards-compliant UEFI path (FABRIC-3.md §III): nothing in the
# image is SER5-specific. The monolithic loader embeds the whole kernel, so
# the image is a GPT disk with one FAT32 EFI System Partition holding
# EFI/BOOT/BOOTX64.EFI. Write it to a USB stick with dd.
BOARD_DESC := Beelink SER5 (AMD Ryzen, x86-64, UEFI)
BOARD_ARCH := amd64
BOARD_BOOT := uefi-esp
BOARD_IMAGE := lithos-ser5.img
+17
View File
@@ -0,0 +1,17 @@
# Xilinx Zynq-7020 boot media
The Zynq-7020's processing system is a dual Cortex-A9: ARMv7-A, 32-bit. It is the host for the
StarForth v4 mesh (`docs/v4.0.0/JUSTIFICATION.md` §5, §8).
Boot chain: BootROM, then `BOOT.BIN` (U-Boot SPL with the board's `ps7_init` DDR and clock setup),
then U-Boot, then LithosAnanke. Everything is read from the SD card's FAT partition.
Status: no image yet. `make boot_image TARGET=ZYNQ7020` stops with an error, because there is no
ARMv7 kernel port (`kernel/src/arch/armv7`: entry code, MMU, GIC, private timer). A trial
compile for Cortex-A9 built 147 of the 151 kernel and engine sources. The four that failed use
`__int128` or have no ARMv7 branch: `kernel/src/crypto/fe25519.c`,
`kernel/src/crypto/scalar25519.c`, `kernel/src/hal/hal.c` and
`v3/src/word_source/mixed_arithmetic_words.c`.
`BOOT.BIN` depends on the exact board (Zybo Z7-20, PYNQ-Z2, Arty Z7-20, ZedBoard, ...), because
`ps7_init` holds that board's DDR timing. The board has not been chosen yet.
+11
View File
@@ -0,0 +1,11 @@
# boards/zynq7020/board.mk -- Xilinx Zynq-7020 (dual Cortex-A9, ARMv7-A, 32-bit).
#
# Boot chain: BootROM -> BOOT.BIN (U-Boot SPL with the board's ps7_init DDR/
# clock setup) -> U-Boot -> LithosAnanke, all from the SD card's FAT
# partition. Needs the ARMv7 kernel port (kernel/src/arch/armv7), which is
# in progress: until it exists, boot_image stops with an explicit error
# rather than producing an image that cannot run.
BOARD_DESC := Xilinx Zynq-7020 (Cortex-A9, ARMv7-A, BOOT.BIN) -- ARMv7 port in progress
BOARD_ARCH := armv7
BOARD_BOOT := zynq-bootbin
BOARD_IMAGE := lithos-zynq7020.img
-12
View File
@@ -1,12 +0,0 @@
# Raspberry Pi 5 native boot media
What goes on the SD card for the native (non-UEFI) boot path (FABRIC-3.md §IV):
- `config.txt` — checked in here, done (§IV.3 item 6).
- `bcm2712-rpi-5-b.dtb` — Raspberry Pi firmware's own stock DTB; not built by this
project, copied from the firmware release the card is otherwise built from.
- `kernel_2712.img` — **does not exist yet.** `config.txt` names it, but no build target
in `Makefile.starkernel` currently emits a raw image by this name at load address
0x80000 — item 1 (entry stub) explicitly scoped a separate-image build target as future
work, not part of that item. Building this file is the remaining prerequisite before
item 7 (assembling the card) is possible.
+53 -41
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-22T22:29:26Z -->
<!-- Generated by mkcapsule --manifest 2026-10-07T16:22:56Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
@@ -8,42 +8,46 @@
| Capsule | Blocks claimed | xxHash64 | Signed |
|---------|----------------|----------|--------|
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4015 | `0xf8890c05c0d8f921` | yes |
| `acl-std79.4th` | 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048 | `0x773bf9209df191d1` | yes |
| `artemis:init.4th` | 4110, 4111, 4112, 4113, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4177, 4178, 4179, 4180, 4181, 4182, 4851, 4852, 4853, 4854, 4856, 4857, 4858, 4860 | `0xe6fba9ae56e1c916` | yes |
| `block-acl.4th` | 4019, 4020 | `0xf6cc2a59e3a6734e` | yes |
| `doe-campaign.4th` | 4060, 4061, 4062, 4063, 4064, 4065 | `0x26fb485e5c9dc6ff` | yes |
| `doe.4th` | 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107 | `0xf154616d248e861f` | yes |
| `fabric.4th` | 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4911, 4912, 4913, 4914, 4915, 4916, 4917, 4918, 4919, 4920, 4921, 4922, 4923, 4924, 5000, 5001, 5002 | `0x9d9489cbeca4099b` | yes |
| `font.4th` | 4925, 4926, 4927, 4928, 4929, 4930, 4931, 4932, 4933, 4934, 4935, 4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, 4944, 4945, 4946, 4947, 4948, 4949, 4950, 4951, 4952, 4953, 4954, 4955, 4956, 4957, 4958, 4959, 4960, 4961, 4962, 4963, 4964, 4965, 4966, 4967, 4968, 4969, 4970, 4971, 4972, 4973, 4974, 4975, 4976, 4977, 4978, 4979, 4980, 4981, 4982, 4983, 4984, 4985 | `0x3f305911500c78f6` | yes |
| `hestia:init.4th` | 4986, 4987, 4988 | `0x64e111990fbc45ff` | yes |
| `init-l8-diverse.4th` | 4820, 4821, 4822 | `0xaa293201a6c91838` | yes |
| `init-l8-omni.4th` | 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079 | `0x5979e314d6452045` | yes |
| `init-l8-stable.4th` | 4806 | `0xdc3830f189063a9a` | yes |
| `init-l8-temporal.4th` | 4830, 4831 | `0x51abd4c138246651` | yes |
| `init-l8-transition.4th` | 4840, 4841, 4842 | `0xbcc1a81976f0a4c9` | yes |
| `init-l8-volatile.4th` | 4810, 4811, 4812, 4813 | `0x98caabbbd92abac4` | yes |
| `init.4th` | 2049, 2050, 2057 | `0xea038ba684c53443` | yes |
| `lib.4th` | 4050 | `0x4b216635c359ef73` | yes |
| `multiuser-doe.4th` | 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055 | `0x22140588ee7c39e6` | yes |
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | yes |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | yes |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | yes |
| `workload-0.4th` | 2200, 2201 | `0x93f86f60aeba8feb` | yes |
| `workload-1-lite.4th` | 5058, 5059 | `0x44a7a7e3176dcc8d` | yes |
| `workload-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | yes |
| `workload-2.4th` | 4506, 4515, 4525, 4535, 4545 | `0xf113b3d0bcccae47` | yes |
| `workload-3.4th` | 4606, 4615, 4625, 4635, 4645, 4655, 4665 | `0x62b7a71576ad1041` | yes |
| `workload-4.4th` | 2130, 2131, 2132 | `0x099619264e650b1c` | yes |
| `workload-5-lite.4th` | 5056, 5057 | `0xea0303f38824f254` | yes |
| `workload-5.4th` | *(none — raw code capsule)* | `0x526964c7c9506a11` | yes |
| `workload-6.4th` | 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095 | `0x06fc0ce1e369ef5a` | yes |
| `workload-7.4th` | 2150 | `0x02862291387eb7ac` | yes |
| `workload-8.4th` | 2160 | `0x56b7f2f0efa000df` | yes |
| `workload-9.4th` | 4706, 4715, 4725, 4735, 4745 | `0x3f2bec73142aa424` | yes |
| `workload-calib1.4th` | 5043 | `0x3b9f2d17b554fabc` | yes |
| `zuse-eligibility.4th` | 4021, 4022 | `0x7b28f4776a32e0b7` | yes |
| `zuse.4th` | 4016, 4017, 4018 | `0x490ded9be257a90b` | yes |
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4015 | `0xf8890c05c0d8f921` | n/a |
| `acl-std79.4th` | 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048 | `0x773bf9209df191d1` | n/a |
| `artemis:init.4th` | 4110, 4111, 4112, 4113, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4177, 4178, 4179, 4180, 4181, 4182, 4851, 4852, 4853, 4854, 4856, 4857, 4858, 4860 | `0xe6fba9ae56e1c916` | n/a |
| `block-acl.4th` | 4019, 4020 | `0xf6cc2a59e3a6734e` | n/a |
| `doe-campaign.4th` | 4060, 4061, 4062, 4063, 4064, 4065 | `0x26fb485e5c9dc6ff` | n/a |
| `doe.4th` | 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107 | `0xf154616d248e861f` | n/a |
| `fabric.4th` | 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4911, 4912, 4913, 4914, 4915, 4916, 4917, 4918, 4919, 4920, 4921, 4922, 4923, 4924, 5000, 5001, 5002 | `0x9d9489cbeca4099b` | n/a |
| `font.4th` | 4925, 4926, 4927, 4928, 4929, 4930, 4931, 4932, 4933, 4934, 4935, 4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, 4944, 4945, 4946, 4947, 4948, 4949, 4950, 4951, 4952, 4953, 4954, 4955, 4956, 4957, 4958, 4959, 4960, 4961, 4962, 4963, 4964, 4965, 4966, 4967, 4968, 4969, 4970, 4971, 4972, 4973, 4974, 4975, 4976, 4977, 4978, 4979, 4980, 4981, 4982, 4983, 4984, 4985 | `0x3f305911500c78f6` | n/a |
| `hestia:init.4th` | 4986, 4987, 4988 | `0x64e111990fbc45ff` | n/a |
| `init-l8-diverse.4th` | 4820, 4821, 4822 | `0xaa293201a6c91838` | n/a |
| `init-l8-omni.4th` | 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079 | `0x5979e314d6452045` | n/a |
| `init-l8-stable.4th` | 4806 | `0xdc3830f189063a9a` | n/a |
| `init-l8-temporal.4th` | 4830, 4831 | `0x51abd4c138246651` | n/a |
| `init-l8-transition.4th` | 4840, 4841, 4842 | `0xbcc1a81976f0a4c9` | n/a |
| `init-l8-volatile.4th` | 4810, 4811, 4812, 4813 | `0x98caabbbd92abac4` | n/a |
| `init.4th` | 2049, 2050, 2057 | `0xea038ba684c53443` | n/a |
| `lib.4th` | 4050 | `0x4b216635c359ef73` | n/a |
| `multiuser-doe.4th` | 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055 | `0x22140588ee7c39e6` | n/a |
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | n/a |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | n/a |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | n/a |
| `v4:forth79.4th` | 6000, 6001, 6002 | `0x4055641ee17d176b` | n/a |
| `v4:hera.4th` | 8000, 8001, 8002, 8003, 8004 | `0x526252bfef9b722b` | n/a |
| `workload-0.4th` | 2200, 2201 | `0x93f86f60aeba8feb` | n/a |
| `workload-1-lite.4th` | 5058, 5059 | `0x44a7a7e3176dcc8d` | n/a |
| `workload-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | n/a |
| `workload-2.4th` | 4506, 4515, 4525, 4535, 4545 | `0xf113b3d0bcccae47` | n/a |
| `workload-3.4th` | 4606, 4615, 4625, 4635, 4645, 4655, 4665 | `0x62b7a71576ad1041` | n/a |
| `workload-4.4th` | 2130, 2131, 2132 | `0x099619264e650b1c` | n/a |
| `workload-5-lite.4th` | 5056, 5057 | `0xea0303f38824f254` | n/a |
| `workload-5.4th` | *(none — raw code capsule)* | `0x526964c7c9506a11` | n/a |
| `workload-6.4th` | 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095 | `0x06fc0ce1e369ef5a` | n/a |
| `workload-7.4th` | 2150 | `0x02862291387eb7ac` | n/a |
| `workload-8.4th` | 2160 | `0x56b7f2f0efa000df` | n/a |
| `workload-9.4th` | 4706, 4715, 4725, 4735, 4745 | `0x3f2bec73142aa424` | n/a |
| `workload-calib1.4th` | 5043 | `0x3b9f2d17b554fabc` | n/a |
| `zuse-eligibility.4th` | 4021, 4022 | `0x7b28f4776a32e0b7` | n/a |
| `zuse.4th` | 4016, 4017, 4018 | `0xa14d5a7a88791cf6` | n/a |
*Signed column is `n/a`: this manifest run had no `--sign-key`. Re-run with `--sign-key <path>` to check signing status (does not modify or require rebuilding capsule_generated.c).*
## Block Map (sorted by LBN)
@@ -109,9 +113,9 @@
| 4007 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4008 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4015 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4016 | `zuse.4th` | `0x490ded9be257a90b` | ok |
| 4017 | `zuse.4th` | `0x490ded9be257a90b` | ok |
| 4018 | `zuse.4th` | `0x490ded9be257a90b` | ok |
| 4016 | `zuse.4th` | `0xa14d5a7a88791cf6` | ok |
| 4017 | `zuse.4th` | `0xa14d5a7a88791cf6` | ok |
| 4018 | `zuse.4th` | `0xa14d5a7a88791cf6` | ok |
| 4019 | `block-acl.4th` | `0xf6cc2a59e3a6734e` | ok |
| 4020 | `block-acl.4th` | `0xf6cc2a59e3a6734e` | ok |
| 4021 | `zuse-eligibility.4th` | `0x7b28f4776a32e0b7` | ok |
@@ -364,10 +368,18 @@
| 5113 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5114 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5115 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 6000 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 6001 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 6002 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 8000 | `v4:hera.4th` | `0x526252bfef9b722b` | ok |
| 8001 | `v4:hera.4th` | `0x526252bfef9b722b` | ok |
| 8002 | `v4:hera.4th` | `0x526252bfef9b722b` | ok |
| 8003 | `v4:hera.4th` | `0x526252bfef9b722b` | ok |
| 8004 | `v4:hera.4th` | `0x526252bfef9b722b` | ok |
## Conflicts
None.
---
*36 capsule(s) scanned. Re-run `mkcapsule --manifest <dir>` to refresh.*
*38 capsule(s) scanned. Re-run `mkcapsule --manifest <dir>` to refresh.*
+28
View File
@@ -0,0 +1,28 @@
Block 6000
( forth79.4th -- the FORTH-79 Required Word Set for v4, )
( as colon definitions, loaded when the system boots. )
( docs/v4.0.0/NUCLEUS.md. Blocks 6000 up. )
( A word moves here from the assembled nucleus, v4/capsule, )
( once its colon definition passes POST. )
Block 6001
( U* U/MOD -- FORTH-79 unsigned multiply and divide. )
( Neither v3 nor the v4 nucleus had them. )
( The first two words count the bits in a cell. )
: (BITS) ( -- n )
0 1 BEGIN DUP WHILE 2* SWAP 1+ SWAP REPEAT DROP ;
(BITS) CONSTANT (NB)
VARIABLE (UD)
( u1 u2 -- ud the unsigned double product )
: U* UM* ;
Block 6002
( ud u1 -- u2 u3 unsigned: u2 the remainder, u3 the quotient )
( One bit a step: shift ud left, and take u1 from its high )
( cell whenever it goes, counting that in the low cell. )
( A zero divisor is error 11, Division by zero. )
: U/MOD
DUP 0= IF DROP DROP DROP 11 NODE-ERROR ! THEN (UD) !
(NB) 0 DO
DUP 0< >R 2* OVER 0< IF 1+ THEN SWAP 2* SWAP
R> IF (UD) @ - SWAP 1+ SWAP
ELSE DUP (UD) @ U< 0= IF (UD) @ - SWAP 1+ SWAP THEN THEN
LOOP SWAP ;
+80
View File
@@ -0,0 +1,80 @@
Block 8000
( hera.4th -- Hera: the node that has the others born and )
( manages them. docs/v4.0.0/MESH.md sections 9 and 10. )
( She asks whoever holds the fabric for a node, wires it, )
( and sends it its capsules through the port that joins )
( them. The unit rule is here and nowhere else: four outer )
( nodes round a centre, an outer node wired only inside its )
( unit. )
( Text for another node is built in PAD. )
VARIABLE (T#)
: (T0) ( -- ) 0 (T#) ! ;
: (TC) ( c -- ) PAD (T#) @ + C! 1 (T#) +! ;
: (TS) ( baddr u -- )
DUP IF 0 DO DUP I + C@ (TC) LOOP DROP ELSE DROP DROP THEN ;
: (TN) ( n -- ) 0 <# #S #> (TS) 32 (TC) ;
: (T) ( -- baddr u ) PAD (T#) @ ;
Block 8001
( The node being born: its place in the fabric, the number )
( it is to have, and the port of this node that leads to it. )
VARIABLE (KID) VARIABLE (KID#) VARIABLE (KIDP)
: (FAIL) ( -- )
." BIRTH: node " (KID#) @ . ." did not do as it was told" CR
-1 NODE-ERROR ! ;
: (TOLD) ( how -- ) 1 - IF (FAIL) THEN ;
: (TELL) ( baddr u -- ) (KID#) @ SEND (KID#) @ AWAIT (TOLD) ;
( the nucleus, a capsule of F18 code, cell by cell to its port )
: (NUCLEUS) ( -- )
S" v4:nucleus-64.f18" CAPSULE-OPEN 0= IF (FAIL) THEN
BEGIN CAPSULE-CELL WHILE (KIDP) @ PORT! REPEAT DROP ;
( a capsule of FORTH blocks, a line at a time )
: (LINES) ( baddr u -- ) CAPSULE-OPEN 0= IF (FAIL) THEN
BEGIN PAD CAPSULE-LINE DUP 0< 0= WHILE PAD SWAP (TELL) REPEAT
DROP ;
Block 8002
( who it is, where its printing goes, and that everything )
( not told of goes by its port 2, where this node is )
: (WHO) ( -- )
(T0) (KID#) @ (TN) S" (ME) ! " (TS)
(CONSOLE) @ (TN) S" (CONSOLE) ! 2 DEFAULT-ROUTE " (TS)
(ME) @ (TN) S" 2 NEIGHBOUR" (TS)
(T) 0 (KIDP) @ SEND-ON (KID#) @ AWAIT (TOLD) ;
( number port -- place A NODE IS BORN on that port of )
( this one: empty, then the nucleus, then FORTH-79. )
: BIRTH ( number port -- place )
(KIDP) ! (KID#) !
NODE-BORN DUP 0< IF (FAIL) THEN (KID) !
NODE-ME (KIDP) @ (KID) @ 2 NODE-WIRE 0= IF (FAIL) THEN
(NUCLEUS)
(KID#) @ (KIDP) @ ROUTE (KID#) @ (KIDP) @ NEIGHBOUR (WHO)
S" v4:forth79.4th" (LINES)
Block 8003
S" (SEAL)" (TELL)
(KID) @ (KID#) @ NODE-PARITY
(KID) @ ;
( Two outer nodes are joined: port 3 of X to port 4 of Y, )
( and each is told the other is there. )
VARIABLE (XP) VARIABLE (XN) VARIABLE (YP) VARIABLE (YN)
: (X) ( place number -- ) (XN) ! (XP) ! ;
: (Y) ( place number -- ) (YN) ! (YP) ! ;
: (SAY) ( them port me -- ) (KID#) !
(T0) OVER (TN) DUP (TN) S" ROUTE " (TS)
SWAP (TN) (TN) S" NEIGHBOUR" (TS) (T) (TELL) ;
: (JOIN) ( -- )
(XP) @ 3 (YP) @ 4 NODE-WIRE 0= IF (FAIL) THEN
(YN) @ 3 (XN) @ (SAY) (XN) @ 4 (YN) @ (SAY) ;
Block 8004
( n -- A UNIT: four outer nodes, n+1 to n+4, on ports 2 )
( to 5 of this one, and joined in a square: 1-2, 2-4, 4-3, )
( 3-1. The corners across from each other are not wired: )
( what one sends the other goes by this node. )
VARIABLE (U)
VARIABLE (P1) VARIABLE (P2) VARIABLE (P3) VARIABLE (P4)
: (N) ( k -- number ) (U) @ + ;
: UNIT ( n -- ) (U) !
1 (N) 2 BIRTH (P1) ! 2 (N) 3 BIRTH (P2) !
3 (N) 4 BIRTH (P3) ! 4 (N) 5 BIRTH (P4) !
(P1) @ 1 (N) (X) (P2) @ 2 (N) (Y) (JOIN)
(P2) @ 2 (N) (X) (P4) @ 4 (N) (Y) (JOIN)
(P4) @ 4 (N) (X) (P3) @ 3 (N) (Y) (JOIN)
(P3) @ 3 (N) (X) (P1) @ 1 (N) (Y) (JOIN) ;
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
# configs/
Example Kconfig `defconfig` files — starting points for `make -f
Makefile.starkernel ARCH=<arch> defconfig` (kernel) or the equivalent
kernel/Makefile ARCH=<arch> defconfig` (kernel) or the equivalent
hosted target, matching the current committed default build behavior for
each profile. Loaded via `tools/kconfig/conf`.
View File
View File
+112
View File
@@ -0,0 +1,112 @@
# docs/book/Makefile -- the single LithosAnanke book, built from LaTeX.
#
# Run from the repo root (the root Makefile does this):
# make docs -> build/docs/LithosAnanke.pdf
# make docs TARGET=<board> -> build/docs/LithosAnanke-<board>.pdf
# (same book + that board's appendix)
#
# docs/book/main.tex is the master. Markdown sources are converted by pandoc
# into LaTeX fragments under build/docs/gen/ and \input from main.tex; the
# Markdown stays the source of truth until a chapter is rewritten in LaTeX.
# Figures and tables that present data read the CSV directly at LaTeX time
# (pgfplots / pgfplotstable) and cite it with \datasource{path}; see
# docs/book/README.md.
BOOK_DIR := docs/book
OUT := build/docs
GEN := $(OUT)/gen
BOARD ?=
BOARD_NAME ?=
VARIANT := $(if $(BOARD),$(BOARD),book)
VAR_DIR := $(OUT)/$(VARIANT)
PDF := $(OUT)/LithosAnanke$(if $(BOARD),-$(BOARD)).pdf
PANDOC ?= pandoc
LATEXMK ?= latexmk
PANDOC_FILTERS := $(BOOK_DIR)/pandoc/table-widths.lua $(BOOK_DIR)/pandoc/code-breaks.lua
PANDOC_FLAGS := -f gfm -t latex --top-level-division=chapter --wrap=preserve \
$(foreach f,$(PANDOC_FILTERS),--lua-filter=$(f))
# fragment name : Markdown source. The order of \input lines is main.tex's.
CHAPTERS := \
ontology:docs/ONTOLOGY.md \
roadmap:docs/ROADMAP.md \
boards:boards/README.md \
justification:docs/v4.0.0/JUSTIFICATION.md \
decomposition:docs/v4.0.0/DECOMPOSITION.md \
fabric-0:docs/fabric/FABRIC-0.md \
fabric-1:docs/fabric/FABRIC-1.md \
fabric-2:docs/fabric/FABRIC-2.md \
fabric-3:docs/fabric/FABRIC-3.md \
fabric-3-5:docs/fabric/FABRIC-3.5.md \
fabric-3-6:docs/fabric/FABRIC-3.6.md \
fabric-3-7:docs/fabric/FABRIC-3.7.md \
fabric-4:docs/fabric/FABRIC-4.md
ifneq ($(BOARD),)
ifeq ($(wildcard boards/$(BOARD)/README.md),)
$(error boards/$(BOARD)/README.md is missing: it is the board's appendix)
endif
CHAPTERS += board-$(BOARD):boards/$(BOARD)/README.md
endif
chapter_name = $(word 1,$(subst :, ,$(1)))
chapter_src = $(word 2,$(subst :, ,$(1)))
FRAGMENTS := $(foreach c,$(CHAPTERS),$(GEN)/$(call chapter_name,$(c)).tex)
.PHONY: all tools FORCE
all: $(PDF)
tools:
@missing=""; \
for t in $(PANDOC) $(LATEXMK) xelatex; do command -v $$t >/dev/null 2>&1 || missing="$$missing $$t"; done; \
if [ -n "$$missing" ]; then \
echo "Error: make docs needs:$$missing"; \
echo " sudo apt-get install -y pandoc latexmk texlive-xetex texlive-latex-extra texlive-pictures texlive-fonts-recommended fonts-dejavu fonts-dejavu-extra"; \
echo " (or a user-local TinyTeX + pandoc in ~/.local; see docs/book/README.md)"; \
exit 1; \
fi
# --id-prefix keeps heading labels unique across documents that reuse
# section names ("Summary", "Open questions", ...).
define chapter_rule
$(GEN)/$(call chapter_name,$(1)).tex: $(call chapter_src,$(1)) $(PANDOC_FILTERS) | tools
@mkdir -p $(GEN)
@echo " PANDOC $$< -> $$@"
@$(PANDOC) $(PANDOC_FLAGS) --id-prefix=$(call chapter_name,$(1))- $$< -o $$@
endef
$(foreach c,$(CHAPTERS),$(eval $(call chapter_rule,$(c))))
# Pandoc's syntax-highlighting macros, taken from the installed pandoc so the
# fragments and their macros always come from the same version.
$(GEN)/pandoc-highlighting.tex: $(BOOK_DIR)/pandoc/highlighting.latex | tools
@mkdir -p $(GEN)
@printf '```c\nx\n```\n' | $(PANDOC) -f gfm -t latex -s --template=$< -o $@
# Per-build facts the book prints: the commit every \datasource refers to,
# and which board appendix (if any) is included.
$(VAR_DIR)/meta.tex: FORCE
@mkdir -p $(VAR_DIR)
@{ \
c=$$(git rev-parse --short=12 HEAD 2>/dev/null || echo unknown); \
git diff --quiet HEAD -- 2>/dev/null || c="$$c (+ uncommitted changes)"; \
printf '\\newcommand{\\bookcommit}{%s}\n' "$$c"; \
printf '\\newcommand{\\bookdate}{%s}\n' "$$(date -u +%Y-%m-%d)"; \
printf '\\def\\bookboard{%s}\n' "$(BOARD_NAME)"; \
$(if $(BOARD),printf '\\newcommand{\\bookboardappendix}{board-%s}\n' "$(BOARD)";) \
} > $@.tmp
@cmp -s $@.tmp $@ && rm -f $@.tmp || mv $@.tmp $@
FORCE:
TEXINPUTS_BOOK := $(abspath $(VAR_DIR)):$(abspath $(GEN)):$(abspath $(BOOK_DIR)):
$(PDF): $(BOOK_DIR)/main.tex $(FRAGMENTS) $(GEN)/pandoc-highlighting.tex $(VAR_DIR)/meta.tex | tools
@echo " LATEXMK $(BOOK_DIR)/main.tex -> $@"
@TEXINPUTS=$(TEXINPUTS_BOOK) $(LATEXMK) -xelatex -interaction=nonstopmode -halt-on-error \
-file-line-error -outdir=$(VAR_DIR) $(BOOK_DIR)/main.tex > $(VAR_DIR)/latexmk.log 2>&1 || { \
grep -A4 -E '^(.*:[0-9]+:|!)' $(VAR_DIR)/main.log | head -40; \
echo "Error: LaTeX failed; full log: $(VAR_DIR)/main.log"; exit 1; }
@cp $(VAR_DIR)/main.pdf $@
@echo " PDF $@"
+66
View File
@@ -0,0 +1,66 @@
# docs/book/
`make docs` builds one PDF, `build/docs/LithosAnanke.pdf`, from `main.tex` with xelatex.
`make docs TARGET=<board>` builds `build/docs/LithosAnanke-<board>.pdf`: the same book with
`boards/<board>/README.md` added as an appendix.
Tools: `sudo apt-get install -y pandoc latexmk texlive-xetex texlive-latex-extra texlive-pictures texlive-fonts-recommended fonts-dejavu fonts-dejavu-extra`
Without root, a user-local TeX Live works too (this is how the book was first built):
TinyTeX (`curl -sL https://yihui.org/tinytex/install-bin-unix.sh | sh`) plus
`tlmgr install latexmk xetex fontspec pgf pgfplots booktabs multirow fancyvrb fvextra lineno upquote ulem enumitem newunicodechar bookmark hyperref geometry xcolor graphics tools etoolbox fancyhdr truncate amsfonts`,
and the pandoc release tarball unpacked into `~/.local`.
The installed DejaVu decides italics: `fonts-dejavu-core` alone has no serif italic, so
`main.tex` falls back to a slanted upright face; `fonts-dejavu-extra` gives real italics.
## How it fits together
- `main.tex` is the master file. It sets the parts and the chapter order.
- Chapters that are still Markdown are listed in `Makefile` (`CHAPTERS`, as `name:source.md`).
At build time pandoc converts each one into `build/docs/gen/<name>.tex`, and `main.tex`
includes it with `\input{<name>}`. The Markdown stays the source until the chapter is
rewritten in LaTeX. At that point the `.tex` moves into this directory and the `CHAPTERS`
entry is removed.
- `pandoc/highlighting.latex` extracts the code-highlighting macros from the installed pandoc,
so the macros and the fragments always come from the same pandoc version.
- Two Lua filters run on every chapter. `pandoc/table-widths.lua` gives wide tables
proportional wrapping columns, because pandoc's gfm reader leaves column widths unset.
`pandoc/code-breaks.lua` lets long inline identifiers and paths break after `_ / . - :`.
Code blocks wrap through fvextra (`breaklines`).
- `build/docs/<book|board>/meta.tex` is written on every build. It records the commit (and
whether there were uncommitted changes), the date, and the board.
## Rule for data
Any figure or table that shows measured numbers is generated from the CSV when the book is
built. Numbers are never typed in by hand:
```latex
\begin{tikzpicture}
\begin{axis}[xlabel=run, ylabel=K]
\addplot table[col sep=comma, x=run, y=K]{experiments/<campaign>/results.csv};
\end{axis}
\end{tikzpicture}
\datasource{experiments/<campaign>/results.csv}
```
Use `\pgfplotstabletypeset[col sep=comma]{...}` for tables. `\datasource` prints the CSV path
and the commit, so a reader can find the exact file the figure was drawn from. CSV paths are
repo-relative, because xelatex runs from the repo root.
## Older pipelines to fold in or retire
These still exist and still have their own targets. Each one needs a decision during the
curation pass: move it into this book, or retire it.
| Source | Current target | Produces |
|---|---|---|
| `docs/formal/vol1-vm-physics`, `vol2-kernel`, `vol3-research` | `make -C docs/formal vols` | three volume PDFs |
| `docs/formal/dev-guide`, `user-guide`, `cookbook` | `make -C docs/formal books` | three practitioner PDFs |
| `docs/formal/experiments`, `proofs`, `ssrn`, `patent` | `make -C docs/formal standalone` | four standalone PDFs |
| Doxygen (`Doxyfile`) | `make -C docs/formal doxygen` | API reference PDF |
| `scripts/generate-doxygen-appendix.sh` | `make -f v3/Makefile api-docs` | AsciiDoc API appendix |
| `docs/src/internal/formal/*.thy` | `make -f v3/Makefile docs-isabelle` | Isabelle report |
| `scripts/asciidoc-to-latex.sh` | `make -f v3/Makefile docs-latex` | `docs/latex/` |
| `docs/SSRN_companion/Math_Companion_SSRN.tex` | `make -f v3/Makefile math-companion` | SSRN math companion |
+161
View File
@@ -0,0 +1,161 @@
% docs/book/main.tex -- master file of the single LithosAnanke book.
%
% Built by `make docs [TARGET=<board>]` (docs/book/Makefile) with xelatex.
% Fragments named below (ontology, fabric-0, ...) are generated by pandoc
% from the Markdown listed in docs/book/Makefile's CHAPTERS into
% build/docs/gen/; meta.tex is generated per build. Neither is committed.
\documentclass[11pt,openany]{book}
\usepackage{amsmath,amssymb}
\usepackage{fontspec}
% fonts-dejavu-core has no serif/sans italics (fonts-dejavu-extra does); when a
% shape is missing, slant the upright one rather than silently drop emphasis.
\setmainfont{DejaVu Serif}[AutoFakeSlant=0.2]
\setsansfont{DejaVu Sans}[AutoFakeSlant=0.2]
\setmonofont{DejaVu Sans Mono}[Scale=0.85]
\newfontfamily\symbolfont{DejaVu Sans}
\usepackage[letterpaper,margin=1in]{geometry}
\usepackage{xcolor}
\usepackage{graphicx}
\usepackage{longtable,booktabs,array,calc,multirow}
\usepackage{fancyvrb}
\usepackage[normalem]{ulem}
\usepackage{enumitem}
\usepackage{newunicodechar}
\usepackage{pgfplots}
\usepackage{pgfplotstable}
\pgfplotsset{compat=1.18}
\usepackage[hidelinks]{hyperref}
\usepackage{bookmark}
% --- what pandoc's LaTeX fragments expect (normally from its own template)
\providecommand{\tightlist}{\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
\providecommand{\pandocbounded}[1]{#1}
\providecommand{\st}[1]{\sout{#1}}
\newcounter{none} % pandoc: {\def\LTcaptype{none} ...} marks unnumbered tables
\usepackage{etoolbox}
\makeatletter
\def\fnum@table{\tablename~\thetable}
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
\makeatother
\usepackage{fvextra}
\input{pandoc-highlighting}
% Code and diagrams are wider than the page in places: wrap, never overflow.
\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\},breaklines,breakanywhere,fontsize=\small}
\RecustomVerbatimEnvironment{verbatim}{Verbatim}{breaklines,breakanywhere,fontsize=\small}
\setlistdepth{9}
\renewlist{itemize}{itemize}{9}
\setlist[itemize]{label=\textbullet}
\renewlist{enumerate}{enumerate}{9}
\setlist[enumerate]{label=\arabic*.}
% Running heads: chapter on the left page, section on the right, both cut to
% the page width (several design-record titles are a full sentence long).
\usepackage{fancyhdr}
\usepackage[fit]{truncate}
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\chaptermark}[1]{\markboth{#1}{}}
\renewcommand{\sectionmark}[1]{\markright{#1}}
\fancyhead[LE]{\small\truncate{\dimexpr\headwidth-3em}{\leftmark}}
\fancyhead[RO]{\small\truncate{\dimexpr\headwidth-3em}{\rightmark}}
\fancyhead[RE,LO]{\small\thepage}
\renewcommand{\headrulewidth}{0.4pt}
\setlength{\headheight}{14pt}
\fancypagestyle{plain}{\fancyhf{}\fancyfoot[C]{\small\thepage}\renewcommand{\headrulewidth}{0pt}}
\setlength{\emergencystretch}{3em}
\setlength{\parindent}{0pt}
\setlength{\parskip}{0.5em}
% --- glyphs DejaVu Serif lacks: the status marks used across the docs
\newunicodechar{✅}{{\symbolfont ✔}}
\newunicodechar{✓}{{\symbolfont ✓}}
\newunicodechar{❌}{{\symbolfont ✘}}
\newunicodechar{✗}{{\symbolfont ✗}}
\newunicodechar{⬜}{{\symbolfont ☐}}
\newunicodechar{⭐}{{\symbolfont ★}}
\newunicodechar{🔶}{{\symbolfont ◆}}
\newunicodechar{⚠}{{\symbolfont ⚠}}
\newunicodechar{📋}{}
\newunicodechar{🐛}{[bug]}
\newunicodechar{🎯}{[goal]}
\newunicodechar{🟡}{{\symbolfont ●}}
\newunicodechar{📍}{{\symbolfont ▸}}
\newunicodechar{🔓}{[unlocked]}
\newunicodechar{❓}{?}
\newunicodechar{⟺}{\ensuremath{\Longleftrightarrow}}
\newunicodechar{⋯}{\ensuremath{\cdots}}
\newunicodechar{^^^^fe0f}{}
% --- the documents number their own sections (§III.1, D-3, ...)
\setcounter{secnumdepth}{0}
\setcounter{tocdepth}{1}
\input{meta}
% \datasource{path/to/data.csv}: cite the data behind a figure or table.
% Every figure or table of measured numbers reads its CSV at build time
% (\addplot table / \pgfplotstabletypeset) and is followed by this line, so
% the reader can find the exact file at the exact commit.
\newcommand{\datasource}[1]{%
\par{\small Data: \texttt{\detokenize{#1}} at commit \texttt{\bookcommit}.}\par}
\title{LithosAnanke and StarForth}
\author{}
\date{Built \bookdate{} from commit \texttt{\bookcommit}%
\ifx\bookboard\empty\else\\Board appendix: \bookboard\fi}
\begin{document}
\frontmatter
\maketitle
\tableofcontents
\chapter{About this book}
This book is generated from the repository by \texttt{make docs}. Each
chapter below is still maintained as the Markdown file named in the table and
converted at build time; a chapter moves into LaTeX source when it is
rewritten. Part~III is the design record: those documents are archival and
are reproduced as written, including their struck-through corrections.
\begin{longtable}{@{}ll@{}}
\toprule
Chapter & Source \\
\midrule
\endhead
Ontology & \texttt{docs/ONTOLOGY.md} \\
Roadmap & \texttt{docs/ROADMAP.md} \\
Boot targets & \texttt{boards/README.md} \\
v4 justification & \texttt{docs/v4.0.0/JUSTIFICATION.md} \\
v4 decomposition & \texttt{docs/v4.0.0/DECOMPOSITION.md} \\
FABRIC-0 \dots{} FABRIC-4 & \texttt{docs/fabric/FABRIC-*.md} \\
Board appendix (\texttt{TARGET=}) & \texttt{boards/<board>/README.md} \\
\bottomrule
\end{longtable}
\mainmatter
\part{The system}
\input{ontology}
\input{roadmap}
\input{boards}
\part{StarForth v4}
\input{justification}
\input{decomposition}
\part{Design record (FABRIC series)}
\input{fabric-0}
\input{fabric-1}
\input{fabric-2}
\input{fabric-3}
\input{fabric-3-5}
\input{fabric-3-6}
\input{fabric-3-7}
\input{fabric-4}
\ifdefined\bookboardappendix
\appendix
\input{\bookboardappendix}
\fi
\end{document}
+29
View File
@@ -0,0 +1,29 @@
-- docs/book/pandoc/code-breaks.lua
--
-- Inline code in the design record is full of long identifiers and paths
-- (g_wirebind_attached_username, kernel/src/arch/riscv64/...), which LaTeX
-- cannot break, so they run into the margin. Emit inline code as \texttt
-- ourselves, escaped the same way pandoc does, with a break opportunity
-- after each separator character.
local BREAK_AFTER = { ["_"] = true, ["/"] = true, ["."] = true, ["-"] = true,
[":"] = true, [","] = true, ["("] = true, ["="] = true }
local ESCAPE = {
["\\"] = "\\textbackslash{}", ["{"] = "\\{", ["}"] = "\\}",
["$"] = "\\$", ["&"] = "\\&", ["#"] = "\\#", ["%"] = "\\%", ["_"] = "\\_",
["^"] = "\\textasciicircum{}", ["~"] = "\\textasciitilde{}",
["'"] = "\\textquotesingle{}", ["`"] = "\\textasciigrave{}",
[" "] = "\\ ",
}
function Code(el)
if not FORMAT:match("latex") then return nil end
local out = {}
for _, cp in utf8.codes(el.text) do
local ch = utf8.char(cp)
out[#out + 1] = ESCAPE[ch] or ch
if BREAK_AFTER[ch] then out[#out + 1] = "\\allowbreak{}" end
end
return pandoc.RawInline("latex", "\\texttt{" .. table.concat(out) .. "}")
end
+3
View File
@@ -0,0 +1,3 @@
$if(highlighting-macros)$
$highlighting-macros$
$endif$
+55
View File
@@ -0,0 +1,55 @@
-- docs/book/pandoc/table-widths.lua
--
-- pandoc's gfm reader leaves every column width unset, so the LaTeX writer
-- emits `l` columns and a table with long cells runs off the page. For any
-- table wider than WIDE characters, give each column a width proportional
-- to its longest cell (clamped to [MIN_COL, MAX_COL] characters); the
-- writer then emits wrapping p{} columns that together fill \linewidth.
local WIDE = 72
local MIN_COL = 6
local MAX_COL = 60
local stringify = pandoc.utils.stringify
local function measure(rows, widths)
for _, row in ipairs(rows) do
local col = 1
for _, cell in ipairs(row.cells) do
local span = cell.col_span or 1
local len = utf8.len(stringify(cell.contents)) or #stringify(cell.contents)
local per = len / span
for c = col, col + span - 1 do
if per > (widths[c] or 0) then widths[c] = per end
end
col = col + span
end
end
end
function Table(tbl)
local n = #tbl.colspecs
local widths = {}
for i = 1, n do widths[i] = 0 end
measure(tbl.head.rows, widths)
for _, body in ipairs(tbl.bodies) do
measure(body.head, widths)
measure(body.body, widths)
end
measure(tbl.foot.rows, widths)
local total = 0
for i = 1, n do total = total + widths[i] end
if total <= WIDE then return nil end
local sum = 0
for i = 1, n do
widths[i] = math.max(MIN_COL, math.min(MAX_COL, widths[i]))
sum = sum + widths[i]
end
for i = 1, n do
tbl.colspecs[i] = { tbl.colspecs[i][1], widths[i] / sum }
end
return tbl
end
File diff suppressed because it is too large Load Diff
+434
View File
@@ -0,0 +1,434 @@
# StarForth v4.0.0 — The F18 engine in the VM's place
Design, 2026-10-05. Written from Captain Bob's rulings of that day
(`V3-PARITY.md` sections 1a to 1h, where each is recorded with its words)
and from v3's code. It replaces the lone-node assumptions of `NUCLEUS.md`
sections 5.3 and 7 to 9.
## 1. The principle
`JUSTIFICATION.md` section 16: v4 is equivalent to v3 at any point in time;
"the only difference is the machine underneath: the F18-derived engine
instead of the original StarForth VM."
Ruled 2026-10-05: v4 is exactly like v3 in functional requirements up to
the first FORTH prompt, and that is the stopping point for now. Nothing is
stubbed, simulated or stood in for.
So v4 is not a new system beside LithosAnanke. It is LithosAnanke with a
different machine executing its FORTH.
## 2. What stays, and what is replaced
**Stays, untouched:** everything the kernel does. The Stadium and each
patron's own accounts; sessions; the switcher; kernel-Hermes; the capsule
directory, birth protocol, hashing, signing and parity; the block
subsystem and its devices; identity and Zuse; the HAL console, Hestia, the
console proxies and the kernel's REPL; the heartbeat; the boot order in
`kernel_main.c`.
**Replaced:** the part of a v3 VM that executes FORTH.
| v3 | v4 |
|---|---|
| The inner interpreter (`v3/src/vm.c`, `kernel/src/vm/vm_core.c`) | The node: `v4/src`, 32 opcodes |
| The dictionary as a list of C `DictEntry` records | The dictionary in the node's memory (`v4/capsule/dict.v4`) |
| The FORTH-79 words as C functions (`v3/src/word_source`) | The assembled nucleus (`v4/capsule/*.v4`) and colon definitions in `capsules/v4/forth79.4th` |
| The outer interpreter and compiler in C | The same, in the nucleus |
A v3 VM is also a record the kernel keeps: its identity, its Stadium ID,
its heartbeat state, its place in the registry, its Zuse flags. That
record stays. What changes is what stands behind it.
## 3. The interface
The kernel reaches a VM through a small number of things. Each is listed
with what v3 has, what was ruled, and what a node needs. This is the whole
of the work: when a node answers all of these, the kernel's boot runs on
it as it runs on a v3 VM.
### 3.1 Interpret this text
*v3:* `vm_interpret(vm, text)`. The kernel's REPL hands over each line it
has read; `capsule_exec_payload` hands over each line of a capsule;
kernel-Hermes hands over a message's payload when the target drains its
queue. The VM runs the text and returns, with its error flag set or not.
*Ruled:* the kernel hands a node a whole line and takes characters back;
the node's own prompt loop plays no part at that level. A node receives a
message by being handed its text.
*v4:* the node has an entry that interprets the text in its input buffer
and then stops, saying how the line ended: completed, ended in an error,
or `QUIT`. It prints no prompt and no ` ok`. The buffer takes 1024
characters, a block, as v3's does. `QUIT` and `ABORT` end the line and
return to whoever handed it over. `KEY`, `EXPECT` and `QUERY` stay
FORTH-79 words that read characters.
With this one entry v3's capsule loader, REPL and message drain can all
drive a node, and `v4/system/boot.c`'s own loader is not needed.
### 3.2 A character out
*v3:* the host service `putc`, then `console_putc`; the fabric adds
`[user@VM]`.
*v4:* the node's `EMIT` gives the character to its host, which calls the
same service. Present today.
### 3.3 Asking the kernel
*v3:* a kernel word is a C function registered in the VM's dictionary:
`BIRTH`, `KILL`, `USE`, the block words, the Stadium and Hermes words, the
framebuffer words. It takes its arguments from the VM's data stack.
*Ruled:* a node asks for a block by number and the kernel decides the
rest; a node sends a message by asking.
*v4, ruled 2026-10-05:* a kernel word is an ordinary dictionary entry on
the node whose body writes its request number to the node's port. The
write blocks the node until the kernel, its neighbour on that port, has
served it (`DECOMPOSITION.md` section 6: "a write blocks until the
neighbour reads"). The kernel serves between the node's opcodes, so the
node is always stopped when C touches it; the function takes its arguments
from the node's data stack and leaves its results there. Kernel words are
made by handing the node text at boot. The C functions are v3's.
### 3.4 The stacks and the dictionary, from the kernel's side
*v3:* `vm_push`, `vm_pop`, `vm_find_word`, and the fields of a
`DictEntry` (the kernel pins `BIRTH` and `CAPSULE-BIRTH` by setting two of
them).
*v4:* the same operations on the node's stacks and on entries in the
node's memory.
### 3.5 A word is being executed
*v3:* the inner loop, for every word: the ACL's countdown and recheck,
`execution_heat`, `stadium_word_dispatch`, the rolling window, pipelining.
*Ruled:* the word card stays exactly as v3 — a run-time check, the TTL
adaptive from the word's own count. Every kind of patron keeps its own
accounts; the word's are as v3. Opcodes are counted and left unwired.
*v4:* the node dispatches a word at the `call` opcode, one place in the
engine. The node does there what v3's loop does. A word that is to be
checked must be called, so the in-line words that can be called become
calls.
### 3.6 An error
*v3:* `vm->error`. *v4:* how a line ended (3.1), and the node's error
register for a C function to raise one.
### 3.7 A tick
*v3:* the kernel's timer drives `vm_tick` and the heartbeat state in the
VM's record. *v4:* unchanged; that state is the record's, not the
engine's.
### 3.8 The dictionary hash
*v3:* `vm_dict_hash_fn`, a hook the birth protocol calls. *v4:* the same
hook, answered from the node's dictionary.
## 3b. A word's two halves (ruled 2026-10-05: "A is good")
`V3-PARITY.md` section 1j. A word on a node is its name and its code, in
the node's memory, with its word ID in its header. Its accounts are v3's
own `DictEntry` record, kept by the kernel: `execution_heat`, `physics`,
the four ACL fields, the transition metrics, the word ID. The two are
joined by the word ID.
- The node tells the kernel when a word is defined and when words are
forgotten, by a request through its port. The kernel makes or drops the
record.
- At each `call` the kernel does on the record what v3's inner loop does
(3.5).
- v3's physics, heartbeat, ACL words, Stadium word layer and parity run on
the records, unchanged. The ACL fields v4 keeps in an entry's flags cell
(`v4/capsule/acl.v4`) go: v3's C ACL words serve a node as they are.
## 3c. What exactly is swapped, in v3's own files
Measured 2026-10-05. Each build already has one file that is the
interpreter, and the rest of v3 calls into it:
| Build | The interpreter file | The stacks |
|---|---|---|
| Hosted | `v3/src/vm.c` | `v3/src/stack_management.c` |
| Kernel | `kernel/src/vm/vm_core.c` (the kernel build leaves `v3/src/vm.c` out) | the same |
The functions in it that execute FORTH, and so are the node's to answer:
`vm_interpret`, `vm_interpret_word`, `execute_colon_word`, `acl_recheck`,
`vm_parse_word`, `vm_parse_number`, `vm_enter_compile_mode`,
`vm_exit_compile_mode`, `vm_compile_word`, `vm_compile_literal`,
`vm_compile_call`, `vm_compile_exit`, `vm_make_immediate`; the memory
accessors `vm_addr_ok`, `vm_ptr`, `vm_load_u8`, `vm_store_u8`,
`vm_load_cell`, `vm_store_cell`; and the stacks, `vm_push`, `vm_pop`,
`vm_rpush`, `vm_rpop`. The rest of those files — host services, the time
base, `vm_cleanup` — is not the engine and stays.
So the swap is a third interpreter file, for both builds: the same
functions, answered by a node. Everything else of v3 is compiled as it is.
**The two products part here (Captain Bob, 2026-10-05):** "I would say we
are at a fair point where the hosted product and the bare metal product
diverge completely."
This section first said the hosted v4 product should become v3's hosted
program with the node as its interpreter. That is withdrawn. From here:
- **The bare-metal product** is LithosAnanke with the node in the VM's
place. Everything in this document about the kernel's interface, word
records, the hook at `call`, the fleet and identity is the bare-metal
product's. The third interpreter file is for the kernel build.
- **The hosted product** is its own thing and is not required to follow
the bare-metal one. Today it is the node, the nucleus, the FORTH-79
capsule, POST and its own prompt (`v4/tools/hosted.c`,
`v4/system/boot.c`), and it works on three ISAs.
- **The six builds no longer have to print the same lines.** The three
hosted builds agree with each other; the three bare-metal builds agree
with each other.
**What the two share (confirmed by Captain Bob the same day, "yes,
exactly"):** the engine in `v4/src`; the FORTH-79 capsule,
`capsules/v4/forth79.4th`; and its POST (then `capsules/v4/post79.4th`; since `MESH.md` step 6b, 2026-10-07, the kernel's cases, `v4/system/post_cases.c`). **The
nuclei are separate.** The bare-metal nucleus needs word IDs in its
headers and words that are called where the hosted one compiles them in
line; the hosted nucleus need not have either. The hosted product may
become a hosted version of an SDK.
**Two things in v3's C words do not carry over as they are**, and must be
dealt with word by word:
- A C word that takes an address from the stack and reads memory through
`vm_ptr` gets a pointer into v3's flat bytes. A node's bytes are four to
a cell (D-1), so they are not a flat run of C bytes. Such a word needs
the text copied out of the node. `mama_forth_words.c` has 40 such uses.
- A C word that reaches into `vm->data_stack` or `vm->dsp` directly must
go through `vm_push`, `vm_pop` or a depth accessor. `mama_forth_words.c`
has 45.
Both are changes to files v3 also builds, so each must leave v3 as it is
and be accepted by v3's own three-ISA boot.
## 3a. Many VMs at once (Captain Bob, 2026-10-05)
> Do not forget that this is multiuser, multitasking, and a hybrid of
> preemptive and cooperative.
And, the same day: "Hera will be the process manager via compudynamics per
node."
What that asks of the engine, and what it has:
- **A node can be stopped between any two instruction words and gone on
with later.** Everything a node is doing is in the node and its
execution state; `v4_exec_step_word` runs one instruction word and
returns. So whoever runs the nodes can take the processor from one at any
word and give it to another: that is the preemptive half, and the engine
already allows it. Nothing may be built that needs a node to run a line,
or a request, to its end without interruption.
- **A node gives way by itself when it writes to its port.** It is blocked
until served (3.3), and while it is blocked another can run: that is the
cooperative half.
- **Each node has its own execution state.** The place a blocked node goes
on from is kept per node (`v4_exec_state`), never in one shared place.
- **Each user is a VM** (`FABRIC-2.md` D.2: "a session IS a VM"), so each is
a node, with its own dictionary, stacks and ACL cards.
What is not designed: who decides which node runs next, and when. In v3
that is the switcher, reading what kernel-Hermes publishes, at the
checkpoint in the inner loop; the ruling makes it Hera's, by compudynamics.
It is step 6's, and section 6 lists it as open.
**The loop that runs one node until its line ends** (`v4_boot_line`,
`v4/system/boot.c`) is the lone node's and the hosted program's, where
there is one node and nothing to share the processor with. It is not how
the kernel will run a fleet, and goes with the lone node at step 5.
## 3d. Where v4 is going, and the next step (Captain Bob, 2026-10-05)
> The entire point is that ultimately we have F18 engines digesting
> capsules alone, and in a sense can be anything written in F18 assembler
> for our fabric — 144 someday as a 12x12 grid, but I want a 12^3 FPGA
> ultimately, where using a capsule digester like StarForth, it's more
> than an operating system.
So the unit is an engine and the capsule it digests; StarForth is one
digester. The engine in `v4/src` must stay free of anything specific to
StarForth or to the kernel, and what is built to make v4 equal v3 on a PC
(section 3b's records, v3's C functions serving requests) stays on the
kernel's side. (12^3 is a three-dimensional grid: six neighbours.
`DECOMPOSITION.md` section 6 has four ports. Noted, not ruled.)
Asked whether v4 = v3 on bare metal comes first, or nodes talking to
nodes: **"The next step is talking nodes sharing the common SSD, and [they]
may or may not have block storage available."** Step 4 below waits.
Rulings on that step so far:
- **The ports are the transport; the message is what is transported.**
Node to node, a write blocks until the neighbour reads
(`DECOMPOSITION.md` section 6). What travels is v3's Hermes message with
what it carries — type, from, to, channel, heat and TTL, ACL tag, a
payload of FORTH text up to a block — so v3's messaging rules are not
dropped; they go with the message.
- **Storage: some nodes have storage of their own** (a thumbdrive, as a
user's identity has today), in addition to or instead of the common SSD.
The common SSD is the system-resident store, Artemis's disk
(`FABRIC-2.md` F.16).
- **The first set of nodes: "2x2 + 1 central".** Five: four in a 2x2,
and one in the middle. Read back to Captain Bob, and not corrected, as:
each outer node wired to its two grid neighbours and to the centre; the
centre wired to all four; the centre is Hera.
- **A node has six ports, not four** — "A, as long as it can scale at
runtime adaptively." With four, the centre's are all taken by the outer
nodes and nothing is left for the common SSD or the console. Six is also
what a 12^3 grid needs. `DECOMPOSITION.md` section 6 is to be corrected.
- **It must scale at run time, adaptively.** The number of nodes and their
wiring are not fixed when the system is built.
- **The geometry is not fixed: "Not constrained by a 3D world. Other
geometries might be better."** Said when asked what a centre's two
remaining ports of six were for. So six, which came from a 12^3 grid's
six neighbours, is not a given either; nor is any one shape.
- **The geometry is data, not design (confirmed, "yes").** A node has a
number of ports, and that number is a parameter, as cell width and node
memory are. Which port connects to what is a table that can change while
the system runs. A geometry is a rule for filling in that table; the
units-and-centres rule below is the first, not the only one. Devices —
the common SSD, the console, a node's own drive — are things on the
other end of a port, in the same table. Nothing in the engine knows
which geometry is in use. A node is told, or finds out, which port leads
toward a destination.
- **What Hera's process management decides (ruled, "A"): which nodes
exist and are awake, not whose turn it is.** Every node that is not
blocked runs; on one processor that is each in rotation, an instruction
word at a time, which is what "all at once" is there and is not a
policy. From each node's heat Hera decides which are born, put to sleep,
woken and killed (`JUSTIFICATION.md` sections 6 and 7). Cooperative is a
node blocking itself at a port; preemptive is Hera putting a node to
sleep or killing it between any two instruction words. Nothing in this
depends on there being one processor. With it: an idle node does not
spin; it is blocked reading its ports and wakes when a neighbour writes,
and the console handing it a line is a message arriving on a port like
any other.
- **A node is born empty (ruled, "A").** It has nothing but the ability
to listen: it is blocked reading its ports, and the first thing a
neighbour sends it is a capsule of F18 code, which it takes in and runs,
as an F18 node executes what arrives at its port. StarForth's nucleus is
then itself a capsule — the one that makes an empty node able to digest
FORTH-source capsules — and is named, hashed, signed and recorded in
parity like any other, where today it is linked into the binary. The one
thing every node has, whatever it becomes, is the fixed behaviour at
reset by which it takes in its first capsule.
- **Where it is built and proven (ruled, "A"): in the shared engine,
hosted first, then bare metal.** Ports, wiring, nodes born empty and
messages are the engine's and belong to both products. Hosted, on three
ISAs, with a disk image as the common SSD; then the same nodes on bare
metal with the real console, SSD and thumbdrives.
- **How it grows: "Only the central node can connect to only another
central node."** So the five are a unit: four outer nodes and their
centre. An outer node is wired only inside its own unit. Units are joined
centre to centre, and the system grows by units.
The design of this step is being worked out by question and answer and is
not written yet. Nothing of it is built.
## 4. What was built on the detour
| Built 2026-10-05 | What becomes of it |
|---|---|
| `kernel_main.c` calling `sk_v4_run()` before the fleet tables | Goes at step 4. Until then it is how the bare-metal build is kept booting while the interface is built. |
| `v4/system/boot.c`: its own capsule loader and `PARITY:V4_*` lines | Stays for the hosted product. On bare metal v3's birth protocol loads the capsules and prints v3's parity lines (step 4). |
| The node's prompt loop, and the code that strips ` ok` from its output | Goes at step 1. |
| `capsules/v4/forth79.4th`, `post79.4th`, `mkpost.py`, the rules | Stay. POST is the gate for every word moved out of the nucleus. |
| `(CATCH)`, `(EMIT-HOOK)`, `NODE-ERROR` by name | `NODE-ERROR` stays. `(CATCH)` and `(EMIT-HOOK)` went with the POST capsule, `MESH.md` step 6b, 2026-10-07: POST is the kernel's and needs neither. |
| The per-call-target count (`v4/src/heat.c`, `call[]`) | Goes at step 3, when the node does at `call` what v3 does. |
| The hosted Linux product (`v4/tools/hosted.c`, `v4/system/boot.c`) | Stays, as its own product (3c). |
## 5. Steps
Each ends with all six builds booting and agreeing, and is committed with
its logs.
1. **Interpret this text (3.1).** The node's line entry; no prompt loop.
The hosts print the prompt and ` ok`, as v3's REPL does.
**Done 2026-10-05.** `(LINE)`, `(IDLE)`, `(DONE)` and `(LINE-STATUS)` in
`v4/capsule/quit.v4`; `v4_line_*` in `v4/src/image.c`; `v4_boot_line` in
`v4/system/boot.c`. All v4 tests pass at both widths; the six builds
agree; lines typed at the three bare-metal prompts are answered.
`logs/20261005-180922`, `-181152`, `-181541`.
2. **Asking the kernel, and the stacks from the kernel's side (3.3,
3.4).** Settle how a request is carried. Kernel words callable from a
node.
**The carrier is done, 2026-10-05; the rest is not.** Ruled: a node asks
by a blocking write to a port. Built: the port (`v4/src/node.c`,
`v4_node_port_attach`, `v4_node_port_served`); a blocked node goes on
from the opcode after the store, in the same instruction word
(`v4/src/exec.c`); `KERNEL-WORD` (`v4/capsule/compile.v4`), which makes a
word whose body writes its request number to the port; the boot makes
the kernel's words by handing the node text, and serves requests
(`v4/system/boot.c`). A request no one serves is error 12 on the node.
The one kernel word so far is `BYE`, on both products: hosted it leaves
the program, as hosted v3; on the lone node it is v3's cold restart.
`v4/tests/test_port.c`; six builds agree; `BYE` and an unserved request
typed at each bare-metal prompt. `logs/20261005-185506`, `-185734`,
`-190101`.
**The node tells its kernel of its words, 2026-10-05** (3b). An entry
is made in one place in the nucleus and entries go in two; each now
tells through a variable holding the xt of a word to run:
`(WORD-DEFINED)` ( xt -- ) from `(HEADER)`, `(WORD-FORGOTTEN)` ( w -- )
from `FORGET` and `COLD` (`v4/capsule/dict.v4`, `system.v4`). With 0
there, as on the hosted product, no one is told. `test_host_quit.c` is a
kernel that keeps the list and checks it is exactly the node's
dictionary after definitions, a vocabulary, an abandoned definition,
`FORGET`, a refused `FORGET` and `COLD`.
**Found and fixed with it:** since the capsules moved from build time to
boot time, what `COLD` returns to and `FORGET` protects was still the
nucleus alone, so `COLD` lost `U*`, `U/MOD` and `BYE`. The boot now
seals the system when it has loaded it (`v4_image_seal`); `hosted-check`
checks it. `logs/20261005-193045`, `-193307`, `-193636`.
**Not done:** v3's own C functions serving a node. They take a `VM *`
and use `vm_push`, `vm_pop` and, in places, the stack's fields directly;
a node has to stand behind that `VM` record first. That, the records
themselves, and the kernel's interpreter file are step 4.
3. **A word is being executed (3.5).** The hook at `call`; the ACL's
countdown and recheck; the word's count; `stadium_word_dispatch`.
In-line words become calls. The per-call-target count goes.
4. **The node as the kernel's interpreter.** The third interpreter file
(3c), for the kernel build. Under `STARFORTH_V4` the kernel's boot
brings the node up where it brings a v3 VM up: `vm_interpret` hands the
node the line; `register_word` makes a kernel word and its record; the
node's own words get records. `sk_v4_run()` goes. v3's POST runs
through it and its failures are the list of what is not yet there.
5. (Folded into 4: the hosted product is no longer part of this path.)
6. **Hera as v3 has her.** `init.4th` runs on the node; v3's whole POST
passes; v3's parity lines.
7. **The fleet.** Hestia and Artemis; message delivery; who runs next.
With Artemis: she is the owner of the disk, who says it may be
formatted, so that the disk is written as well as read (`MESH.md` step
6); and a drive that arrives is registered by her, at the chain's tail.
8. **Identity and the prompt.** Zuse; `[zuse@Hera] ok>`.
With identity: drives that come and go, as v3 has them — the
home-blocks signature, `WIREBIND`, the user's VM born from the drive,
`EJECT`, and a surprise pull (`MESH.md` 8.7, which says why the storage
step that was to build them another way was withdrawn).
Moving words from the assembled nucleus to `forth79.4th` goes on beside
these, a group at a time, with POST after each.
## 6. Open, and where each must be settled
| Open | Before |
|---|---|
| How the node tells a dictionary entry from a bare address at `call` | Step 3 |
| `EXECUTE`, which enters a word by a return | Step 3 |
| `>R R> R@ I J LEAVE`, which cannot be called | Step 3 |
| Who decides which node runs next, and when: Hera, by compudynamics; preemptive and cooperative | 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 |
+31
View File
@@ -224,3 +224,34 @@ LithosAnanke filing is a question for counsel.
- K≡1.0 holds on the golden model at both widths, on all three host ISAs.
- A hosted mesh runs the Tripod roles as nodes, with Hermes as the network.
- Verilator co-simulation of one node matches the golden model instruction for instruction.
## 15. Products
Ruled by Captain Bob, 2026-10-03. v4 turns one project into three products built on the same engine:
1. **Hosted StarForth F18.** The v4 engine as a native Linux build for amd64, arm64 and riscv64, on
real hardware.
2. **FPGA StarForth F18.** A 32-bit build of the engine loaded into the FPGA. This is the gateway into
building the whole system, and its foundation.
3. **StarshipOS.** The bare-metal product: LithosAnanke running the v4 F18 engine.
The FORTH-79 vocabulary, recomposed on the F18 engine (`DECOMPOSITION.md`), is stored as a capsule. The
StarshipOS-specific portions are recomposed and stored as capsules in the same way. The source tree
will be reorganised around this split.
## 16. Acceptance criteria
Ruled by Captain Bob, 2026-10-03. Until then v4 had none: this document and `v4/README.md` named a
POST suite and K≡1.0 on three host ISAs, neither of which exists yet, so nothing could be called
accepted.
- **v4 is equivalent to v3 at any point in time.** It must provide the same vocabularies and behave
the same way. The only difference is the machine underneath: the F18-derived engine instead of the
original StarForth VM.
- **Every ISA, hosted and bare metal, must still reach its `ok` prompt.** That is amd64, aarch64 and
riscv64, as a hosted build and as a bare-metal boot.
The existing acceptance tests keep their force and their rules: the three-architecture QEMU boot for
anything bare metal (`.claude/CLAUDE.md`), and the hosted three-architecture test
(`docs/lithosananke/hosted-acceptance-test/README.md`). Passing `make -C v4 test` is a development
check on the golden model, not acceptance.
+834
View File
@@ -0,0 +1,834 @@
# StarForth v4.0.0 — Talking nodes
Design, 2026-10-06. Ruled by Captain Bob by question and answer on
2026-10-05 and -06; each ruling is recorded with his words in `ENGINE.md`
section 3d. This file is the design that follows from them. Where it goes
beyond a ruling it says so, and those parts are proposals.
## 1. What this step is
> The entire point is that ultimately we have F18 engines digesting
> capsules alone, and in a sense can be anything written in F18 assembler
> for our fabric — 144 someday as a 12x12 grid, but I want a 12^3 FPGA
> ultimately, where using a capsule digester like StarForth, it's more
> than an operating system.
> The next step is talking nodes sharing the common SSD, and [they] may or
> may not have block storage available.
More than one node, each born empty and made into something by the capsule
it takes in, talking to each other through ports, sharing a disk. It comes
before making v4 equal v3 on bare metal (`ENGINE.md` step 4), which waits.
## 2. The rulings
1. **The ports are the transport; the message is what is transported.**
Node to node, a write blocks until the neighbour reads and a read blocks
until the neighbour writes (`DECOMPOSITION.md` section 6). What travels
is v3's Hermes message with what it carries, so v3's messaging rules are
not dropped: they go with the message.
2. ~~Some nodes have storage of their own, in addition to or instead of
the common SSD. Some have none.~~ **Withdrawn 2026-10-07:** every node
has blocks, and asks the kernel for them (section 8).
3. **The first set of nodes is "2x2 + 1 central"**: five.
4. **The geometry is data, not design.** A node has a number of ports, and
that number is a parameter. Which port connects to what is a table that
can change while the system runs. "Not constrained by a 3D world. Other
geometries might be better."
5. **The first geometry: "Only the central node can connect to only another
central node."** Four outer nodes and their centre are a unit. An outer
node is wired only inside its unit. Units are joined centre to centre.
6. **It must scale at run time, adaptively.**
7. **Hera decides which nodes exist and are awake, not whose turn it is.**
Every node that is not blocked runs. Cooperative is a node blocking
itself at a port; preemptive is Hera putting a node to sleep or killing
it between any two instruction words. An idle node does not spin: it is
blocked reading its ports.
8. **A node is born empty.** It has nothing but the ability to listen. The
first thing a neighbour sends it is a capsule of F18 code. StarForth's
nucleus is such a capsule.
9. **Built in the shared engine, proven hosted on three ISAs first, then on
bare metal.**
## 3. Acceptance (approved 2026-10-06)
1. **Five nodes come up from nothing.** Hera is born empty, takes in the
nucleus capsule and the FORTH-79 capsule, and passes POST. She births
the four outer nodes while running; each is born empty and takes in the
same capsules through its port from Hera. Each prints a parity line;
the four outer nodes' dictionary hashes are identical. (Changed
2026-10-07: an outer node is not POSTed. POST is the kernel's, once, as
in v3.)
2. **They talk.** A line typed at the console reaches Hera as a message. A
message from Hera runs on an outer node and its output comes back. A
message between two outer nodes that are not wired to each other is
forwarded by a node in between.
3. **They share the SSD.** Every node asks the kernel for its blocks. A
block written by one node is read by another. (Changed 2026-10-07: no
node has a drive of its own and none is without storage.)
4. **It scales while running.** A second unit of five is born and joined
centre to centre. A message crosses from one unit to the other. The
second unit is removed and the first carries on.
5. **Hera manages.** An idle node executes nothing while it waits, which
the anti-clock shows. Hera puts a node to sleep and wakes it. Hera kills
a node that is stuck in an endless loop, and everything else keeps
running.
6. **On every build.** The three hosted ISAs, then the three bare-metal
ISAs under QEMU with the real console and disk.
**Left for the step after, by agreement:**
- Hera sleeping, waking and killing nodes by herself from each node's
heat. Here she does it by command. The rule for it has not been given.
- v3's messaging rules checked at every hop. From this step a message
carries its heat, TTL and ACL tag; checking them is the router's, and
the checks await rulings.
## 4. The engine: ports
Everything in this section is the engine's (`v4/src`) and knows nothing of
StarForth or of any kernel.
### 4.1 A node's ports
A node has `V4_PORTS` ports. The number is a build parameter, as
`V4_CELL_BITS` and `V4_NODE_WORDS` are. (Proposal: 8 for now, which is what
the first geometry needs of a centre — four outer nodes, two devices, two
other centres — with nothing depending on the number.)
Ports are addresses, as `DECOMPOSITION.md` section 6 has them. Attached at
word address `base`:
| Address | What |
|---|---|
| `base` … `base + V4_PORTS − 1` | Port 0 … port `V4_PORTS − 1` |
| `base + V4_PORTS` | Any port: a read here takes from whichever port has a neighbour writing |
| `base + V4_PORTS + 1` | Which port the last read from "any" came from. Read only. |
- **A write to a port blocks the node until the neighbour has read it.**
Built 2026-10-05 for one port; the opcode after the write runs when the
write has been taken.
- **A read from a port blocks the node until the neighbour writes.** The
fetch does not happen until there is something to fetch. This is new.
- **A read is any fetch**: `@`, `@b`, `@+`, the literal fetch `@p`, and the
fetch of an instruction word when `P` is a port address. When `P` is a
port, it is not advanced: the node goes on executing what arrives there.
That is how an F18 node runs code from a port, and it is what makes
ruling 8 need no code in a newborn node.
- A port with nothing on the other end blocks for ever, as on the fabric.
**Fixed 2026-10-07 ("Fix the error. no bad code is ever released"):**
that holds for a bare node. A node that can take an error is not left
there. When it is blocked writing to one port, or reading from one, that
nothing is wired to — because nothing ever was, or because what was
there has been killed or the wire cut — error 18, "No one on that
port", is raised on it: what it was doing ends with the message and it
goes on (`v4/src/node.c`, `v4_node_port_gone`; `v4/src/fabric.c`,
`v4_fabric_gone_error`; the lone node, `v4/system/boot.c`). A node whose
neighbour is asleep waits, and so does one reading "any port".
What it mended: a node writing to a node that was stuck, and was then
killed, stayed blocked for ever, so that killing a stuck node could cost
its neighbours (found while step 7 was being designed); and on the two
products a write to an empty port, `5 7 PORT!`, ended the program.
`v4/tests/test_host_unit.c`: Hera kills node 14 while node 12 is blocked
writing to it; 12's line ends "No one on that port", every other node
answers, and a later send to 14 is that error at once.
`v4/tests/test_fabric.c`: a bare node still waits. `hosted-check` and the
three boots type `5 7 PORT!` and go on: `logs/20261007-121743` (amd64),
`-122008` (aarch64), `-122337` (riscv64); POST 538 of 538,
`dict_hash=0x5f0a949a6fc8ef2b` on all six.
**Not mended, and reported:** a node that waits at "any port" for an
answer that never comes (`AWAIT`, on a node that never answers) waits
for ever, and on the lone node that ends the line as "stopped".
### 4.2 A node at reset
`P` is the "any port" address, both stacks are empty, memory is zero. The
node is blocked reading its ports. Nothing else is in it.
### 4.3 The fabric
`v4/src/fabric.c`: the nodes there are, how their ports are wired, and
time passing for all of them at once.
- **The nodes.** A set that grows and shrinks while the system runs
(ruling 6). A node is added empty (4.2) and removed whole.
- **The wiring.** For each port of each node: nothing, or a port of
another node, or a device. It is a table, changed at run time
(ruling 4). The fabric does not know what shape it makes.
- **A device** is what is on the other end of a port that is not a node:
two functions, one that takes a word the node writes and one that gives
a word when the node reads, each able to say "not yet". The console, a
disk, and the kernel that serves a node's requests (`ENGINE.md` 3.3) are
devices.
- **A step.** Every node that is awake and not blocked executes one
instruction word. Then every write that has a reader waiting on the
other end of its wire is handed over, and both nodes are unblocked. That
is all: there is no choice of whose turn it is (ruling 7).
- **Asleep.** A node that is asleep executes nothing and nothing is handed
to it or taken from it. It is put to sleep and woken from outside, at
any instruction word.
### 4.4 What is not in the engine
Messages, routing, capsules, the unit of five, Hera. Those are sections 5
to 8 and are made of capsule code and of the host that owns the devices.
## 5. A capsule of F18 code, and how an empty node takes it in
**As built.** A capsule of F18 code is not a format a node has to
understand. It is the words a neighbour writes to the node's port, in the
order they are written: for each stretch of memory that is not zero the
two instruction words below with their address and count and then the
words themselves, and at the end a jump to where to start. The file is
those words, each in a cell's bytes, low byte first, and its hash is the
hash of exactly what is sent. The nucleus is one, in the capsule directory
with a name, a hash and a signature like any other. `mkcapsule` was not
changed: the nucleus capsule is a built file kept under `capsules/v4/`.
A neighbour puts it into an empty node by writing to the port between
them, and the node executes what arrives (4.1). For each run it sends
```
@p a! @p push \ then the address, then count − 1: executed from the port
@p !+ unext \ then the words: each is fetched from the port and stored
```
and at the end `jump` to the start address. That is the F18's own way,
and it needs nothing in the node beforehand.
## 6. A message
**Proposal**, from `DECOMPOSITION.md` section 6.1 and v3's `SkHermesMessage`
(`kernel/include/starkernel/vm/kernel_hermes.h`), which it must be able to
carry whole:
| Word | Contents |
|---|---|
| 0 | To: the node it is for |
| 1 | From: the node that sent it |
| 2 | Type, and the channel |
| 3 | Heat and TTL |
| 4 | ACL tag: the sender's identity fingerprint |
| 5 | Sequence |
| 6 | Payload length in characters, 0 to 1024 |
| 7 … | Payload: FORTH text, four characters to a word |
It is written to a port a word at a time and read a word at a time. Words
3 and 4 are carried from this step on and are not yet checked
(section 3).
A node that is a StarForth digester, when it has nothing to do, reads a
message from "any port". If it is for this node, the payload is
interpreted, as a line is today (`ENGINE.md` 3.1). If it is for another,
it is written to the port that leads there (section 7).
What a node prints goes to its console, and its console is a place like
any other: for the node wired to the console device, that port; for any
other node, a message to the node that is. So what an outer node prints
comes back through its centre.
## 7. Finding the way
**Proposal.** Each node has a small table: for a destination, the port
that leads toward it; and one port for everything not in the table.
Whoever wires a node writes its table, and changes it when the wiring
changes. Under the first geometry an outer node's table is its two grid
neighbours and "everything else to my centre"; a centre's is its four
outer nodes, and for each other unit the port toward that unit's centre.
A different geometry is a different way of filling in the wiring table
and these tables. Nothing else changes.
## 7a. Two neighbours writing to each other (ruled 2026-10-06)
The fault is in section 10, step 4. **Ruled: a node writes only to a
neighbour that is already reading; a node keeps the messages it takes in
meanwhile, and one there is no room for is lost and counted.**
**As built.**
- *The engine.* Two more addresses follow a node's ports, as the F18's
`io` register would give them: which ports have a neighbour waiting to
write to this node, and which have one waiting to read from it, a bit to
a port. Fetching them waits for nothing and changes nothing
(`v4/src/node.c`; the fabric keeps them, `v4/src/fabric.c`). A device
that takes what is written to it shows as waiting to read.
- *Looking before writing* (`(GATE)`, `v4/capsule/core.v4`). Before a node
begins any message it takes in every message a neighbour is waiting to
write to it; then it writes if the neighbour it means to write to is
waiting to read; and if not, it looks again. Once a message is begun it
is written to its end: the neighbour that took its first word reads the
rest.
- *The messages waiting* (`(MQ)`). What a node takes in is kept in a ring
of 400 cells of its own memory -- for each message its seven words, the
port it came on, and its text -- and dealt with, oldest first, when the
node has nothing else to do. A node with none waiting is blocked reading
its ports, as before. One there is no room for is read to its end, let
go, and counted in `(LOST)`.
**What had to be added to the ruling, and why.** As put to Captain Bob the
rule was "write only to a neighbour that is reading, and look again if it
is not". That is not enough: two neighbours each with a message for the
other would each look, see the other not reading, and look again, for
ever. No rule that treats both ends of a wire alike can get out of that.
So one end of every wire may write without waiting for a reader, and one
only: **the node with the lower number.** A node that waits to write is
then always waiting on a higher number, so no ring of nodes can all be
waiting on each other, and the highest of any that wait is not waiting to
write: it is looking, and takes in what is being written to it.
For this a node must know the number of the node on each of its ports.
Whoever wires it tells it, as it is told the ways: `NEIGHBOUR ( node port
-- )`, `v4/capsule/quit.v4`. A port it has not been told of -- a device's
-- is written to only when what is there is waiting to read. **Two nodes
wired together and not told of each other can still stop each other**,
each looking for ever; they execute, but nothing passes. Telling them is
part of wiring them. Put to Captain Bob after it was built, and ruled:
"1 is fine."
**What it costs.** A node that is flooded loses messages, of every kind:
text for it, answers to text it sent, and messages it was only passing
on. In the test three nodes send each other 800 messages at once; 287
arrive and 714 messages are let go (the count includes answers and text
that was to start a node sending). Six from each to each, at once, all
arrive. Nothing here makes a sender slow down or send again; that is
kernel-Hermes's work in v3 and is not decided for the mesh.
## 8. Storage
**Ruled 2026-10-06 and 2026-10-07** (Captain Bob). On 2026-10-07 he found
this section to have left the OS as designed, and brought it back: every
node asks the kernel directly, as every v3 VM does. What that withdrew is
in 8.6, so that it is not proposed again.
### 8.1 The rulings that stand
1. **A node only ever asks for a block by number, and it asks the kernel.**
`V3-PARITY.md` 1d and `ENGINE.md` 3.3: a kernel request, written to the
port where the node's kernel is, served between the node's opcodes. No
node asks another node for a block, and none passes such a request on.
2. **Every node has blocks, always.** There is no node without storage.
3. **Two numbers.** A logical block number is what `BLOCK n` and capsules
use. A physical block number is a place on a real device. The kernel's
mapper stands between them.
4. **The view is static; reality shifts to keep it so.** A logical number
always means the same block. Devices chain one after another in physical
space, and that chain changes as devices come and go; the mapper moves
data and changes its map so that the logical view does not change. All
the user sees change is how much storage there is.
5. **One metadata format for every block device** — a cloud store, a swap
file, an SSD, a USB drive, a thumbdrive, and any other within reason.
It is v3's (`v3/include/block_subsystem.h`): the `STFR` version 2 header,
the allocation map, the relocation table, and a card for every block
(`blk_meta_t`). A disk v3 formatted reads in v4.
6. ~~Plus an identity: a device's and its chain's, in the header's spare
space.~~ **Withdrawn 2026-10-07**, 8.7.
7. **The mapper is the kernel's**: the device chain, the metadata,
first-touch claim, ACL, migration and the Stadium touch stay in the
kernel's block subsystem, which is v3's C code.
8. ~~A device leaves by being asked for, its blocks moved onto the
others; one pulled without asking leaves holes; a returning device has
its old numbers.~~ **Withdrawn 2026-10-07**, 8.7.
### 8.2 What a node does *(step 6)*
**Ruled 2026-10-07: v3's way in full.** The block words are the kernel's.
`BLOCK`, `BUFFER`, `UPDATE`, `SAVE-BUFFERS` and `EMPTY-BUFFERS` in
`v4/capsule/blocks.v4` each write one request to port 0, where the node's
kernel is, and do nothing else: `BLOCK` and `BUFFER` with the block's
number on the stack, the others with nothing. The node gives the kernel no
address and keeps no record of what is in its buffers.
The node has a **window** in its memory, four slots of 256 cells, as a v3
VM has four slots of 1 KiB at the top of its memory. The kernel copies a
block into a slot and gives the node the slot's address.
The five requests' numbers are the same for every node and are below zero,
-1 to -5; the requests a host names for its own words (`KERNEL-WORD`)
count up from 1. The status a request leaves: 0 it worked, 2 it was
refused (error 17, "Storage refused"), 3 there is no such block (error 13).
The four storage registers and `v4_node_storage_attach` go from the engine.
### 8.3 What the kernel does *(step 6)*
Every node has its kernel on port 0, whatever else is there for it: Hera's
requests for nodes (section 9) are honoured only from Hera, and the block
requests from every node.
`v4/system/blocks.c`, which the hosted and the bare-metal builds and the
tests share, serves them, from v3's block subsystem
(`v3/src/block_subsystem.c`). For each node it keeps what v3 keeps for each
VM (`v3/src/word_source/block_words.c`): which block is in which slot of
the window, which have been UPDATEd, and the chain as it was when they
were filled. The engine (`v4/src`) knows nothing of it.
- It writes into the node's window and nowhere else in the node. That is
how a request cannot overwrite the node's code (ruled 2026-10-07: the
kernel refuses it): there is no address for a node to give.
- `BLOCK` gives the slot a block is in, and reads it from storage only if
it is in none. `BUFFER` gives a slot of zeros without reading.
- **When a block is written is FORTH-79's:** `UPDATE` marks it, and it is
written by `SAVE-BUFFERS` or when its slot is wanted. v3's `UPDATE`
copies the slot to the kernel at once; the standard is followed
(standing ruling). A slot whose block is not marked is taken before one
whose block is.
- A block storage will not take is let go of, and its slot is empty.
- When the chain of devices changes, everything in the slots is let go and
none of it is written, as v3 does.
- Four characters to a cell, the first lowest; a read leaves the rest of
each cell zero and a write takes the low 32 bits.
- **Each node has its own copy of a block it holds, as each v3 VM has.**
If node A has block *n* in a slot and node B writes it, A goes on reading
what it had, and A's next `UPDATE` and `SAVE-BUFFERS` writes all of the
block over B's. v3 is the same. Closing that would be a departure from
v3 and is not ruled.
- v3's code does what it does: header, allocation map, block cards,
relocation, three blocks and their cards to a 4 KiB device block.
- There is one chain, as in v3: fast RAM at blocks 0 to 2047, then the
devices. Every node sees the same blocks at the same numbers.
- The devices are v3's own back ends: RAM and a file when hosted, the
virtio disk on bare metal. A hosted program started with no disk has the
fast RAM alone, as hosted v3 has.
`blk_subsys_init` took a v3 `VM`, stored it and never used it; the hosted
v4 system has none to give. **Ruled 2026-10-06:** the argument is removed.
**On bare metal** the node's blocks are the kernel's real chain — RAM, the
ramdrive and the virtio disk — in place of the RAM array
`kernel/src/v4/sk_v4.c` gives it today. (Approved.) Found 2026-10-06:
`kernel_main.c` starts the v4 node (line 523) before it sets that chain up
(lines 620 to 668), and the v4 node never returned, so under `STARFORTH_V4`
the chain did not exist. As built: the node boots and is POSTed against
POST's own block RAM, blocks 1 to 2047 and nothing above them, and the
chain is set up after POST and before the prompt. That is v3's order
(`sk_vm_bootstrap.c` gives POST `sk_post_blk_ram`; `kernel_main.c` sets the
chain up later), and POST's cases depend on it: several expect a high
block number not to exist, and on the real chain it does.
### 8.4 Not in step 6
- **Who may have which block is not checked.** First-touch claim, the block
card's ACL and the Stadium touch need the node's identity
(`V3-PARITY.md` 1d). Blocks are read and written through v3's subsystem
with nobody's claim on them. Not as intended yet.
- **Drives that come and go.** Step 6a, which was to build them, is
withdrawn (8.7). They are built as v3 has them, when v4 has identity
and the fleet (`ENGINE.md` steps 7 and 8).
- **Cloud stores and real USB drives** wait for their drivers.
### 8.5 Ruled 2026-10-07, after the review of step 6
The review found that a block request could be made by hand with an
address in the node's own code, and said that a node's own copy of a
block was a departure from v3. The second was wrong: a v3 VM has a window
of four slots in its own memory and `BLOCK` copies the kernel's block into
one (`v3/include/vm.h`, `BLK_VM_SLOTS`; `block_words.c`, `blk_vm_load`).
It was reported to Captain Bob as a departure before it was checked
against v3, and he ruled on it as one; it was then checked and taken back.
His rulings, on what v3 does: **the kernel refuses a request that would
overwrite the node's code**, and **v3's way in full** — the block words
are the kernel's, by number only, with the kernel keeping the record of
the window and doing the copying. Both are built: 8.2 and 8.3.
### 8.7 Step 6a withdrawn 2026-10-07: drives that come and go
Rulings 6 and 8 of 8.1 were given on 2026-10-06 in answer to questions put
without first saying how v3 does it. On 2026-10-07, shown v3's design,
Captain Bob withdrew step 6a as written. None of the four things it was to
build is v3's:
| Ruled 2026-10-06 | v3 |
|---|---|
| A device identity and a chain identity in the block header | Identity is the signature and the keypair on the drive; the header has none |
| A returning device has its old block numbers | It joins at the tail again; its numbers depend on what else is attached |
| Release by asking: the mapper moves the device's blocks onto the others | `EJECT` flushes to the drive itself and kills the user's VM; nothing is moved off it |
| A surprise pull leaves holes that answer "no storage" | Only the tail can go; the user's VM is killed; there are no holes |
**v3, as built** (`kernel/src/repl.c`, `v3/src/block_subsystem.c`,
`v3/src/word_source/block_words.c`; `FABRIC-2.md` Phase 8, D.3 and F.10):
- A removable drive is a person's. It carries an identity, the home-blocks
signature and a keypair Zuse mints onto it. Plugged in, its signature is
checked and the user's VM is born from the drive (`WIREBIND`); the
user's blocks are on their own drive.
- The kernel polls USB; Hera asks Artemis to register the drive
(`HERA-BLK-ATTACH-REQ`), and Artemis adds it to the chain.
- A drive joins at the tail, and only the tail can leave
(`blk_subsys_detach_device`): taking one out of the middle would
renumber every device after it.
- Leaving by asking is `EJECT`, a word of Hera's alone: the user's blocks
are flushed to their drive and the user's VM is killed.
- A surprise pull kills the user's VM with no flush, and the kernel drops
the device and whatever was not written.
- Coming back, the drive is known by its signature and the user's VM is
born again; the data is there because it never left the drive.
- Every VM's block window is emptied when the chain changes. (v4 has this
already: `v4/system/blocks.c`.)
v4 has none of what that rests on yet: Zuse and identities, `WIREBIND`,
user VMs born from a drive, Artemis, USB in the v4 boot. They are
`ENGINE.md` steps 7 and 8, and drives that come and go are built there,
as v3 has them.
### 8.6 Withdrawn 2026-10-07
Each of these was ruled on 2026-10-06 or stood in this document, and each
left v3's design. Captain Bob: "something is really off. it sounds like a
divergence from the os as designed"; and of the three below, "all three,
every node asks the kernel directly".
- **A disk as a device on a port that speaks messages**, and a block
request passed from node to node until it reaches the node wired to the
disk. Built and tested as far as one node (commit `4a505a15`), then
withdrawn. In v3 a VM calls the kernel.
- **Nodes with no storage** (ruling 2 of section 2, and acceptance 3 as it
was). In v3 every VM has blocks.
- **A drive of a node's own**, numbered from the top of the number space
downward. In v3 there is one chain.
- **POST on every node at its birth** (acceptance 1 as it was). In v3 the
kernel runs POST once, at boot, with block RAM it supplies, and a born VM
prints its parity. See section 9.
## 9. Birth, and Hera
**Proposal, built as step 5 (section 10).** Hera asks, through the port
where her requests go (`ENGINE.md` 3.3), for a node to be added and wired;
she then sends the newborn its capsules through the port that joins them.
Putting a node to sleep, waking it, and removing it are requests of the
same kind. Only Hera's requests are honoured.
The unit rule (ruling 5) is Hera's, in capsule code: it is what she does
with those requests. The engine and the host do not know it.
**Ruled 2026-10-07:** a node Hera births is not POSTed. POST is the
kernel's, run once at boot on Hera, as v3's kernel runs it once on its
first VM; a born node prints its parity. And the kernel is to hold POST's
cases and feed them to Hera itself, with no POST words loaded into her
dictionary — today they are a capsule she loads (`NUCLEUS.md` section 6).
## 10. Steps
Each is tested, committed and pushed before the next.
1. **Ports and the fabric (section 4).** Reads; "any port"; a node at
reset; execution from a port; nodes added and removed; wiring changed;
asleep and awake. Tests at the level of opcodes: two nodes exchange
words; an empty node is filled through its port and runs what it was
sent; a word is passed on by a node in between; a node waiting executes
nothing; a node is put to sleep, woken, and removed while looping.
**Done 2026-10-06.** `v4/src/node.c` (the ports, `v4_node_born`),
`v4/src/exec.c` (a fetch from a port waits; execution from a port; the
slot a blocked node goes on from), `v4/src/fabric.c`. `V4_PORTS` is 8.
`v4/tests/test_fabric.c`, 53 checks at both cell widths and under ASan
and UBSan: all of the above, with the empty node filled once by a device
and once by another node holding the capsule in its own memory; the
fabric given more room while nodes run; what cannot be wired refused.
The single-node products are unchanged by it: `hosted-check` on three
ISAs, and the three bare-metal boots with lines typed at each prompt
(`logs/20261006-074907`, `-075150`, `-075532`).
2. **The nucleus as a capsule, and an empty node made a StarForth node
through its port (section 5).**
**Done 2026-10-06.** `v4/src/capsule.c`, `v4_capsule_write`: any
node's memory as the words to send an empty node. `mkimage` writes the
nucleus so, to `capsules/v4/nucleus-64.f18`, a built file kept in the
tree as `BLOCK_MAP.md` is; `mkcapsule` is unchanged and bakes, hashes
and signs it with the rest. The boot's node is born empty
(`v4_image_born`) and is given the nucleus a word at a time as it reads
its port, after the capsule's hash and signature are checked
(`v4/system/boot.c`). Nothing of the nucleus is linked into either
product any more. `test_fabric.c`: a memory with a programme and words
here and there arrives word for word and runs. Both products boot so:
`hosted-check` on three ISAs; `logs/20261006-102421`, `-102706`,
`-103048`.
**What a newborn node still has from its host:** the registers the
nucleus expects — the console's three, the two stack registers, the
error register, the fault table's address, the storage registers — are
attached by the host when the node is born, from the description
`mkimage` writes. They are the node's hardware as the golden model has
it, at addresses the memory map (D-4, still open) will fix. The console
and storage ones go when those become devices on ports (steps 3 and
6).
3. **Messages (section 6):** a StarForth node that reads messages when
idle; the console as a device; a line typed is a message.
**Done 2026-10-06, but for the keyboard.** A node with nothing to do is
blocked reading "any port" (`(IDLE)`, `v4/capsule/quit.v4`). What
arrives is a message; text for this node is interpreted; what it prints
is kept and sent back as messages to the sender, on the port the
message came on, and then a message saying how the text ended (`EMIT`,
`(FLUSH-OUT)`, `(HDR)` in `core.v4`; `(FINISH)` in `quit.v4`).
`v4/src/message.c` is the same format for whatever is on the other end
of a port and is not a node. The boot is that, on the node's port 1, as
the console (`v4_boot_line`); the prompt tests are too. Handing a node a
line by writing its input buffer and setting its `P` is gone
(`v4_line_begin` and the rest). Nothing reads the `CONSOLE-TX` register
any more.
`EMIT` still needs one free cell of the data stack and no more, as
before; it keeps its working values on the return stack. The full-stack
tests hold at the same figures as before.
All v4 tests pass at both widths and under ASan and UBSan;
`hosted-check` on three ISAs; bare metal `logs/20261006-110551` (amd64),
`-111621` (aarch64), `-111341` (riscv64). `-110837` is an aarch64 run
that was ended by the test wrapper's limit while still in UEFI firmware,
before the kernel had started; it shows nothing about v4.
**Not done:** `KEY`, `EXPECT` and `QUERY` still read the console's two
input registers, not a message. A message not for this node, or not
text, is let go: passing it on is step 4.
4. **Finding the way (section 7).**
**Built 2026-10-06.** Each node has a table of up to 16 destinations and the port
toward each, and one port for everything else (`ROUTE ( node port -- )`,
`DEFAULT-ROUTE ( port -- )`, `NO-ROUTES`; `(PORT-FOR)` in
`v4/capsule/core.v4`). A message not for this node is written, whole,
to the port its destination's entry names (`(PASS-ON)`,
`v4/capsule/quit.v4`); with no entry and no port for everything else it
is dropped and counted (`(LOST)`). What text prints, and how it ended,
go back to the node the text came from by the same table, so an answer
crosses as many nodes as the text did. `SEND ( baddr u node -- )` sends
text to another node; `(ME)` is a node's own number; `(CONSOLE)`, when
set, is where a node's printing goes instead of to the sender.
`v4/tests/test_host_mesh.c`, 28 checks at both widths and under ASan
and UBSan: three StarForth nodes in a row behind a console; text for
the far one passes through the other two and its answer comes back;
each node keeps its own dictionary; output longer than one message; a
1024-character message passed on whole; a message with nowhere to go
counted; a table changed while running. The single-node products are
unchanged: `hosted-check` on three ISAs; bare metal
`logs/20261006-115225` (amd64), `-115501` (aarch64), `-115849`
(riscv64).
**The fault found here, and put right 2026-10-06 (section 7a).** Two
neighbours that wrote to each other at once waited for ever: a write
blocks until the neighbour reads, and a node that is blocked writing is
not reading. Found when the far node `SEND`s text to the middle one and
then sends word of how its own text ended, which goes by the middle
one, while the middle one answers the far one. Ruled: a node writes only
to a neighbour that is reading, keeps what it takes in meanwhile, and
loses and counts what it has no room for. Built so, with one thing
added that the ruling needs (section 7a): of two neighbours the lower
number may wait to write, and `NEIGHBOUR` tells a node who is on each
port. `test_host_mesh.c`, 44 checks: the case that stopped the nodes
now passes; every node sending every other six messages at once, all
arrive; 800 at once, the nodes come to rest and every message either
arrived or was counted. `test_fabric.c`: a node sees who is waiting to
write to it and who to read from it, and looking disturbs neither.
The prompt and `EMIT` take no more of the data stack than they did.
All v4 tests at both widths and under ASan and UBSan; `hosted-check` on
three ISAs; bare metal, with lines typed at each prompt,
`logs/20261006-134817` (amd64), `-135044` (aarch64), `-135440`
(riscv64).
**Found on the way: POST was writing into the nucleus's own code.** On
v4 `HERE` is a cell address and `C!`, `FILL` and `CMOVE` take byte
addresses (D-1), so cases such as `65 HERE C!` wrote their bytes into
the nucleus at cell `HERE/4`, read them back from there, and passed.
`65 HERE C!` was watched changing cell 2223 during POST; the other
cases of the same form were not watched, and the boots committed
before this were not gone back to. It showed only when this step moved
other code onto that cell and POST stopped. Ruled: the twelve cases
are left out, marked OPEN (`v4/tools/post79_rules.py`,
`docs/v4.0.0/POST79.md` section 4), and how `HERE` and the byte words
are to agree is its own step. POST is 538 cases, not 550. `C!` and
`FILL` now have fewer cases; nothing stops any other text doing what
those cases did.
5. **Birth and Hera's requests (section 9); the unit of five.**
**Done 2026-10-06, in the fabric under test; the products are still one
node each until steps 8 and 9.** Section 9's proposal, as built:
- *What Hera asks* (`v4/include/v4/manage.h`, `v4/src/manage.c`). Eleven
requests, each a word on her made with `KERNEL-WORD`, written to the
port her requests go to: `NODE-ME`, `NODE-BORN`, `NODE-WIRE`,
`NODE-UNWIRE`, `NODE-SLEEP`, `NODE-WAKE`, `NODE-KILL`, `NODE-PARITY`;
and `CAPSULE-OPEN`, `CAPSULE-CELL`, `CAPSULE-LINE`, by which she reads
a capsule the host has found and checked. A node is named to the host
by its place in the fabric; its number, which messages go by, is the
nodes' own. Only the node the host has wired to this is answered.
- *What a node needs to do it* (`v4/capsule/quit.v4`): `PORT! ( w port
-- )`, a cell written to a port as it is; `SEND-ON ( baddr u node port
-- )`, text by a port named, for a neighbour with no number yet;
`AWAIT ( node -- how )`, blocked until that node's word of how text
ended comes, keeping every other message for later; `(SEAL)`, what is
in the dictionary now is the system.
- *Hera's capsule* (`capsules/v4/hera.4th`, blocks 8000 to 8004), FORTH.
`BIRTH ( number port -- place )`: a node is asked for and wired to
that port; the nucleus is sent it cell by cell; it is told its number,
its console and that Hera is on its port 2; FORTH-79 and POST are sent
it a line at a time, each waited for; it seals; its parity is
recorded. `UNIT ( n -- )`: four births, on ports 2 to 5, and the four
joined in a square, each told of the two beside it. The unit rule is
there and nowhere else.
`v4/tests/test_host_unit.c`, 34 checks, 64-bit: Hera is born empty and
takes the nucleus through her port, then FORTH-79, POST (538 of 538) and
her capsule from the console; `10 UNIT`; four nodes are born, each
passes POST, the host records four parities with one dictionary hash;
all five wait and execute nothing; text for each outer node is done
there and what it prints comes back; `COLD` on one comes back to the
nucleus and FORTH-79; one outer node sends text to the one beside it,
and to the one across from it by way of Hera. Acceptance 1 and 2, in
the fabric. All v4 tests at both widths and under ASan and UBSan. The
single-node products are unchanged but for the nucleus's new words:
`hosted-check` on three ISAs; bare metal `logs/20261006-143934`
(amd64), `-144204` (aarch64), `-144601` (riscv64).
**What stands in, in the test only:** the capsules are read from the
files the build bakes in, and the nucleus capsule is written from the
nucleus as the test assembles it, not found in the baked directory with
its hash and signature checked; that is the host's part and comes with
step 8. Each node has a disk of its own attached at birth, as the boot's
one node has; storage through the ports is step 6.
**Not done, and open:**
- The test is not run on 32-bit cells: the POST capsule's expected
results are 64-bit v3's and the nucleus capsule is `nucleus-64`. POST
has never been run at 32 bits.
- A node that never answers leaves Hera waiting for ever in `AWAIT`;
she cannot then kill it. What she does about a birth that does not
finish is not decided.
- What an outer node prints while Hera is giving birth waits in Hera's
400 cells until she is idle, and is lost if there is more than they
hold. POST prints one line.
- An outer node has no port to the kernel, so no kernel words: no
`BYE`, and nothing it asks is honoured, as ruled.
- The suite now takes about four minutes, eight under the sanitizers:
five POSTs.
- **Changed at step 6 (2026-10-07):** `BIRTH` no longer sends POST, and
a born node is not POSTed. What is said of POST on the outer nodes
above is step 5 as it was built.
6. **Storage (section 8): every node asks the kernel for its blocks.**
**Done 2026-10-07.** It was first built another way and brought back;
8.6 says what was withdrawn and why.
- *The requests* (`v4/include/v4/blocks.h`, `v4/system/blocks.c`). Five,
-1 to -5: `BLOCK`, `BUFFER`, `UPDATE`, `SAVE-BUFFERS`,
`EMPTY-BUFFERS`, for any node, by block number only. The kernel keeps
the node's window of four slots. `v4/tests/test_host_blocks.c`: 41
checks at 64 bits, 39 at 32 — a block copied into a slot and found
there again, `UPDATE` and when a block is written, `EMPTY-BUFFERS`,
`BUFFER`, more blocks than slots, numbers that are no block, too
little on the stack, a window not in the node's memory, and the chain
changing under what the slots hold.
- *The node* (`v4/capsule/blocks.v4`). Each block word is one request
on port 0; the node's own buffers and its record of them are gone, and
so are the four storage registers and `v4_node_storage_attach`. Error
17 is "Storage refused". The window takes 512 cells more than the two
buffers did, and the dictionary space ends 512 cells lower, at 13824.
`v4/tests/test_host_quit.c`: its block cases, those that counted on
two buffers rewritten for four slots, and a disk that will not be
written says so and is let go of.
- *v3* (`v3/src/block_subsystem.c`). `blk_subsys_init` takes no `VM`.
Nothing else of it is changed. Accepted on the v3 configuration, three
ISAs, `PARITY:M7.1a` hash `0x08873e0f44b7cb2a` as on 2026-10-03:
`logs/20261007-082647`, `-082752`, `-082938`.
- *The unit of five* (`v4/tests/test_host_unit.c`, 47 checks). Every
node has its kernel on port 0: it serves a node's blocks, and Hera's
requests for nodes from Hera alone. One node writes a block and the
other four read it; two nodes write ten blocks each at once and all
twenty reach the chain. Hera's `BIRTH` no longer sends POST
(`capsules/v4/hera.4th`): one POST tally is seen, Hera's.
- *The products.* Hosted: the chain's fast RAM, blocks 1 to 2047, as
hosted v3 has with no disk; `hosted-check` passes on three ISAs.
Bare metal: POST against its own block RAM, then the chain — fast
RAM, the ramdrive at 2048 to 3071, the virtio disk from 3072. Three
boots, POST 538 of 538, and typed at the prompt: block 2100 written
and read back, block 3072 read from the disk, a write to it refused,
a block that is not there. `logs/20261007-081603` (amd64), `-081839`
(aarch64), `-082226` (riscv64); and again after the review's changes
below, with blocks 1 and 2047 read clean at the prompt:
`logs/20261007-085017`, `-085254`, `-085636`; and as it stands, with the
block words the kernel's: `logs/20261007-092835`, `-093112`,
`-093456`. The parity hashes are the same on the
three hosted and the three bare-metal systems.
- *Review, 2026-10-07.* One reading of the whole step by a fresh
reviewer; no critical defect. Changed after it: a block request with
fewer than two values on the stack is refused (it had stopped the
node); on bare metal the node's two buffers are emptied when the
chain takes the place of POST's block RAM (it had gone on holding
POST's block 1), and the chain's fast RAM is cleared in the v4 path;
`blocks.c` is built under each test's own warnings and sanitizers;
the hosted link no longer takes whatever objects lie in its
directory (it had linked the withdrawn `store_v3.o`). Two findings
went to Captain Bob, and his rulings on them are section 8.5; they
made the block words the kernel's, which replaced the request first
built here, `( n waddr -- status )`.
- **Not as intended yet.**
- Who may have which block is not checked (8.4).
- On bare metal the virtio disk is read and not written. v3's
subsystem will not write a disk until its owner says it may be
formatted, and that owner is Artemis; so are the genesis signature
and Zuse's root key, which the v3 path does at the same place. v4
has neither Artemis nor Zuse yet.
- POST is still a capsule Hera loads; step 6b.
- The suite's unit test is still not run at 32 bits.
- **Seen and not changed.** On the v3 path the chain's fast RAM comes
from `kmalloc` and is not cleared (`kernel_main.c`, where the chain is
set up); only the ramdrive is. The v4 path clears its own.
**6a. Storage that changes while running.** **Withdrawn 2026-10-07**
(section 8.7): what it was to build is not how v3 does it. Drives that
come and go are built as v3 has them, with identity and the fleet
(`ENGINE.md` steps 7 and 8).
**6b. POST is the kernel's.** Ruled 2026-10-07 (section 9); design
approved 2026-10-07, `NUCLEUS.md` 6.3 and section 7.
**Done 2026-10-07.**
- *The cases* (`v4/system/post_cases.c`, written by `v4/tools/mkpost.py`):
the same 538, checked against the capsule case by case. One expected
result changed: `>IN.initial` prints 6 where it printed 9, because a
case's line no longer begins with the capsule's `T| `; the generator
now runs v3 on the lines as the kernel sends them.
- *The runner* (`v4/system/post.c`), tested in
`v4/tests/test_host_quit.c` with cases that must pass and cases that
must fail.
- *The boot* (`v4/system/boot.c`) runs it after the capsules, prints
`PARITY:V4_SYSTEM`, and seals. `capsules/v4/post79.4th`, `(CATCH)` and
`(EMIT-HOOK)` are gone.
- *First built* with what the cases define left in the dictionary
(`4bdb210a`; `logs/20261007-105118`, `-105335`, `-105651`).
- *Review, 2026-10-07*, by a fresh reviewer. It found that v3 does not
leave them (above, and `NUCLEUS.md` 6.3), and that a case the node did
not come back from would have stalled the boot for hours where the
capsule had ended it. Changed: the boot seals, runs POST, and has the
node do `COLD`; the runner ends POST at such a case and names it;
`hosted-check` boots a program whose POST has failing cases
(`v4/tests/post_cases_fail.c`) and requires `PARITY:FAIL`, `POST:
FAILED`, no prompt, and none of a case's printing on the console.
- *Verified.* `make -C v4 test`, `sanitize` and `hosted-check`; three
bare-metal boots, `logs/20261007-112638` (amd64), `-112901`
(aarch64), `-113220` (riscv64). On all six: POST 538 of 538,
`word_count=314`, `dict_hash=0x220ab283a504a3b3`. At the prompt `T{`
and `RS1` are unknown words, and `HERE` is 8300.
Acceptance, as approved:
1. *Same verdict.* The three hosted programs and the three bare-metal
boots print `PARITY:V4_POST tests=538 pass=538 fail=0`,
`PARITY:V4_SYSTEM word_count=N dict_hash=0x...`, `PARITY:OK` and
`POST: PASSED`, with the same hashes on all six.
2. *Nothing of the harness is on the node.* At the prompt `T{` is an
unknown word, and `(CATCH)` and `(EMIT-HOOK)` are gone from the
nucleus.
3. ~~What the cases define is there.~~ **Reversed 2026-10-07:** *POST
leaves nothing.* A word a case defines is unknown at the prompt
after boot. (The first form rested on a false statement about v3;
`NUCLEUS.md` 6.3.)
4. *The judge can fail.* Given a case with a wrong expected stack, one
with wrong expected output, one that must end in an error and does
not, and one that ends in an error and must not, the runner reports
each as a failure by name, and a boot with a failing case ends
`PARITY:FAIL`, `POST: FAILED`.
5. *A case's printing does not reach the console.* The boot log shows
nothing that a passing case printed.
6. *The suite.* `make -C v4 test` and `sanitize` pass at both widths;
`mkcapsule --lint capsules/` is clean.
7. **A second unit; scaling while running; sleep, wake and kill by
command.**
8. **The hosted product is the five nodes**, on three ISAs: acceptance 1
to 5.
9. **The same on bare metal:** acceptance 6.
## 11. What becomes of what is there
- `v4/system/boot.c` and `v4/tools/hosted.c` hand one node a line and run
it to the end. They stay until step 8, where the hosted product becomes
the five nodes.
- `(LINE)` and `(IDLE)` (`v4/capsule/quit.v4`): `(LINE)` stays, as what
interprets a message's payload. `(IDLE)` becomes the read of a message
at step 3.
- The one port of 2026-10-05, where a node's requests go, is port 0 of the
ports of 4.1.
- The nucleus linked into the binary goes at step 2.
- `DECOMPOSITION.md` section 6 says four ports; it is corrected at step 1.
+362
View File
@@ -0,0 +1,362 @@
# StarForth v4.0.0 — Nucleus, FORTH-79 Capsule and POST
Design, 2026-10-05. Ruled by Captain Bob in conversation the same day; this
file records those rulings. It sits beside `JUSTIFICATION.md` (why v4) and
`DECOMPOSITION.md` (every v3 word's fate), and changes how the vocabulary
`DECOMPOSITION.md` describes is delivered, not what the words do.
## 1. Goal
v4 comes up on amd64, aarch64 and riscv64, both as the bare-metal kernel
under QEMU and as the hosted Linux binary, in this order:
1. start the assembled nucleus;
2. load FORTH-79 from a capsule;
3. run POST on FORTH-79;
4. reach the `ok>` prompt.
That is all this work delivers. Both the kernel and the hosted binary are
products.
## 2. What is wrong today
- All 292 named words are written in F18 assembler text (`v4/capsule/*.v4`).
Only the editor and `SEE` are FORTH source.
- `v4/tools/mkimage.c` assembles and compiles everything on the build
machine. The kernel links the finished memory image. Nothing is loaded
from a capsule at boot.
- There is no v4 POST.
- No QEMU log of a v4 boot exists, so bare-metal v4 has never been accepted.
## 3. Scope
In scope: the FORTH-79 Required Word Set, as colon definitions in a capsule,
and a POST for it.
Out of scope until this is accepted: the Double Number extension set, Q48
arithmetic, logging, access control, the editor, `SEE`, `DEFER`, and every
other StarForth extension. Their `.v4` and `.fth` sources stay in the tree
but are not built into the nucleus image or loaded at boot.
## 4. The nucleus
The nucleus is the part of the vocabulary that stays in assembler. A word is
in the nucleus only if one of these holds.
1. **It is an opcode or a register access.** v4 compiles colon definitions
to native instruction words, so FORTH source cannot write an opcode.
`DUP DROP OVER + AND XOR 2* 2/ @ ! >R R> R@`, the stack-depth registers,
and the console registers behind `KEY` and `EMIT`.
2. **The node needs it to read a capsule.** `WORD FIND NUMBER INTERPRET
QUIT`, the error trap, `HERE ALLOT ,`, `CREATE`, `:`, `;`, `IMMEDIATE`,
`LITERAL`, `[`, `]`, `(` and the code generator.
3. **It lays down code for a colon definition in the capsule.** The control
words become colon definitions, and they need the code generator's
words by name: `(OP,)`, `(LIT,)`, `(LABEL)`, `(BRANCH>)`, `(RESOLVE)`,
`(JUMP,)`, `(CALL,)`, and the control-flow stack words `>CF`, `CF>` and
`(PAIR)`. These get headers. They are not FORTH-79 words.
Everything else in the Required Word Set is a colon definition in the
capsule. That includes words that are assembler today only for speed:
`SWAP ROT - * /MOD 0= < C@ C! CMOVE FILL . <# # #> TYPE COUNT VARIABLE
CONSTANT`, the control words, the vocabulary words and the block words.
The capsule's definitions will run slower than the assembled ones they
replace. That is accepted: the goal is the minimum in assembler.
The list in rule 2 is the starting boundary, not the final one. A word
leaves the nucleus whenever a colon definition of it passes POST (section
8, step 3). The nucleus is at its minimum when no remaining word can be
moved.
## 5. Capsules
### 5.1 Files
| File | Capsule name | Contents |
|---|---|---|
| `capsules/v4/forth79.4th` | `v4:forth79.4th` | The Required Word Set as colon definitions |
| ~~`capsules/v4/post79.4th`~~ | | Gone 2026-10-07: POST is the kernel's (6.3); its cases are `v4/system/post_cases.c` |
Both are ordinary `.4th` capsules: `Block N` headers, lines of at most 64
characters, at most 16 lines to a block. `tools/mkcapsule.c` bakes them into
`capsule_generated.c` with every other file under `capsules/`, hashed, and
signed when the signing key is present. v3 does not load them: it runs
`init.4th` and only what that file `EXEC`s.
Named blobs (any non-`.4th` file under `capsules/`) are baked the same way
and stay available to v4 by name. This work does not need one.
### 5.2 The block-number rule
`validate_forth_blocks` (`tools/mkcapsule.c:409`) accepts block numbers in
`[2048, 5120)`. The floor is real: blocks 0–2047 are the VM's fast RAM. The
ceiling matches no device.
**Change:** the check becomes "block number is at least 2048". There is no
upper bound. Blocks 0–2047 are the only forbidden ones. The collision checks
against other capsules and `tools/capsule-reserved.txt` are unchanged.
`experiments/bare_metal/README.md` and `.claude/CLAUDE.md` state the old
range and are corrected with it.
The v4 capsules take a free range found from `capsules/BLOCK_MAP.md` and
`tools/capsule-reserved.txt`.
### 5.3 Loading
A capsule reaches the node as source lines through its console input, the
way v3's `capsule_exec_payload` interprets a payload line by line. `LOAD`
is not used: the block words are themselves in the capsule.
For each capsule, in order:
1. find it by name (`capsule_find_by_name`);
2. recompute its hash (`capsule_validate`); a mismatch stops the boot;
3. check its signature (`capsule_verify_signature`); invalid stops the
boot, missing warns — v3's rule at `capsule_birth.c:585`;
4. split the payload on `Block N` headers and feed each line to the node,
running the node until it waits for input again;
5. require the node to answer ` ok` to every line.
A line that is not accepted stops the boot and prints the capsule name,
block number, line number and what the node said. v3's retry, which skips a
failing line and runs the block again, is not carried over: a skipped line
in the standard's own capsule must not pass unnoticed.
What the node prints while loading is not shown, except on failure.
In v4 the block numbers are labels for now. They become storage addresses
when the block words exist and a capsule is copied to block storage.
## 6. POST
### 6.1 Source of the cases
POST is v3's own test cases, ported. The cases are in
`v3/src/test_runner/modules/*.c`; each is a name, a line of FORTH and a
flag saying whether an error is expected. Only cases for Required Word Set
words are ported, chosen by word name against the standard's list.
### 6.2 Judging
v3 judges a case only by whether it raised an error
(`v3/src/test_runner/test_common.c:258-285`); the "expected" text is a
comment. A wrong result that raises no error passes. That is not enough for
POST's purpose here, which is to prove each colon definition against the
assembled word it replaces.
**Ruling:** every ported case compares its result. The expected data stack
and printed output are taken from running the same line on the hosted v3
binary, so v3 remains the reference. A case v3 expects to raise an error
must raise one on v4.
Where v3 departs from FORTH-79 for a standard word, FORTH-79 wins (standing
ruling); the case's expected value is then the standard's, and the
departure is reported.
### 6.3 Form
**Ruled 2026-10-07 (`MESH.md` step 6b): POST is the kernel's, as in v3.**
The kernel holds the cases and feeds them to the node; nothing of POST's
harness is loaded into the node's dictionary. What this section said
before — a harness in FORTH in `post79.4th`, and two variables in the
nucleus for it, `(CATCH)` and `(EMIT-HOOK)` — is withdrawn; 6.3a keeps it
for the record.
**v3, as built.** The cases are C tables compiled into the kernel
(`v3/src/test_runner/modules/*.c`). For each, the kernel hands the line to
the interpreter, reads the VM's error flag, and puts the VM's stack
pointers, error and mode back (`test_common.c`, `run_single_test`). It
runs once at boot on the first VM, against block RAM of POST's own, and
the parity line is taken afterwards. What the cases define does not stay:
`run_test_suite` saves the dictionary before each word's cases and puts it
back after them, "to remove test-created words" (`test_common.c:333`,
`:365`).
**v4.**
- *The cases* are a table in C, `v4/system/post_cases.c`, generated by
`v4/tools/mkpost.py` (6.4) and linked into the hosted program and the
kernel. Each is a name, its lines of FORTH, whether an error is
expected, and what it must leave: the data stack, or only how many
values for an address-dependent case (6.5), and the text it prints.
- *The runner*, `v4/system/post.c`, is the kernel's and is shared by the
hosted program, the bare-metal kernel and the tests. For each case it
empties the node's data stack from outside; sends `DECIMAL FORTH
DEFINITIONS`, the state every expected value was generated from (6.4);
sends the case's lines one at a time, as the boot sends any line,
keeping what the node prints and showing none of it; and judges. A case
passes when it ended in an error exactly if one was expected and, if
none was, its stack and its printed text are what the table has. An
error ends the line it is on, so a case is several short lines; it has
ended in an error if any of its lines did.
- *What it prints.* For a failing case, `POST FAIL: name out<...>
stack<...>`. Then `PARITY:V4_POST tests=N pass=N fail=N`.
- *What is left: nothing*, as in v3. The system is sealed before POST,
and after the last case the kernel has the node do `COLD`, which comes
back to the system as sealed; what `COLD` prints is not shown. The
dictionary is put back once, after all the cases, and not after each
word's as v3 does: the expected results were taken from one v3 session
in which nothing was put back between cases (6.4), and the capsule did
the same with its one `FORGET` at the end.
**How this came to be ruled twice.** On 2026-10-07 Captain Bob was told
that v3 leaves the cases' definitions in the dictionary, and ruled "as
v3: they stay". That was false: it was read from `run_single_test`,
which puts back only the stacks, without reading `run_test_suite`. It
was built so, and the review of the step found it; shown what v3 does,
he ruled that POST leaves nothing.
- *A case the node does not come back from* — it has stopped, or runs for
ever — fails, and POST ends there: the runner names it and says how many
cases were not run.
- *What use still changes.* After POST and `COLD`, `HERE` and `LATEST` are
where they were, but the dictionary hash is not what it was before POST:
each word POST had interpreted has a different TTL in its entry, the
count of uses left before its access is checked again
(`v4/capsule/compile.v4`, `(ACL?)`). 138 cells, measured 2026-10-07. It
is the same on every system, so the six still agree.
- *Gone:* `capsules/v4/post79.4th` and its blocks 7000 up; the harness
words; `(CATCH)` and `(EMIT-HOOK)`, with what `EMIT` and the prompt
loop did for them.
### 6.3a As it was until step 6b
`post79.4th` defined a small harness in FORTH and a tally, and needed two
variables from the nucleus: `(CATCH)`, so that a line ending in an error
ended ` ok` and POST could go on; and `(EMIT-HOOK)`, the xt of a word given
each character in place of the console, so that POST could compare what a
case printed. Its last block was `T-REPORT` and `FORGET T#`: the harness
and everything the cases had defined were forgotten.
### 6.4 Generating the expected values
`v4/tools/mkpost.py` reads the v3 test modules, keeps the cases for
Required Word Set words, runs them in one session of the hosted v3 binary,
and writes `v4/system/post_cases.c` (until step 6b, `capsules/v4/post79.4th`). It is a development tool
(`make -C v4 post79`), run when the cases or the rules change; its output
is committed and reviewed like any source. Every case starts from the same
state on both machines: empty stack, `DECIMAL`, `FORTH DEFINITIONS`.
### 6.5 Where POST does not take v3's word (ruled 2026-10-05)
`v4/tools/post79_rules.py` holds the exceptions, each with its reason, and
`mkpost.py` writes them out as `POST79.md`, which lists every one.
1. **Address-dependent cases.** A case that leaves or prints a memory
address is checked for the number of values it leaves, and for what it
prints unless an address is printed. The values are not compared.
2. **Where v3 departs from FORTH-79.** The expected result is the
standard's. Where v3's line is not valid FORTH-79 (`>R` at the prompt, a
single number given to `<#`), the line is rewritten so that it is.
3. **Cases written for v3's machine.** A v4 address unit is a cell (D-1);
cases that assume v3's byte addresses are rewritten in v4's terms.
4. **Words v3 has no case for** get cases written by hand, with the
standard's results. `U*` and `U/MOD`, which neither v3 nor the v4
nucleus had, are colon definitions in `forth79.4th`.
Not tested: `KEY`, `EXPECT` and `QUERY`, which wait for the keyboard, and
`QUIT`, which returns to the terminal without ` ok`.
## 7. Hashing, signing and parity
Hashing and signing are the existing capsule mechanisms, used unchanged
(section 5.3).
Parity needs one new function. `sk_dict_canonical_hash` walks v3's
`DictEntry` list and cannot hash a v4 node. The v4 dictionary hash is
FNV-1a (`fnv1a_64`, already in `kernel/src/vm/parity.c`) over the node's
memory from the start of code to `HERE`, and `LATEST`. Stacks, input
buffers, block buffers and heat are left out.
The boot prints, in order:
```
PARITY:V4_NUCLEUS words=N image_hash=0x...
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x... capsule_hash=0x... dict_hash=0x...
PARITY:V4_POST tests=N pass=N fail=N
PARITY:V4_SYSTEM word_count=N dict_hash=0x...
PARITY:OK
POST: PASSED
ok>
```
`PARITY:V4_SYSTEM` (step 6b) is the parity of the system as it is sealed,
after POST, as v3's `PARITY:M7.1a` is taken after POST: how many words
FORTH holds and the dictionary hash. Until step 6b the POST capsule had a
`PARITY:V4_CAPSULE` line of its own in that place.
On any failure it prints `PARITY:FAIL` and `POST: FAILED` in place of the
last three lines and does not give a prompt.
The tags are `PARITY:V4_*` so tooling that looks for v3's `PARITY:M7.1a`
is not misled.
Every build uses 64-bit cells on the same engine, so every line above is
identical on all six builds of one commit (section 9). The dictionary hash
changes whenever a word moves between the nucleus and the capsule, so
there is no fixed golden hash until decomposition is finished.
The nucleus is data linked into the binary and is trusted as the binary's
own code is. It is hashed for the parity line and not signed.
## 8. Order of work
1. **Capsule loading at boot**, hosted and bare metal, with the block-number
change. The capsule may be nearly empty at this step.
2. **POST against today's assembled words.** POST must pass here, on words
`make -C v4 test` already covers. This proves POST before it judges
anything new.
3. **Decompose group by group.** Move one group from `.v4` to
`forth79.4th` and run POST hosted: stack, arithmetic, comparison,
memory, strings, number output, control, defining, vocabulary, blocks.
A failure points at the group just moved.
4. **Acceptance** (section 9).
## 9. Products and acceptance
The hosted binary and the kernel run the same engine (`v4/src`), the same
nucleus image, the same baked capsule directory and the same boot sequence
(section 5.3 and section 7). Only the console differs: stdin and stdout
hosted, the serial console on bare metal. The hosted binary links
`capsule_generated.c` and reads nothing from the source tree.
Acceptance is six boots of one commit:
| | amd64 | aarch64 | riscv64 |
|---|---|---|---|
| Hosted Linux | native | user-mode QEMU | user-mode QEMU |
| Bare metal | `clean qemu` | `clean qemu` | `clean qemu` |
Each must print identical `PARITY:V4_*` lines, pass POST and reach `ok>`.
Bare-metal runs follow the repository's QEMU rules: one instance at a time,
in the foreground, `clean` before `qemu`, logs kept under `logs/`.
`make -C v4 test` also runs at 32-bit cells. That stays as a development
check for the FPGA gateway and is not part of this acceptance.
## 10. What is reused and what is new
| Need | Existing | Change |
|---|---|---|
| Bake capsules | `tools/mkcapsule.c`, `kernel/Makefile` capsule rule | Block-number check only (5.2) |
| Find, hash-check, verify | `capsule_find.c`, `capsule_validate.c`, `capsule_sig.c` | None; also compiled into the hosted binary |
| Split a payload into blocks and lines | `is_block_header` and the walk in `capsule_loader.c` | Moved to a file both loaders call |
| Start a node from an image | `v4/src/image.c`, `v4_image_boot` | None |
| Build the nucleus image | `v4/tools/mkimage.c` | File list shrinks as groups move; the step that types `editor.fth` and `tools.fth` is removed |
| Kernel entry | `kernel/src/v4/sk_v4.c`, Kconfig `STARFORTH_V4` | Calls the shared boot sequence before reading the keyboard |
| Hosted entry | `v4/tools/hosted.c` | Same |
| Parity lines and FNV-1a | `kernel/src/vm/parity.c` | v4 dictionary hash added |
| v3 cases | `v3/src/test_runner/modules/*.c` | Read only |
| Expected values | hosted v3 binary | Read only |
| Hosted builds per ISA | `v4/Makefile` | Cross-compiler targets for aarch64 and riscv64 |
New: the shared v4 boot sequence (one file), the v4 dictionary hash, the
case-extraction script, `forth79.4th` and `post79.4th` (the last gone since step 6b, 6.3).
The v3 boot is untouched: `STARFORTH_V4` defaults to off.
## 11. Not decided here
- Where the v4 hosted binaries are installed. They are built under
`v4/build/`; `lfs/` holds v3's and is left alone.
- Signing the nucleus. It would have to become a named blob under
`capsules/`, which puts a generated file in a source directory.
+204
View File
@@ -0,0 +1,204 @@
# POST for the FORTH-79 Required Word Set: where it departs from v3
Written by `v4/tools/mkpost.py` from `v4/tools/post79_rules.py`; do not edit.
Design: `NUCLEUS.md` section 6. The cases are `v4/system/post_cases.c`.
POST runs 538 cases. 431 are v3's, with what the hosted v3 binary did as the
expected result. This file lists every other case, and why.
## 1. Rewritten for FORTH-79 (59)
v3 departs from the standard, or the case is written for v3's machine. v4
follows the standard (standing ruling), so the expected result is the
standard's and not v3's.
| Case | v3's line | What v3 did | As POST runs it | Must do | Why |
|---|---|---|---|---|---|
| `PICK.pick_0` | `1 2 3 0 PICK . CR` | stack `1 2 3`, printed `3 \n` | unchanged | an error | FORTH-79: PICK and ROLL count from 1; v3 counts from 0; 0 PICK is an error |
| `PICK.pick_1` | `1 2 3 1 PICK . CR` | stack `1 2 3`, printed `2 \n` | unchanged | stack `1 2 3`, prints `3 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `PICK.pick_2` | `1 2 3 2 PICK . CR` | stack `1 2 3`, printed `1 \n` | unchanged | stack `1 2 3`, prints `2 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `ROLL.roll_1` | `1 2 3 1 ROLL . . . CR` | stack `empty`, printed `1 3 2 \n` | unchanged | stack `empty`, prints `3 2 1 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `>R.basic` | `42 >R R@ . R> . CR` | stack `empty`, printed `42 42 \n` | `: RS1 42 >R R@ . R> . CR ; RS1` | stack `empty`, prints `42 42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.zero` | `0 >R R@ . R> . CR` | stack `empty`, printed `0 0 \n` | `: RS2 0 >R R@ . R> . CR ; RS2` | stack `empty`, prints `0 0 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.negative` | `-123 >R R@ . R> . CR` | stack `empty`, printed `-123 -123 \n` | `: RS3 -123 >R R@ . R> . CR ; RS3` | stack `empty`, prints `-123 -123 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.multiple` | `1 2 >R >R R@ . R> . R@ . R> . CR` | stack `empty`, printed `1 1 2 2 \n` | `: RS4 1 2 >R >R R@ . R> . R@ . R> . CR ; RS4` | stack `empty`, prints `1 1 2 2 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R>.basic` | `42 >R R> . CR` | stack `empty`, printed `42 \n` | `: RS5 42 >R R> . CR ; RS5` | stack `empty`, prints `42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R>.lifo_order` | `1 2 >R >R R> . R> . CR` | stack `empty`, printed `1 2 \n` | `: RS6 1 2 >R >R R> . R> . CR ; RS6` | stack `empty`, prints `1 2 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R@.basic` | `42 >R R@ . R> DROP CR` | stack `empty`, printed `42 \n` | `: RS7 42 >R R@ . R> DROP CR ; RS7` | stack `empty`, prints `42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R@.non_destructive` | `99 >R R@ R@ = . R> DROP CR` | stack `empty`, printed `-1 \n` | `: RS8 99 >R R@ R@ = . R> DROP CR ; RS8` | stack `empty`, prints `-1 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `,.basic` | `42 , HERE 8 - @ . CR` | stack `empty`, printed `42 \n` | `42 , HERE 1 - @ . CR` | stack `empty`, prints `42 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `,.negative` | `-999 , HERE 8 - @ . CR` | stack `empty`, printed `-999 \n` | `-999 , HERE 1 - @ . CR` | stack `empty`, prints `-999 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `+!.basic` | `10 HERE ! 5 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `15 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.by_zero` | `42 HERE ! 0 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `42 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.negative` | `10 HERE ! -3 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `7 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.accumulate` | `0 HERE ! 1 HERE +! 2 HERE +! 3 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `6 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `HERE.after_comma~2` | `HERE 42 , HERE SWAP - 1 CELLS = 0 SWAP /` | error | unchanged | stack `0`, prints nothing | the case divides by zero unless HERE moved by one cell; on v3 it had moved by 8 |
| `MOVE.basic` | `HERE 65 OVER C! HERE 1+ 66 OVER C! HERE HERE 16 + 2 MOVE HERE 16 + C@ . HERE 17 + C@ . CR` | stack `294 295`, printed `65 66 \n` | `65 HERE ! 66 HERE 1+ ! HERE HERE 16 + 2 MOVE HERE 16 + @ . HERE 17 + @ . CR` | stack `empty`, prints `65 66 \n` | FORTH-79: MOVE moves cells; and v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `BASE.base_store` | `16 BASE ! 255 . CR` | stack `empty`, printed `597 \n` | unchanged | stack `empty`, prints `255 \n` | 255 read in base 16 and printed in base 16 is 255; v3 printed 467 |
| `<#.basic` | `DECIMAL 42 S>D <# #S #> TYPE CR` | stack `empty`, printed `774763251095801167872\n` | unchanged | stack `empty`, prints `42\n` | v3 prints 774763251095801167872 for 42 S>D <# #S #> |
| `<#.empty` | `0 <# #> TYPE CR` | stack `0`, printed `\n` | `0 0 <# #> TYPE CR` | stack `empty`, prints `\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `<#.negative` | `-42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `-42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.single_digit` | `15 <# # #> TYPE CR` | stack `empty`, printed `5\n` | `15 0 <# # #> TYPE CR` | stack `empty`, prints `5\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.multiple` | `15 <# # # #> TYPE CR` | stack `empty`, printed `15\n` | `15 0 <# # # #> TYPE CR` | stack `empty`, prints `15\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.zero_pad` | `5 <# # 0 # #> TYPE CR` | stack `0`, printed `05\n` | `5 0 <# # # #> TYPE CR` | stack `empty`, prints `05\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.basic` | `42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `42 0 <# #S #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.zero` | `0 <# #S #> TYPE CR` | stack `empty`, printed `0\n` | `0 0 <# #S #> TYPE CR` | stack `empty`, prints `0\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.large` | `1234567890 <# #S #> TYPE CR` | stack `empty`, printed `1234567890\n` | `1234567890 0 <# #S #> TYPE CR` | stack `empty`, prints `1234567890\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.negative` | `-42 ABS <# #S SIGN #> TYPE CR` | stack `0`, printed `42\n` | `-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `-42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.positive` | `42 <# #S SIGN #> TYPE CR` | stack `0`, printed `42\n` | `42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.zero` | `0 <# #S SIGN #> TYPE CR` | stack `0`, printed `0\n` | `0 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `0\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.normal` | `42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `42 0 <# #S #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.empty` | `0 <# #> TYPE CR` | stack `0`, printed `\n` | `0 0 <# #> TYPE CR` | stack `empty`, prints `\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.stack_effect` | `42 <# #S #> SWAP . . CR` | stack `empty`, printed `24 2 \n` | `42 0 <# #S #> SWAP DROP . CR` | stack `empty`, prints `2 \n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `HOLD.basic` | `42 <# 46 HOLD #S #> TYPE CR` | stack `empty`, printed `42.\n` | `42 0 <# 46 HOLD #S #> TYPE CR` | stack `empty`, prints `42.\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `COUNT.basic` | `HERE S" Test" DROP COUNT . . CR` | stack `294`, printed `84 295 \n` | `4 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `4 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and S" is not FORTH-79 |
| `COUNT.empty` | `HERE 0 OVER C! COUNT . . CR` | stack `empty`, printed `0 300 \n` | `0 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `0 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.max_length` | `HERE 255 OVER C! COUNT . . CR` | stack `empty`, printed `255 300 \n` | `255 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `255 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.basic~2` | `HERE S" Test" DROP C@ HERE 1+ SWAP COUNT . . CR` | stack `404 410`, printed `0 85 \n` | `4 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `4 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.empty~2` | `HERE 0 OVER C! COUNT . . CR` | stack `empty`, printed `0 410 \n` | `0 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `0 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.max_length~2` | `HERE 255 OVER C! COUNT . . CR` | stack `empty`, printed `255 410 \n` | `255 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `255 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `CMOVE.basic` | `HERE S" Test" DUP >R HERE 10 + SWAP CMOVE CR` | stack `317`, printed `\n` | `65 PAD C! 66 PAD 1+ C! PAD PAD 10 + 2 CMOVE PAD 10 + C@ . PAD 11 + C@ . CR` | stack `empty`, prints `65 66 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and the v3 case leaves a value on the return stack |
| `TYPE.basic_string` | `HERE S" Hello" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `392`, printed `Hello\n` | `72 PAD C! 105 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `Hi\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `TYPE.numbers` | `HERE S" 12345" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `398`, printed `12345\n` | `49 PAD C! 50 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `12\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>IN.initial` | `>IN @ . CR` | stack `empty`, printed `657828592 \n` | unchanged | stack `empty`, prints `6 \n` | FORTH-79: >IN is the offset into the input, 6 here; v3 prints an address |
| `UPDATE.no_block` | `0 SCR ! UPDATE` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for |
| `SCR.after_load` | `1 LOAD SCR @ . CR` | stack `empty`, printed `` | unchanged | stack `empty`, prints `1 \n` | v3 prints nothing after 1 LOAD; SCR is still 1 from the LIST before |
| `FIND.existing` | `FIND DUP . CR` | stack `empty`, printed `657827952 \n` | unchanged | 0 value(s) left; output not compared | FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed |
| `FIND.user_word` | `: test5 44 ; FIND test5 . CR` | stack `empty`, printed `658154448 \n` | unchanged | 0 value(s) left; output not compared | the address FIND leaves is printed |
| `FIND.empty` | `FIND` | error | unchanged | stack `0`, prints nothing | FORTH-79: FIND leaves 0 when there is no word; v3 raises an error |
| `VOCABULARY.cross_vocab_access` | `VOCABULARY V1 V1 DEFINITIONS : V1WORD 11 ; FORTH V1WORD . CR` | stack `empty`, printed `11 \n` | unchanged | an error | FORTH-79: a word defined in another vocabulary is not found from FORTH; v3 finds it |
| `VOCABULARY.duplicate` | `VOCABULARY TESTVOC VOCABULARY TESTVOC` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for: defining a name again |
| `IF.true` | `: TEST1 IF 42 ELSE 24 THEN ; -1 TEST1 . CR` | stack `empty`, printed `42 \n` | unchanged | stack `empty`, prints `42 \n` | v3 runs an older TEST1 from another vocabulary and leaves the flag |
| `IF.false` | `: TEST2 IF 42 ELSE 24 THEN ; 0 TEST2 . CR` | stack `empty`, printed `24 \n` | unchanged | stack `empty`, prints `24 \n` | v3 runs an older TEST2 from another vocabulary and leaves the flag |
| `LEAVE.basic` | `: TLV 0 5 0 DO I 3 = IF LEAVE THEN 1+ LOOP ; TLV . CR` | stack `empty`, printed `3 \n` | unchanged | stack `empty`, prints `4 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
| `LEAVE.at_start` | `: TLV2 0 5 0 DO LEAVE 1+ LOOP ; TLV2 . CR` | stack `empty`, printed `0 \n` | unchanged | stack `empty`, prints `1 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
| `LEAVE.qdloop` | `: TLV3 0 5 0 ?DO I 2 = IF LEAVE THEN 1+ LOOP ; TLV3 . CR` | stack `empty`, printed `2 \n` | unchanged | stack `empty`, prints `3 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
## 2. Address-dependent (20)
The case leaves or prints a memory address, which is a different number on
the two machines. The number of values left is checked, and what is
printed unless an address is printed; the values are not.
| Case | Line | Checked |
|---|---|---|
| `-TRAILING.basic` | `HERE S" Test " -TRAILING TYPE CR` | values left, and output |
| `-TRAILING.all_spaces` | `HERE S" " -TRAILING TYPE CR` | values left, and output |
| `-TRAILING.no_spaces` | `HERE S" Test" -TRAILING TYPE CR` | values left, and output |
| `HERE.stability` | `HERE DUP HERE = . CR` | values left, and output |
| `PAD.stability` | `PAD DUP PAD = . CR` | values left, and output |
| `UPDATE.basic` | `1 BLOCK UPDATE` | values left, and output |
| `UPDATE.multiple` | `1 BLOCK UPDATE UPDATE` | values left, and output |
| `SAVE-BUFFERS.dirty_blocks` | `1 BLOCK UPDATE SAVE-BUFFERS` | values left, and output |
| `EMPTY-BUFFERS.after_use` | `1 BLOCK EMPTY-BUFFERS` | values left, and output |
| `EMPTY-BUFFERS.dirty_blocks` | `1 BLOCK UPDATE EMPTY-BUFFERS` | values left, and output |
| `BUFFER.flush_dirty` | `2 BLOCK 1+ 2 BUFFER` | values left, and output |
| `BLOCK.basic` | `1 BLOCK DUP . CR` | values left only |
| `BUFFER.basic` | `1 BUFFER DUP . CR` | values left only |
| `VOCABULARY.create_and_switch` | `VOCABULARY MYVOC MYVOC DEFINITIONS CONTEXT @ . CR` | values left only |
| `CONTEXT.basic` | `CONTEXT @ . CR` | values left only |
| `CONTEXT.initial` | `FORTH CONTEXT @ . CR` | values left only |
| `CURRENT.basic` | `CURRENT @ . CR` | values left only |
| `CURRENT.after_def` | `VOCABULARY TEST-VOC8 TEST-VOC8 DEFINITIONS CURRENT @ . CR` | values left only |
| `LIST.basic` | `1 LIST` | values left only |
| `SCR.after_list` | `1 LIST SCR @ . CR` | values left only |
## 3. Written by hand (28)
For required words v3 has no case for. The expected results are the
standard's; no v3 run stands behind them.
| Case | Line | Must do |
|---|---|---|
| `U*.small` | `3 4 U* . . CR` | stack `empty`, prints `0 12 \n` |
| `U*.zero` | `0 5 U* . . CR` | stack `empty`, prints `0 0 \n` |
| `U*.full` | `-1 -1 U* 2+ . 1 = . CR` | stack `empty`, prints `0 -1 \n` |
| `U/MOD.small` | `7 0 2 U/MOD . . CR` | stack `empty`, prints `3 1 \n` |
| `U/MOD.exact` | `12 0 4 U/MOD . . CR` | stack `empty`, prints `3 0 \n` |
| `U/MOD.double` | `0 1 2 U/MOD 2* . . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.inverse` | `-1 -1 U* -1 U/MOD 1+ . . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.carry` | `-1 -2 -1 U/MOD 1+ . 2+ . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.by_zero` | `1 0 0 U/MOD` | an error |
| `?.basic` | `VARIABLE Q1 42 Q1 ! Q1 ? CR` | stack `empty`, prints `42 \n` |
| `EXECUTE.found` | `: E1 7 ; FIND E1 EXECUTE . CR` | stack `empty`, prints `7 \n` |
| `U..small` | `42 U. CR` | stack `empty`, prints `42 \n` |
| `U..unsigned` | `-1 U. CR` | stack `empty`, prints `18446744073709551615 \n` |
| `CONVERT.digits` | `: C1 0 0 BL WORD CONVERT DROP ; C1 123 . . CR` | stack `empty`, prints `0 123 \n` |
| `'.found` | `' DUP 0= . CR` | stack `empty`, prints `0 \n` |
| `.".in_definition` | `: Q3 ." hi there" ; Q3 CR` | stack `empty`, prints `hi there\n` |
| `LITERAL.basic` | `: Q4 [ 5 ] LITERAL ; Q4 . CR` | stack `empty`, prints `5 \n` |
| `STATE.interpreting` | `STATE @ . CR` | stack `empty`, prints `0 \n` |
| `STATE.compiling` | `: Q5 STATE @ 0= . ; IMMEDIATE : Q6 Q5 ; CR` | stack `empty`, prints `0 \n` |
| `COMPILE.basic` | `: Q7 COMPILE DUP ; IMMEDIATE : Q8 Q7 ; 3 Q8 . . CR` | stack `empty`, prints `3 3 \n` |
| `[COMPILE].basic` | `: Q9 [COMPILE] IF ; IMMEDIATE : Q10 Q9 1 ELSE 2 THEN ; 0 Q10 . -1 Q10 . CR` | stack `empty`, prints `2 1 \n` |
| `(.comment` | `1 ( 2 ) 3 . . CR` | stack `empty`, prints `3 1 \n` |
| `BLK.terminal` | `BLK @ . CR` | stack `empty`, prints `0 \n` |
| `79-STANDARD.present` | `79-STANDARD` | stack `empty`, prints nothing |
| `WORD.next_word` | `: W1 BL WORD COUNT TYPE ; W1 HELLO` | stack `empty`, prints `HELLO` |
| `WORD.delimiter` | `: W2 44 WORD COUNT TYPE ; W2 A B,` | stack `empty`, prints `A B` |
| `WORD.count` | `: W3 BL WORD C@ . ; W3 ABC` | stack `empty`, prints `3 ` |
| `WORD.skips_leading` | `: W4 BL WORD COUNT TYPE ; W4 X` | stack `empty`, prints `X` |
## 4. v3 cases left out (39)
| Case | Why |
|---|---|
| `EXPECT.zero_length` | reads the keyboard |
| `EXPECT.one_item` | reads the keyboard |
| `EXPECT.empty_stack` | reads the keyboard |
| `FORGET.nonexistent` | forgets a word of the system |
| `FORGET.protected` | forgets a word of the system |
| `CONTEXT.modify` | damages the system on purpose |
| `CURRENT.protect` | damages the system on purpose |
| `QUIT.in_definition` | v3 did not get through it in one session |
| `:.nested` | v3 did not get through it in one session |
| `].basic` | v3 did not get through it in one session |
| `ELSE.double` | v3 did not get through it in one session |
| `THEN.extra` | v3 did not get through it in one session |
| `C!.basic` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.zero` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.high_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C!.truncation` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C@.after_cstore` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `C@.zero_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `FILL.basic` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `FILL.zero_byte` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `COUNT.string_bounds` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `CMOVE.overlap` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `CMOVE.bounds` | moves 1000 bytes over the system; v3 refuses by a bounds rule FORTH-79 has not got |
| `TYPE.single_char` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `COUNT.zero_addr_plus_one` | OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): the case wrote to or read from the nucleus's code at cell HERE/4 |
| `WORD.empty_input` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.space_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.newline_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.tab_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.comma_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.skip_leading` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.single_char` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.long_word` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.zero_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.high_ascii` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.count_format` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.count_value` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.multi_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `PAD.usable` | OPEN: PAD 42 OVER ! faults on v4 -- PAD is a byte address and ! takes a cell address (D-1) |
## 5. Required words POST does not test
`KEY` `EXPECT` `QUERY` `QUIT`.
`KEY`, `EXPECT` and `QUERY` wait for the keyboard, which a boot cannot type
at. `QUIT` returns to the terminal without `ok`.
## 6. Where v3 did not do what its own table expects (6)
POST expects what v3 did, unless section 1 says otherwise.
| Case | |
|---|---|
| `+!.basic` | v3's table says no error, v3 raised one |
| `+!.by_zero` | v3's table says no error, v3 raised one |
| `+!.negative` | v3's table says no error, v3 raised one |
| `+!.accumulate` | v3's table says no error, v3 raised one |
| `HERE.after_comma~2` | v3's table says no error, v3 raised one |
| `CREATE.long_name` | v3's table says error, v3 raised none |
+668
View File
@@ -0,0 +1,668 @@
# StarForth v4.0.0 — Parity with v3 up to the first prompt
Started 2026-10-05. This file measures v4 against one ruling:
> v4 is exactly like v3 in functional requirements up to the first FORTH
> prompt. That is the stopping point for now. (Captain Bob, 2026-10-05)
and a second, given the same day:
> I would never stub, simulate or develop outside of functional,
> as-intended code. Piecemeal is how errors and intent get hidden.
It records what v3 does, where in v3's code, what v4 does today, and what
has to be ruled before v4 can do the same. It proposes; it decides nothing.
Nothing described here as missing has been built.
## 1. What v3 does before its first prompt
From the bare-metal boot log `logs/20261003-100613/amd64/`, in order, after
the kernel's M0–M6 hardware milestones:
| # | v3 | v4 today |
|---|---|---|
| 1 | Stadium allocated (cells, 50 VM slots); switch-signal and kernel-Hermes channel tables | Missing |
| 2 | VM arena; 530 words registered | One node; 295 assembled words and 2 from the capsule |
| 3 | Physics live whenever a word executes: per-word heat, decay, rolling window, pipelining, L8 mode selector (61 mode changes during POST), heartbeat | Missing; see section 2 |
| 4 | POST: 1050 tests, every module (FORTH-79, StarForth extensions, ACL, Mama, Q48, inference, physics freeze) | 550 cases, FORTH-79 Required Word Set only |
| 5 | `PARITY:M7.1a` word count, here, latest, dictionary hash; `PARITY:OK`; `POST: PASSED` | Present, as `PARITY:V4_*` |
| 6 | PCI; virtio-blk attached; Artemis genesis signature | PCI and the disk attached, after POST (2026-10-07). No genesis signature; the disk is read and not written |
| 7 | Mama init capsule `init.4th`: signature checked, executed, `PARITY:MAMA_INIT` | v4 loads its own two capsules the same way; not `init.4th` |
| 8 | ACL: `BIRTH` and `CAPSULE-BIRTH` pinned strict | Missing. The ACL words are assembled but not loaded |
| 9 | Heartbeat running; Stadium conservation checks; quota and kernel-Hermes self-tests | Missing |
| 10 | Hestia born, then Artemis (disk formatted, self-test, K figures), each with `PARITY:BIRTH` | Missing. One node, no fleet |
| 11 | Kernel-Hermes fleet self-tests; USB mass storage; Zuse genesis minted on the thumbdrive | Missing |
| 12 | Banner with versions; `[zuse@Hera] ok>` | `ok>` alone |
Rows 1, 6, 9, 10 and 11 are messaging, blocks and the console's wider
setting. They are to be discussed before anything is written about them.
Section 2 is row 3.
## 1a. Correction (2026-10-05): the table above is wrong about "Missing"
Captain Bob: "You haven't looked at the codebase. You're assuming missing
pieces that are not missing and ignoring the entire v3 architecture. Don't
look at pieces until you see how they fit."
The table was made from a boot log and a few functions. Read as a whole,
the system is this.
**How v3 fits together.** After the hardware milestones, `kernel_main.c`
(`kernel_main_deep`, from line 526) does, in order:
1. Fleet tables, before any VM exists: Stadium, sessions, switch-signal,
kernel-Hermes channels and queues; Hera made patron zero.
2. `sk_vm_bootstrap_parity()` (`kernel/src/vm/bootstrap/sk_vm_bootstrap.c`):
one `VM` is initialised with the kernel's host services; the capsule
hooks and the VM registry are set up; its fleet physics is seeded
(`vm_physics_init`); the kernel's own words are registered (`BIRTH`,
`KILL`, the capsule words); the block subsystem is given to it; POST
runs; parity is collected and printed.
3. Devices: block RAM and ramdrive, PCI, entropy, Artemis's virtio disk and
its signature, keyboard, USB, framebuffer.
4. The capsule directory is copied and `capsule_birth_mama()` runs
`init.4th` on that VM; `BIRTH` and `CAPSULE-BIRTH` are pinned.
5. The timer starts: the heartbeat.
6. Stadium and kernel-Hermes self-tests; Hestia and Artemis are born, each
another `VM`; Zuse; the banner; the REPL.
Every one of those steps is kernel code in `kernel/src` and works on a
`VM` through one interface: the `VM` structure (`v3/include/vm.h`) and the
functions on it. Counted across the kernel's services, the most used are
the stacks (`vm_push`, `vm_pop`), `vm_interpret`, `vm_find_word`, the
dictionary (`latest`, `here`, `DictEntry`), `error`, the heartbeat state,
the rolling window, and `stadium_vm_id`.
**What v4 is, by its own justification** (`JUSTIFICATION.md` section 16):
"The only difference is the machine underneath: the F18-derived engine
instead of the original StarForth VM." And section 15: StarshipOS is
"LithosAnanke running the v4 F18 engine", with the FORTH-79 vocabulary and
"the StarshipOS-specific portions" stored as capsules.
**So nothing in rows 1, 6, 7, 8, 9, 10, 11 or 12 is missing.** It is all
in this kernel and it all runs today. What is wrong is where v4 was put:
`kernel_main.c` calls `sk_v4_run()` *before* step 1 and it never returns.
The v4 node comes up beside the system instead of inside it, and so the
whole of the architecture is skipped. That is the detour. The capsule
loader, POST and parity lines built on 2026-10-05 (`v4/system/boot.c`)
repeat, for a lone node, things steps 2 and 4 already do for a `VM`.
**The real question** is therefore not "what does v4 lack" but "how does
the F18 engine take the place of the StarForth VM behind that one
interface", so that steps 1 to 6 run as they do now. That has not been
worked out, and nothing here should be read as a plan for it.
Section 2 below was written before this correction. Its account of what
v3's inner loop does is from the code and stands. Its section 2.2, "what
v4 has", describes the lone node, not v4 in its place in the system.
## 1b. Console (discussed and ruled 2026-10-05)
**v3, as built** (`FABRIC-3.5.md` sections XVII and XVIII):
- Layer 0, the fabric: UART, framebuffer, VT100, font, in kernel C
(`kernel/src/hal`). Singular, permanent, and it never knows a VM exists;
anything VM-shaped reaches it by a registered callback.
- Layer 1, Hestia: the Tripod leg that owns console policy and is the bind
point. Her vocabulary is the drawing words.
- Layer 2, console proxies: one VM per attached user, born at `WIREBIND`;
their traffic goes through kernel-Hermes.
- Input belongs to the kernel. `kernel/src/repl.c` reads the serial port and
the keyboard, echoes, edits and builds the line, then hands the whole line
to the VM `USE` has made active (`vm_interpret`), or sends it through
kernel-Hermes. It services the heartbeat while idle.
- Output: a VM's `EMIT` goes to the host service `putc`, then
`console_putc`. The fabric puts `[user@VM]` before each line. The prompt
is the REPL's, not the VM's.
A v3 VM never reads its own command line and never prints its own prompt.
**Ruling (Captain Bob, "for now, yeah"):** the kernel hands a v4 node a
whole line and takes characters back, exactly as it does a v3 VM. The
node's own prompt loop plays no part at that level. Layers 0, 1 and 2 and
the kernel's REPL stay as they are. `KEY`, `EXPECT`, `QUERY` and `QUIT`
remain FORTH-79 words but are not how the kernel drives a node.
"For now": `DECOMPOSITION.md` makes `EMIT` and `KEY` messages to a console
device node, which in the mesh is Hestia's part. That is the destination,
not this step.
**What this says about the lone-node boot.** It has the node run its own
`QUIT` loop, print its own prompt and ` ok`, and wait in `KEY`;
`v4/system/boot.c` then takes the ` ok` and prompt back out of what the
node prints. That is a work-around for the node owning what the kernel
owns, and it goes when the engine takes the VM's place.
## 1c. The frame: the Stadium
`FABRIC-2.md` section B: "words are stadium patrons, VMs are patrons,
blocks are patrons, messages are stadium patrons, all should be operated on
by THE SAME ENGINE."
- A patron is one 64-byte cell of nine wires: identity, heat, TTL, pin,
link, behaviour, mass, payload, contains
(`kernel/include/starkernel/vm/stadium.h`).
- Heat is a conserved share of 1.0; K is that conservation.
- The behaviour is what happens when a patron leaves: words and VMs `COOL`,
blocks `MIGRATE`, messages `DELIVER`, ACLs `EXPIRE`.
- A cell never holds the thing itself, only its identity and heat. A word's
code stays in the dictionary; a block's bytes stay in the block
subsystem.
Compudynamics is therefore the kernel's Stadium, not something inside the
VM engine. The engine's part is to report what it executes: in v3,
`stadium_word_dispatch(vm, word_id, tick)` from the inner loop.
**Reading of the opcode ruling (section 2.7) in this frame, stated to
Captain Bob 2026-10-05 and not corrected:** on v4 that report carries an
opcode, and a VM's word patrons become its 32 opcode patrons.
## 1d. Blocks (discussed and ruled 2026-10-05)
**v3, as built:**
- One block address space in kernel C (`v3/src/block_subsystem.c`): fast
RAM at 0 to 2047, the ramdrive, Artemis's virtio disk, USB drives,
chained.
- Every block has metadata (`blk_meta_t`): the owner's fingerprint,
`acl_allow`, `acl_ttl`, a write count, chain links.
- Blocks are patrons: `BLOCK`, `BUFFER` and `UPDATE` call
`stadium_block_dispatch` with the LBN; heat drives migration and wear
levelling (`kernel/src/vm/stadium_blocks.c`).
- Artemis is the Tripod leg that owns persistent storage.
- Identity lives there: a keypair Zuse mints onto a thumbdrive; its
fingerprint is stamped on every block it claims at first touch, behind
the home-blocks fence; the disk's genesis signature is how the kernel
knows Artemis's disk.
A v3 VM's block words ask the kernel's block subsystem for a block and get
a buffer back. Ownership, ACL, physics and devices are the kernel's.
**Ruling (Captain Bob, "yes"):** a v4 node only ever asks for a block by
number. Everything about who may have it is decided on the kernel's side,
under the VM's identity. The device chain, metadata, first-touch claim,
ACL, migration and the Stadium touch stay where they are.
**What this says about the lone-node boot.** It gives the node a bare RAM
array behind its four block registers (D-19), which skips the address
space, the metadata, the owner, the ACL and the Stadium.
**As of 2026-10-07** (`MESH.md` step 6): the registers and the RAM array
are gone. A node's block words are kernel requests by block number, and
`v4/system/blocks.c` serves them through v3's block subsystem into the
node's window of four slots, as v3's block words do for a VM; so the
address space and the metadata are v3's. Still skipped: the owner and first-touch
claim, the ACL, and the Stadium touch, which need the node's identity. A
first build of that step had block requests passed from node to node and
nodes with no storage; Captain Bob withdrew it as a divergence from this
ruling (`MESH.md` 8.6).
## 1e. Messaging (discussed and ruled 2026-10-05)
**v3, as built** (`FABRIC-3.5.md` sections III, XLIII, XLV;
`kernel/include/starkernel/vm/kernel_hermes.h`):
- Hermes is the kernel, not a VM: the routing layer between the Stadium
floor and the HAL.
- A message is a patron. Its heat is the Stadium cell it occupies; sending
draws a slot's worth from the sender's reservoir; a ledger of held,
pulled, returned and consumed is audited for conservation.
- Publish and subscribe: one permanent common channel every VM joins at
birth; a private channel by request and grant or deny, with ACK/NACK.
- Who may open a channel with whom is ACL policy: kernel-Hermes asks
`ACL.4th` and does not decide.
- A payload is FORTH text, one block at most; larger is chunked.
- Hermes delivers into no one. It queues the payload for the target and
publishes the fact. The target drains its own queue at its outermost
interpret checkpoint by interpreting the text; the switcher reads the
same fact to choose which VM runs.
**Ruling (Captain Bob, "yes"):** a v4 node receives by being handed text
to interpret, and sends by asking. Everything else stays in kernel-Hermes:
heat, channels, the ACL question, queues, the ledger, the switcher.
**Open.** v3's safe moment is a per-word checkpoint in the inner loop, at
the outermost interpret level. A v4 node has no inner loop. What a node's
safe moment is has to be defined; it is also where the switcher moves
control, so it decides how the fleet shares the processor.
**Intent stated, for later (Captain Bob, 2026-10-05):** "When Artemis time
comes, we're going to pull that up into the kernel as well." As Hermes
became kernel-Hermes. Not this step. Today Artemis the VM is
`capsules/artemis/init.4th`: 550 lines, 77 words — a flat-pool disk manager
(header, free map, allocator, format and resume, Stadium admission for
allocated blocks, K total, cooling, reap, self-tests).
## 1f. ACLs (discussed 2026-10-05; the word card is to change)
**v3, as built** (`FABRIC-2.md` section H.3): identity carries its ACL as a
stack of cards with pinholes through them. Four cards, a closed set: VM,
word, block, message. An action consults only the cards for the dimensions
it touches.
- Word card: the four fields in each dictionary entry (`acl_ttl`,
`acl_allow`, `acl_mode`, `acl_pinned`); per VM, since each VM has its own
dictionary. Checked at every word dispatch in the inner loop.
- Block card: `acl_allow` and `acl_ttl` in each block's metadata; policy in
`block-acl.4th`.
- Message card: the channel-open question kernel-Hermes puts to `ACL.4th`.
- VM card: the VM-level dimension.
- Creator ceiling: a child's ACL state is a snapshot of its parent's at
birth.
- Policy is FORTH; the fields and the hot-path check are C.
- With physics: a word's TTL is computed from its heat
(`heat/4 + 256`, capped), and an ACL is the patron whose leaving is
`EXPIRE`.
The block, message and VM cards are decided on the kernel's side and carry
over under sections 1d and 1e. The word card does not: a v4 node has no
dispatch point at which to check it, and its TTL comes from per-word heat,
which v4 has not got (section 2.7).
**Captain Bob, 2026-10-05,** asked whether the word card stays per word:
"I think it would be wise to change that as well, because as we move down
our little ladder here into the FPGA area, this is going to be where
pretty much our division point is going to be. We're going to block things
off and we're going to build little teeny teeny machines everywhere on the
fabric."
**Reversed the same day:** "Now that I think about it, let's leave it
exactly as we're doing it in v3 — if we can figure out where to hook into
it to measure that."
So the word card stays per word, as v3, provided a v4 node has a place to
check it.
**Where a v4 node can hook, from the code:**
- There is one. Every call of one word by another is the `call` opcode,
executed at one place in the engine: `v4/src/exec.c`, `case V4_OP_CALL`.
The node has the target's address there, before control moves. It is the
line where the per-call-target count was taken.
- The compiler lays down a `call` for every word that is not in-line
(`(CALL,)`, `v4/capsule/compile.v4:100`) and never turns a call into a
jump, so definitions compiled on the node all pass through it.
**What does not pass through it:**
- **In-line words.** 62 words are compiled as opcodes, not called, among
them `DUP DROP OVER SWAP ROT + - AND XOR OR @ ! +! 1+ 1- 2+ 2- NEGATE`.
v3 checks every one of these at every execution. Six must stay in line
whatever is decided, because a call would bury the return address:
`>R R> R@ I J LEAVE`.
- **`EXECUTE`.** It is `push ;` — the word is entered by a return, not a
call.
- **Hand-written jumps.** The assembled nucleus ends many words by jumping
to another (`: CR 10 jump EMIT`). A colon definition in a capsule does
not, so this shrinks as words move to the capsule.
- **Telling a word from a mere address.** Not every call target is a
dictionary entry: the assembled files define more routines than they
give headers to. The node must be able to tell, at the call, whether the
target has the three cells of an entry before it.
**Intent, stated by Captain Bob 2026-10-05:** "Intent is for a runtime
check of a word. And the way we were originally doing it is the TTL was how
often the check would be performed. If the TTL was still valid, the word
would not get checked, in order to save the overhead of checking for
permissions every time."
So: the check is at run time, per word, at the hook. The countdown is the
cheap part, done on every execution; the permission check itself is done
only when the countdown reaches zero. A check made only when a definition
is compiled does not meet the intent.
What follows for the in-line words: to be checked at run time a word has
to pass through the hook, so it has to be called. The six that cannot be
called (`>R R> R@ I J LEAVE`) are the residue: they can only appear inside
a definition, and can only be checked when it is compiled.
**The TTL is adaptive, as v3 (Captain Bob, 2026-10-05):** "A fixed TTL
would make no sense whatsoever. The hotter the word, the more frequently
it's checked."
v3's policy, `capsules/ACL.4th` block 4003: a word's TTL is its own heat
divided by 4, plus 256, capped — the number of executions until its next
check. A hot word runs through its TTL sooner and so is checked more often
than a cold one, while each check is paid for by more executions.
So a v4 node counts executions **per word**, at the call hook, for the
ACL's use, and `ACL-TTL-COMPUTE` reads that count as it does in v3. This is
a second count beside the per-opcode heat of section 2.7; in v3 the two are
one field.
**Not yet settled, and not to be assumed:**
- Whether the per-word count decays as v3's word heat does (Loop #3), and
whether a word is then a Stadium patron as in v3 or only the 32 opcodes
are.
- How the node tells a dictionary entry from a bare address at the call.
- `EXECUTE`, which enters a word by a return.
## 1g. Correction: every kind of patron keeps its own accounts
Captain Bob, 2026-10-05: "You're looking at it all wrong. Think of v3. The
VM is not the same thing as a word. It has a different set of accounting
rules in the Stadium. Same for any other patron. They define their own
accounting rules. Look at how the code is written."
Sections 1f and 2 above treat heat as one number per thing and ask where
to put it. That is wrong, and the code shows it. The generic engine
(`kernel/src/vm/stadium.c`) knows only a cell, a VM's quota and reservoir,
admission, eviction and the conservation check. What a patron *is*, and
how it is charged, is defined by its own layer:
| Patron | Layer | Its own rules |
|---|---|---|
| VM | `kernel/src/capsule/capsule_vm_physics.c`; quotas in `stadium.c` | Fleet-wide: the heat of all live VMs sums to 1.0. Born at zero. Touched only at dispatch points (`BIRTH`, `KILL`, `VM-EXEC`, `VM-CALL`, `VM-STEP`), which pull heat from the rest of the fleet toward it by elapsed ticks times a slope. On death its heat goes up the parent chain to Hera. Its Stadium quota and reservoir are granted from its parent. |
| Word | `kernel/src/vm/stadium_words.c` | One cell per (VM, word ID). Admitted on first dispatch with a starter grant. Every dispatch pulls a quantum from the VM's reservoir, never below a floor of one third. Cools by a fraction of its own heat per tick, back to the reservoir. Unpinned. Leaves by `COOL`. |
| Block | `kernel/src/vm/stadium_blocks.c` | One cell per (VM, LBN), in a hash table since LBNs are sparse. Its own quantum and cooling rate. Leaves by `MIGRATE`. |
| Message | `kernel/src/vm/kernel_hermes.c` | Admission costs the sender one slot's share. Decays by a fixed factor each time it is applied, and what decays is *consumed*, not returned. A ledger of held, pulled, returned and consumed must balance exactly. |
Each layer's constants are its own Kconfig symbols
(`STADIUM_WORD_HEAT_QUANTUM`, `STADIUM_WORD_COOL_RATE_Q48`,
`STADIUM_BLOCK_HEAT_QUANTUM`, `STADIUM_BLOCK_COOL_RATE_Q48`,
`STADIUM_CAPACITY_TICK`). Per VM, resident heat plus reservoir is 1.0.
**A v3 word already has more than one account.** Its Stadium cell is one.
The `execution_heat` field of its dictionary entry is another, with a
different rule (a flat amount per tick, not a fraction), and
`stadium_words.c` says of admission: "execution_heat plays no role". The
ACL's TTL reads `execution_heat`.
So these earlier statements are withdrawn:
- "A v4 node has two counts where v3 has one field" (section 1f). v3 has
separate accounts already; that is the design, not an anomaly.
- "Whether a word is a Stadium patron or only the 32 opcodes are" (section
1f, open list). It is not either-or. The opcode is a kind of patron with
rules of its own, as the word, the VM, the block and the message each
are.
What is actually open is what the opcode's own rules are.
## 1h. The opcode's accounts: left unwired for now (2026-10-05)
Asked what the opcode's own accounting rules are, Captain Bob: "For the
moment, I think we're going to leave it unwired and kind of maybe just
make some observations on it and see if we even need it, as far as
opcodes are concerned. I think we will when we go to FPGA, but for now,
let's just hang on to the thought and make sure that it's available if
needed."
So:
- No opcode patron layer is built. Opcodes are not wired to the Stadium.
- The node goes on counting each opcode as it retires
(`v4_heat.op[32]`, `v4/src/heat.c`), and the anti-clock. Those counts
are real and are there to be observed. Nothing reads them to decide
anything.
- This amends section 2.7 and the reading in section 1c: a VM's word
patrons do **not** become opcode patrons now.
**Reading, stated to Captain Bob and to be corrected if wrong:** with
opcodes unwired and v4 = v3 functionally, a word on v4 keeps a word's
accounts as v3 has them — its Stadium cell by `stadium_words.c`'s rules,
and the `execution_heat` the ACL reads — taken at the call hook of
section 1f, which is where a v4 node dispatches a word.
## 1i. Asking the kernel; Hera as process manager (ruled 2026-10-05)
**v3, as built:** a kernel word is a C function `void f(VM *vm)` registered
by name (`register_word`); it takes its arguments from the VM's data stack
and leaves its results there. Some interpret text on the same VM while
they run (`EXEC`), saving and restoring the interpreter's state around it.
**The v4 design already has the carrier** (`DECOMPOSITION.md` section 6):
a node's ports are addresses; "a write blocks until the neighbour reads.
This is the GA144 model. No instruction is added."
**Put to Captain Bob:** a node asks the kernel by a blocking write to a
port, served between the node's instructions; a kernel word is an ordinary
dictionary entry whose body stores its request number to the port; kernel
words are made by handing the node text at boot.
**Ruling:** "Yes, and Hera will be the process manager via compudynamics
per node."
The second half is recorded as said and is not yet designed. It bears on
the open question of section 1e — what moves control between nodes, and
when — which `ENGINE.md` leaves to step 6.
## 1j. Where a v4 word's accounts live (ruled 2026-10-05)
**Found:** `struct VM` (`v3/include/vm.h`) is both the record the kernel
keeps for a VM and v3's engine state. The kernel and the physics code work
directly on dictionary records: about 30 source files reach into a
`DictEntry`'s fields, several hundred times. The heartbeat's decay walks
the record list by `link` and resumes by `word_id`; parity hashes it; the
kernel pins `BIRTH` by setting two of its fields. A v3 word's accounts are
that C record: `execution_heat`, `physics`, the four ACL fields, the
transition metrics, the word ID. A v4 word has no such record.
**Put to Captain Bob:**
A — the code is the node's and the accounts are the kernel's, joined by
word ID: for each word on a node the kernel keeps v3's own `DictEntry`,
every account field as it is; the node tells the kernel when a word is
defined or forgotten; at each `call` the kernel does on the record what
v3's inner loop does; v3's physics, heartbeat, ACL words, Stadium word
layer and parity run on the records unchanged.
B — the accounts go into each entry's header in the node's memory, and
the 30 files change to reach them through accessors.
**Ruling:** "A is good."
Noted with it: on the FPGA a node's counters are meant to be in the fabric,
not in a kernel record. A is for v4 = v3 now.
## 2. Row 3: physics and per-word heat
### 2.1 What v3 does
**For every word a definition executes** — the inner loop,
`kernel/src/vm/vm_core.c:757-880`, and the same on the interpreter's path at
`:1053-1110`:
1. **Decay (Loop #3).** The word's heat is reduced by the ticks since it was
last touched times the decay slope, in Q48.16
(`physics_metadata_apply_linear_decay`, `v3/src/physics_metadata.c`). Not
for a frozen word, and not while L8 has gated Loop #3 off.
2. **Heat (Loop #1).** The word's `execution_heat` is raised by one.
3. **Stadium.** The word's ID, the VM's ID and the tick go to the Stadium's
conserved heat wire (`stadium_word_dispatch`).
4. **Rolling window (Loop #2).** The word's ID is recorded
(`rolling_window_record_execution`).
5. **Pipelining (Loop #4).** The transition from the previous word is
recorded, the most likely next word is worked out, and a likely one is
promoted to the hot-words cache.
6. **ACL.** The word's TTL is counted down, or the word rechecked at zero; a
denied word stops the definition.
7. The word runs. Then its physics record is touched (temperature, time) and
the heartbeat's count of words executed is raised.
**On every heartbeat tick** — `kernel/src/vm/vm_runtime.c`, driven by the
timer interrupt: `vm_tick`; the window tuner (Loop #5, `:158`); the slope
validator (Loop #6, `:228`); background decay (`:355`); the L8 mode update
(`:400`), which turns loops on and off; the inference engine (`:579`); and a
published snapshot.
**What each word carries** — `DictEntry`, `v3/include/vm.h:342-358`:
`execution_heat`; a `DictPhysics` record (temperature, last decay tick, mass,
state flags); a pointer to its transition metrics; a stable `word_id`; the
four ACL fields; and the `WORD_FROZEN` and `WORD_PINNED` flags.
All of this is running before the first prompt. POST itself executes under
it.
### 2.2 What v4 has
- A v4 word is native code. Executing one is the `call` opcode going to its
address. There is no inner loop, so there is no place in software where
v3's seven steps could happen.
- Short words are not called at all. `DUP`, `+`, `@`, `>R` and about fifty
others are compiled in line as opcodes (`header DUP inline`,
`v4/capsule/forth.v4`). At run time nothing says "this was `DUP`".
- The engine counts three things as instructions retire (`v4/src/exec.c`):
each opcode's count (32 counters), the anti-clock, and a count for each
call target.
- **The call-target count is a stand-in, by its own account.** It is a C
array beside the node, 4096 entries, written 2026-10-02
(`v4/src/heat.c`). A call to an address of 4096 or above is silently not
counted. The nucleus runs to about word 6300 and capsule definitions start
at 8192, so some assembled words and every capsule word get nothing. No
FORTH word can read it: `ACL-HEAT@` returns 0 (`v4/capsule/acl.v4:80`).
- A dictionary entry is three cells before the code — flags, name, link
(`v4/capsule/dict.v4`). It has no heat, no physics record, no word ID.
- There is no decay, rolling window, pipelining, inference, L8, heartbeat
or Stadium wire on the node.
### 2.3 What the v4 design documents say
The documents ruled on 2026-10-01 do not say "as v3". They say:
- `JUSTIFICATION.md` section 4, item 4: "Compudynamics as a side effect of
execution. Heat counters, the anti-clock, and the heartbeat are driven by
instruction retirement in hardware. They cost no instructions."
- `DECOMPOSITION.md` section 1.4: "every `call` increments the heat counter
of its target".
- `DECOMPOSITION.md` section 7: registers `HEAT-OP[0..31]`,
`HEAT-CALL[...]`, `ANTICLOCK`, `HEARTBEAT`, `GOV-*`.
- `DECOMPOSITION.md` sections 5.19 and 5.23 use the words "heat table" for
`HEAT-CALL` (`ENTROPY@`, `FREEZE-WORD`).
- D-6, **deferred**: "Which heat structures exist in hardware: per-opcode
counters only, or also per-call-target and word-to-word transition
counters. … The golden model implements per-opcode and per-call-target
heat in the meantime."
- Section 5.22: the six `PIPELINING-*` words are retired, to return "if D-6
adds transition counters". Section 5.21: the hot-words cache words are
retired as having no equivalent on a node.
So the 4096-entry array is the "in the meantime" of D-6, and "heat table"
is these documents' own term.
### 2.4 The conflict
Today's ruling and those documents do not say the same thing, in three
places. Each needs a ruling; none is mine to settle.
**A. Where a word's heat is kept.** v3: in the word's own dictionary entry.
The documents: in the node, as a register indexed by call target.
**B. What counts as a word.** v3 counts every execution of `DUP` as
`DUP`'s. v4 compiles `DUP` in line; what is counted is the `dup` opcode,
and a word built of several opcodes in line (`2DUP` is `over over`) leaves
no count of its own. v3's per-word heat for these words cannot be had from
a v4 node as it compiles now.
**C. Which loops exist.** v3 runs all seven and L8 before the prompt. The
documents retire Loop #4 (pipelining) and the hot-words cache, and leave
the rest to registers whose behaviour is not written down beyond their
names.
### 2.5 What each answer would take
Offered so that the ruling can be made knowing its cost. Not a plan.
**If heat is kept as v3 keeps it (A, v3's way).** Each entry gains cells
before its code, beside flags, name and link: heat, last-decay tick, word
ID, and what of `DictPhysics` is kept. `dict.v4`'s entry words, the header
macro of the text assembler and every assembled `header` line account for
them. The node, on `call`, finds the entry from the target address and does
steps 1 to 5 of section 2.1. Heat is then ordinary memory: `HEAT@` is a
fetch, and it is inside the dictionary, so the dictionary hash either
covers it or is defined to skip it, as v3's canonical hash skips it.
**If heat is kept as the documents have it (A, the node's way).** The
call-target counts become part of the node, cover every address of it, and
are readable through `HEAT-CALL` as a memory-mapped register, which needs
D-4, the memory map, settled. This is not "as v3" in where heat lives; it
is in what is counted.
**B either way.** Three ways, and they are not equivalent:
1. In-line words are counted by opcode: `DUP`'s heat is `HEAT-OP[dup]`.
Words that are more than one opcode in line have no heat. Not as v3.
2. Nothing is compiled in line except what must be (`>R`, `R>`, `R@`, `I`,
`J`, `LEAVE`: a call would bury the return address). Every other word is
called and so counted. As v3 for those words; slower, and the six that
must stay in line are still uncounted.
3. The compiler lays down a marker the node counts and does not execute.
That is a new opcode or a new use of one, which is a change to the
instruction set.
**C.** For each of Loops #2 to #7 and L8: where its state lives, what
advances it, and what the heartbeat is on a node that has no timer of its
own (v3's is the kernel's timer interrupt; the documents' is "in the
fabric"). v3's code for each is the reference: `rolling_window_of_truth.c`,
`physics_pipelining_metrics.c`, `inference_engine.c`, `ssm_jacquard.c`,
`vm_runtime.c`.
### 2.6 What depends on this
- **Decomposition.** Moving a word from assembler to a colon definition
does not change how it is called, but it turns in-line opcodes into
calls, which changes what is counted under every answer to B. It should
wait for B.
- **ACL.** v3's TTL comes from heat (`ACL-TTL-COMPUTE`). With `ACL-HEAT@`
returning 0 every word has the same TTL. Row 8 waits for A.
- **POST's other 500 cases** (row 4) include the physics-freeze and
inference modules, which test words that read and write heat.
- **K.** Nothing computes K on v4. It is a figure over heat; it waits for A
and C.
### 2.7 Ruling (Captain Bob, 2026-10-05)
> One more time: v4 = v3 functionally. We will use [heat accumulation] on
> 32 opcodes rather than words. Before, a word was our smallest unit; now
> it is the opcode.
This settles section 2.4:
- **A.** Heat is kept per opcode: 32 accumulators in the node. Not in a
word's dictionary entry, and not per call target.
- **B.** The unit is the opcode. Whether a word is called or compiled in
line makes no difference to what is counted. `call` is one of the 32 and
is counted as itself.
- **C.** v4 = v3 functionally, so every loop v3 runs before its prompt
runs on v4, over opcodes where v3's runs over words.
What follows from it, and is not yet done:
- The per-call-target array (`v4/src/heat.c`, `call[]`, the freeze mask,
`v4_heat_on_call`) has no place and is to be removed, with `HEAT-CALL`
in `DECOMPOSITION.md` sections 1.4, 5.19, 5.23 and 7. D-6 is answered:
per-opcode.
- Sections 5.21 and 5.22 of `DECOMPOSITION.md` retire pipelining and the
hot-words cache. Under C they are not retired.
- Decomposition no longer waits on this: moving a word from assembler to a
colon definition changes which opcodes run, and those are counted
wherever the code is.
Still open, for the words v3 keys by word and v4 must key by something:
- **ACL.** Access control is per word, and v3 works a word's TTL out from
that word's heat (`ACL-TTL-COMPUTE`). A v4 word has no heat of its own.
- **Freeze and pin.** v3 freezes or pins a word's heat (`FREEZE-WORD`,
`WORD_PINNED`). In v4 what is frozen is an opcode, or nothing.
- **Hot-words cache.** v3 promotes a hot word so that it is found faster.
A v4 opcode is not looked up.
- **Heartbeat.** What ticks it on a node (v3: the kernel's timer
interrupt).
## 3. Stand-ins in v4, as found 2026-10-05
Each describes itself as one. None has been changed.
| What | Where | Says of itself |
|---|---|---|
| Per-call-target heat | `v4/src/heat.c`, `v4/include/v4/heat.h` | "in the meantime" (D-6) |
| `ACL-HEAT@` | `v4/capsule/acl.v4:80` | returns 0; "not readable from a programme yet" |
| Console | `v4/include/v4/node.h:136`, `:156` | "the model stands in for the console" until the mesh exists |
| Block storage | `v4/include/v4/node.h:248`, D-19 | "the model stands in" until the mesh carries the storage service |
| Memory map | `v4/tests/host_map.h`, D-4 deferred | a test header the product image is built from |
| CA public key | `v4/capsule/ACL.fth:40` | "placeholders, as in v3" |
| Address-dependent POST cases | `v4/tools/post79_rules.py`, `ADDRESS` | check how many values are left, not which |
`NUCLEUS.md` section 3 puts Q48, logging, ACL and the StarForth extensions
out of scope until FORTH-79 is accepted. Under the ruling at the head of
this file they are in scope, since v3 has them before its prompt. That
section is to be corrected once the rows above are ruled.
@@ -0,0 +1,142 @@
# Step 6: Storage Implementation Plan (revised 2026-10-07)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Every v4 node reads and writes blocks by asking the kernel directly, and the kernel answers from v3's block subsystem (MESH.md acceptance 3 as changed 2026-10-07).
**Architecture:** `BLOCK` and its family keep their two buffers; the one word under them makes a kernel request on port 0 (`ENGINE.md` 3.3): block number and buffer address on the data stack, status left in their place. `v4/system/blocks.c` serves the two requests for any node from v3's one block chain. Nothing is routed and no node is told where storage is.
**Tech Stack:** C99 (engine, strict flags), v3's `block_subsystem.c` and `blkio_*.c` (gnu99), the v4 nucleus dialect (`v4/capsule/*.v4`), `make -C v4`, `make -f kernel/Makefile`.
**Spec:** `docs/v4.0.0/MESH.md` section 8 (8.1 to 8.4; 8.6 lists what was withdrawn), section 9's ruling of 2026-10-07, and step 6 in section 10. `docs/v4.0.0/ENGINE.md` 3.3. `docs/v4.0.0/V3-PARITY.md` 1d.
**History:** the first version of this plan built storage as a device speaking messages, with private drives and nodes that had none. Tasks 1 and 2 of it were committed (`0e761cb1`, `4a505a15`) and Tasks 3 and 4 were working in the tree when Captain Bob ruled, on 2026-10-07, that it had left the OS as designed. This version keeps what still serves and removes the rest.
## Global Constraints
- Work on branch `StarForth-v4.0.0` in the main checkout. No new branch, no stash, no worktree.
- Commit and push after every task. End commit messages with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- v4 follows the OS as designed: before building any mechanism, read how v3 does it, and report a departure instead of building it.
- No stubs, no stand-ins. If something cannot be built as intended, stop and report.
- Do not fix anything in v3 that this plan does not name.
- A block is 1024 characters, 256 cells, four characters to a cell, the first lowest; a read leaves the rest of each cell zero and a write takes the low 32 bits.
- The requests: `V4_REQ_BLOCK_READ` is -1 and `V4_REQ_BLOCK_WRITE` is -2, each `( n waddr -- status )`. Status: 0 worked, 2 refused, 3 no such block.
- Node error codes: 13 "Block out of range" (existing), 17 "Storage refused". A block number below 1 is error 13, as it was.
- Before any QEMU run read `.claude/CLAUDE.md` "Running / Acceptance" and the memory note `acceptance-test-rules.md`. One QEMU at a time, in the foreground, `clean` before `qemu`, all three ISAs, logs kept.
- Access (claim, ACL, Stadium touch) is not checked in this step; do not add it.
## Review Focus
1. **A buffer address that is not in the node's memory.** A request whose 256 cells run off the end of memory, or begin below 0. Expected: status 2, no memory touched, the node not harmed. Test in Task 2.
2. **A write that is refused.** `UPDATE` then `SAVE-BUFFERS` on a disk the subsystem will not write. Expected: "Storage refused", the prompt, and the buffer empty so the next `BLOCK` asks again. Test in Task 3.
3. **A request from a node that is not Hera.** An outer node's block request is served; its request for a node (`NODE-BORN`) still is not. Test in Task 5.
4. **Two nodes writing at once.** Expected: every block of both reaches storage. Test in Task 5.
5. **A request number that is neither a block request nor a named word.** Expected: error 12 on the node, as today. Existing test; must still pass.
---
### Task 1: v3's block subsystem — done (`0e761cb1`)
`blk_subsys_init` takes no `VM`; the state is a chain selected through a pointer. Accepted on three v3-configuration boots.
- [ ] **Step 1 (awaits Captain Bob):** with no second chain wanted any more, `blk_chain_new` and `blk_chain_select` serve nothing. Either they are taken out again, leaving only the `VM` argument's removal, with three v3-configuration boots to accept it; or they stay, unused. Do what he rules.
---
### Task 2: The kernel serves block requests
**Files:**
- Delete: `v4/include/v4/storage.h`, `v4/system/storage.c`, `v4/system/store_v3.c`, `v4/tests/test_store.c`; the four `V4_MSG_BLOCK_*` types and `V4_STORE_*` in `v4/include/v4/message.h`
- Create: `v4/include/v4/blocks.h`, `v4/system/blocks.c`, `v4/tests/test_blocks.c`
- Modify: `v4/Makefile` (the storage rules become: v3's block objects and `blocks.c`, built with v3's flags, linked into tests named `test_blocks*.c` and `test_host_*.c`)
**Interfaces:**
- Produces, in `blocks.h`:
```c
#define V4_REQ_BLOCK_READ (-1)
#define V4_REQ_BLOCK_WRITE (-2)
#define V4_BLOCK_OK 0
#define V4_BLOCK_REFUSED 2
#define V4_BLOCK_RANGE 3
/* If `request` is a block request, serve it -- ( n waddr -- status ) on the
* node's data stack -- and return 1; otherwise touch nothing and return 0. */
int v4_blocks_serve(v4_node *n, v4_cell request);
```
- [ ] **Step 1: Write the failing test** `test_blocks.c`: one chain (2080 KiB of RAM and a raw device of 64 blocks) and a bare `v4_node`. Push a number and an address, call `v4_blocks_serve`, pop the status. Checks: a write then a read of block 2050 gives the same 256 cells back with the high bits of each cell zero; block 20 is the fast RAM; block 0, a negative number, 2112 and (at 64 bits) 4294967296 are status 3; an address whose 256 cells run past `V4_NODE_WORDS`, and a negative address, are status 2 with memory unchanged (Review Focus 1); a block written by v3's own `blk_get_buffer`/`blk_update`/`blk_flush` reads back; a request of 5 returns 0 and leaves the stack as it was.
- [ ] **Step 2: Run** `make -C v4 run-64-test_blocks.c`: fails to build, `v4/blocks.h` does not exist.
- [ ] **Step 3: Write `blocks.c`.** It includes `v4/blocks.h`, `v4/node.h` and v3's `block_subsystem.h`. For a read: pop the address and the number; refuse a bad address with `v4_node_addr_ok` on the first and last cell; `blk_is_valid`, then `blk_get_buffer(n, 0)`, and each cell is its four bytes, first lowest. For a write: `blk_get_buffer(n, 1)`, copy the low four bytes of each cell, `blk_update`, `blk_flush`; any of those failing is `V4_BLOCK_REFUSED`. A number below 1 or above 0xFFFFFFFF is `V4_BLOCK_RANGE` without calling v3.
- [ ] **Step 4: Remove the message device** and its test, types and Makefile rules.
- [ ] **Step 5: Run** `make -C v4 run-32-test_blocks.c run-64-test_blocks.c` and the two sanitizer targets: 0 failures.
- [ ] **Step 6: Commit** `feat(v4.0.0): the kernel serves block requests from v3's block chain; the message device is withdrawn` and push.
---
### Task 3: The node asks its kernel
**Files:**
- Modify: `v4/capsule/blocks.v4` (header comment, `(DEVICE)`; `(FIND-BUF)` and `LOAD` get their sign test back), `v4/capsule/core.v4` (error list), `v4/capsule/quit.v4` (message 17; `STORAGE` and message 18 removed)
- Modify: `v4/tests/host_map.h` (`(STORE)` and `(B-TO)` go; the two request numbers come in as constants), `v4/tools/mkimage.c`
- Modify: `v4/tests/test_host_quit.c` (its `kernel_serve` calls `v4_blocks_serve` first; the storage port, its routes and the private-drive cases go)
- Already done in the tree and kept: the four registers removed from `node.h`, `node.c`, `image.h`, `image.c`, `mkimage.c`, `test_node.c`
- [ ] **Step 1: Change the tests first.** In `test_host_quit.c` keep `disk` as the chain's fast RAM, the range checks at 2112 and 3000, and the refused-write case, now on a blank RAM-backed disk of 2048 blocks attached to the one chain after the raw device and never confirmed (its first block is 2112). Remove the cases for no storage, a private drive and a number 32 bits do not hold being reached; `-1 BLOCK` is "Block out of range" at both widths again.
- [ ] **Step 2: Run** `make -C v4 run-64-test_host_quit.c`: fails, the nucleus still asks by message.
- [ ] **Step 3: `(DEVICE)`:**
```
\ ( command i -- ) read buffer i's block from storage, command 1, or
\ write it, command 2: a request of this node's kernel ( n waddr -- status ).
: (DEVICE)
dup push SWAP push \ i R: i command
dup (B) + a! @ SWAP (BUF) \ n waddr
pop -1 + if RD drop BLOCK-WRITE# jump ASK
RD: drop BLOCK-READ#
ASK: (PORT) b! !b \ status
pop SWAP \ i status
if FINE
push (B) + a! 0 !+ 0 ! pop \ nothing is in the buffer
-2 + if E17 drop NODE-ERROR b! 13 !b ;
E17: drop NODE-ERROR b! 17 !b ;
FINE: drop drop ;
```
to be proven by Step 1's tests. Remove `(B-ASK)`, `(B-DATA)`, `(B-AWAIT)`, `(B-END)` and `STORAGE`.
- [ ] **Step 4: Run** `make -C v4 run-32-test_host_quit.c run-64-test_host_quit.c run-64-test_node.c`: 0 failures. Go straight on; Tasks 3, 4 and 5 commit together.
---
### Task 4: The lone node
**Files:** `v4/include/v4/boot.h`, `v4/system/boot.c`, `v4/tools/hosted.c`, `v4/Makefile`
- [ ] **Step 1:** In `boot.c` a request on the kernel port is offered to `v4_blocks_serve` before the host's named words. Remove the storage port, `STORAGE_TOLD` and the `storage` member of `v4_boot`. `v4_boot_run(const v4_boot *b)` keeps its one argument.
- [ ] **Step 2:** `hosted.c` calls `sf_time_init` and `blk_subsys_init` on 2080 KiB of static RAM, as hosted v3 does with no disk, and nothing else.
- [ ] **Step 3: Run** `make -C v4 hosted-check`: `POST: PASSED`, 538 of 538, on the three ISAs.
---
### Task 5: The unit of five
**Files:** `v4/tests/test_host_unit.c`, `capsules/v4/hera.4th`
- [ ] **Step 1: Write the failing checks.** Hera's `BIRTH` no longer sends POST: after `10 UNIT`, exactly one POST tally has been seen, Hera's, and the four parities and their one dictionary hash are as before. Node 12 writes block 2100 and Hera, 11, 13 and 14 each read it. An outer node asked to do `NODE-BORN` is refused, as before (Review Focus 3). Hera sets node 12 writing ten blocks and writes ten of her own meanwhile, and all twenty reach the chain (Review Focus 4).
- [ ] **Step 2: Run** and see them fail. Then: in `host_born`, every node born gets the kernel on its port 0 — a device whose `take` serves `v4_blocks_serve` for that node and refuses everything else; Hera's existing kernel device does the same before it serves her own requests. Remove the two message devices, the storage routes and `STORAGE` lines. In `hera.4th`, `BIRTH` sends `v4:forth79.4th` and not `v4:post79.4th`; keep every block to 16 lines of 64 characters and run `build/tools/mkcapsule --lint capsules/`.
- [ ] **Step 3: Run** `make -C v4 run-64-test_host_unit.c`: 0 failures. Then `make -C v4 -k test`, `make -C v4 sanitize`, `make -C v4 hosted-check`.
- [ ] **Step 4: Commit** Tasks 3, 4 and 5: `feat(v4.0.0): every node asks the kernel for its blocks; a born node is not POSTed` and push.
---
### Task 6: Bare metal
**Files:** `kernel/src/kernel_main.c:518-523,620-668`, `kernel/src/v4/sk_v4.c`, `kernel/Makefile:616-624`
- [ ] **Step 1:** Read `kernel_main.c:600-700` and list what the chain's setup needs that has not happened by line 523. Have the chain set up before the v4 node starts, leaving the v3 path's order of events as it is. If something it needs cannot be had by then, report and stop.
- [ ] **Step 2:** `sk_v4.c` loses `sk_v4_disk`; `v4/system/blocks.c` is linked.
- [ ] **Step 3: Acceptance.** The three bare-metal boots. Each log: `PARITY:V4_POST tests=538 pass=538 fail=0`, `POST: PASSED`, the typed session of the previous step, and `2100 BLOCK 1024 BLANK 65 2100 BLOCK C! UPDATE SAVE-BUFFERS EMPTY-BUFFERS 2100 BLOCK C@ .` printing `65`.
- [ ] **Step 4: Commit** with the logs: `feat(v4.0.0): bare metal -- the node's blocks are the kernel's block chain` and push.
---
### Task 7: Write it up
- [ ] **Step 1:** MESH.md step 6 as done, in the manner of step 5; `v4/README.md`; `V3-PARITY.md` 1d (what goes through v3's subsystem now, what is still skipped). Under "not as intended yet": access not checked; POST is still a capsule Hera loads until the kernel holds its cases.
- [ ] **Step 2: Commit** `docs(v4.0.0): storage -- step 6 done` and push.
+138
View File
@@ -0,0 +1,138 @@
# Step 6b: POST Is the Kernel's — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** The kernel holds POST's cases and feeds them to Hera itself; nothing of POST's harness is in her dictionary.
**Architecture:** `mkpost.py` writes a C table of cases instead of a capsule. A runner, `v4/system/post.c`, sends each case's lines to the node through a callback its host supplies, keeps what the node prints, reads the node's data stack from outside, and judges. `boot.c` calls it where it loaded the POST capsule. The two nucleus variables the capsule harness needed go.
**Tech Stack:** C99, Python 3 (`v4/tools/mkpost.py`), the v4 nucleus dialect, `make -C v4`, `make -f kernel/Makefile`.
**Spec:** `docs/v4.0.0/NUCLEUS.md` 6.3 and section 7; acceptance in `docs/v4.0.0/MESH.md` step 6b.
## Global Constraints
- Branch `StarForth-v4.0.0`, main checkout. No new branch, no stash, no worktree. Commit and push after every task; end messages with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- v4 follows the OS as designed: read how v3 does a thing before building it, and report a departure instead of building it.
- No stubs or stand-ins. Do not fix anything in v3.
- The cases are the same 538, with the same lines, as `capsules/v4/post79.4th` has today: the generator's choice of cases and its cutting of lines are not changed.
- What the cases define stays in the dictionary; the system is sealed after POST.
- Boot lines, in order: `PARITY:V4_NUCLEUS`, `PARITY:V4_CAPSULE name=v4:forth79.4th`, `PARITY:V4_POST tests=N pass=N fail=N`, `PARITY:V4_SYSTEM word_count=N dict_hash=0x%016llx`, `PARITY:OK`, `POST: PASSED`.
- Before any QEMU run read `.claude/CLAUDE.md` "Running / Acceptance" and the memory note `acceptance-test-rules.md`. One QEMU at a time, `clean` before `qemu`, all three ISAs, logs kept. Never delete a log.
## Review Focus
1. **A case whose line never ends** (a loop, or a word waiting for the keyboard). Expected: the generator already leaves such cases out; the runner treats a line that does not come back as a failed case and POST as failed, and does not hang the boot silently. Test in Task 2.
2. **A case that prints more than the runner keeps.** Expected: it fails by name; nothing is written past the buffer. Test in Task 2.
3. **A case that leaves more on the stack than the table holds, or a full stack.** Expected: compared by depth first; a mismatch fails the case. Test in Task 2.
4. **An error in the line that sets the state** (`DECIMAL FORTH DEFINITIONS`). Expected: the case fails by name. Test in Task 2.
5. **The dictionary filling during POST**, now that nothing is forgotten. Expected: 538 of 538 with room to spare (measured about 1,290 of 5,632 cells); if a case fails for want of room, stop and report. Checked in Task 4.
---
### Task 1: The generator writes a table
**Files:** Modify `v4/tools/mkpost.py`, `v4/Makefile` (`post79` target's comment); Create `v4/include/v4/post.h`, `v4/system/post_cases.c` (generated); Regenerate `docs/v4.0.0/POST79.md`
**Interfaces — Produces** (`post.h`):
```c
#define V4_POST_STACK 0 /* the stack and the output are compared */
#define V4_POST_DEPTH 1 /* only how many values are left; the output is not compared */
#define V4_POST_DEPTH_OUTPUT 2 /* how many values, and the output */
#define V4_POST_ERROR 3 /* it must end in an error; nothing else is compared */
typedef struct {
const char *name;
const char *const *lines; /* line_count of them */
unsigned line_count;
int expect; /* V4_POST_* */
const long long *stack; /* depth values, the deepest first */
unsigned depth;
const char *output; /* output_len characters */
unsigned output_len;
} v4_post_case;
extern const v4_post_case v4_post_cases[];
extern const unsigned v4_post_case_count;
```
- [ ] **Step 1:** In `mkpost.py`, keep everything up to the choice and cutting of cases as it is. Replace the writing of blocks: for each case keep `lines` with the `T| ` prefix taken off, and the expectation as data (the mode, the stack list, the output string) where `expectation()` now returns harness lines. Write `v4/system/post_cases.c`: one `static const char *const` array of lines and one `static const long long` array of stack values per case, then the table. Escape every character of a line or an output that is not printable ASCII, and `"` and `\`, as octal. Remove `HARNESS` and the block writer; `OUTPUT` becomes the C file. `POST79.md`'s header line says the cases are in `v4/system/post_cases.c`.
- [ ] **Step 2: Check the count before running it.** `grep -c '^T{ ' capsules/v4/post79.4th` is 538. Run `make -C v4 post79`; the report says 538 cases, and `grep -c '^ { "' v4/system/post_cases.c` is 538. If the number differs, stop: the generator's choice of cases changed.
- [ ] **Step 3: Check the lines are the same.** A one-off script in the scratchpad: the `T| ` lines of the old capsule, in order with the prefix removed, equal the table's lines in order. Zero differences.
- [ ] **Step 4:** `cc -std=c99 -Wall -Wextra -Wpedantic -Werror -Iv4/include -DV4_CELL_BITS=64 -c v4/system/post_cases.c -o /dev/null` compiles clean.
- [ ] **Step 5: Commit** `feat(v4.0.0): POST's cases as a table the kernel holds` and push. (The capsule is still there and still what the boot runs.)
---
### Task 2: The runner
**Files:** Create `v4/system/post.c`, `v4/tests/test_host_post.c`; Modify `v4/include/v4/post.h`, `v4/Makefile` (tests named `test_host_post*.c` link `post.c`)
**Interfaces — Produces:**
```c
#define V4_POST_OUT 1024u /* the most of a case's printing that is kept */
typedef struct {
void *self;
v4_node *n; /* the node POST is run on: its data stack is read and emptied */
/* Send the node a line and run it to its end. What it prints is put at
* `out`, at most `cap` characters, and *len is how many it printed in
* all. Returns V4_TEXT_QUIT, V4_TEXT_COMPLETED or V4_TEXT_ERROR
* (message.h), or a negative number if the line did not come back. */
int (*line)(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len);
void (*say)(void *self, const char *text, unsigned len); /* the console */
} v4_post_host;
typedef struct { unsigned tests, pass, fail; } v4_post_tally;
/* Run the cases. Prints a line for each failing case and the PARITY:V4_POST
* line. Returns 1 if none failed. */
int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally);
```
- [ ] **Step 1: Write the failing test** `test_host_post.c`: a host node with the nucleus assembled as `test_host_quit.c` assembles it, a kernel that serves its requests, and a `line` callback built on the same exchange of messages that test's `say` uses, returning the raw text and how the line ended. Cases written in the test, each with the verdict it must get:
- `1 2 +` expecting stack `3`, no output: passes; `5 DUP . . CR` expecting empty stack and `5 5 \n`: passes;
- `1 2 +` expecting `4`: fails; expecting two values: fails (Review Focus 3);
- `65 EMIT` expecting output `B`: fails; expecting `A`: passes;
- `NOSUCHWORD` expecting an error: passes; expecting stack empty: fails;
- `1 2 +` expecting an error: fails;
- a case of two lines, `: PQ 7 ;` then `PQ`, expecting `7`: passes, and afterwards `PQ` is still a word on the node;
- a depth-only case `HERE` expecting depth 1: passes; depth 2: fails;
- a case printing 2,000 characters: fails by name, and the byte after the runner's buffer is untouched (Review Focus 2);
- a `line` callback that returns -1 for one case: that case fails, the rest are still judged, and the run returns 0 (Review Focus 1);
- with `BASE` left at 16 and a non-empty stack before a case, the case still sees decimal and an empty stack;
- a callback that makes the state line end in an error: the case fails by name (Review Focus 4).
The tally and the `PARITY:V4_POST tests=N pass=N fail=N` line match the count; each failing case is named in a `POST FAIL: ` line; no passing case's output reaches `say`.
- [ ] **Step 2: Run** `make -C v4 run-64-test_host_post.c`: fails to build, there is no `v4_post_run`.
- [ ] **Step 3: Write `post.c`.** Nothing from the C library (the kernel links it). For each case: `v4_dstack_reset`; the state line, which must complete; each line in turn, the output appended to one buffer of `V4_POST_OUT`, an error on any line remembered; then the verdict:
```c
if (c->expect == V4_POST_ERROR) ok = errored && came_back;
else if (errored || !came_back) ok = 0;
else {
ok = n->ds.depth == c->depth;
if (ok && c->expect == V4_POST_STACK)
for (k = c->depth; ok && k-- > 0; ) ok = v4_dstack_pop(&n->ds) == (v4_cell)c->stack[k];
if (ok && c->expect != V4_POST_DEPTH)
ok = total == c->output_len && total <= V4_POST_OUT && same(out, c->output, total);
}
```
Read the stack before anything is popped when printing a failing case's `stack<...>`. `v4_dstack_reset` after every case.
- [ ] **Step 4: Run** at 64 and 32 bits and under the sanitizers: 0 failures.
- [ ] **Step 5: Commit** `feat(v4.0.0): the POST runner -- the kernel feeds a case and judges it from outside` and push.
---
### Task 3: The boot runs it, and the hooks go
**Files:** Modify `v4/system/boot.c` (the POST capsule out of `boot_capsules`; `post_watch` and its variables out; a line function that keeps output; the call of `v4_post_run`; the `PARITY:V4_SYSTEM` line), `v4/Makefile` (`SYSTEM_SRCS`), `kernel/Makefile:623-624` (`post.o`, `post_cases.o`), `v4/capsule/core.v4` (`EMIT`), `v4/capsule/quit.v4` (`(DONE)`, the two headers), `v4/tests/host_map.h`, `v4/tests/test_host_unit.c`; Delete `capsules/v4/post79.4th`
- [ ] **Step 1: Tests first.** In `test_host_unit.c`, Hera's POST becomes `v4_post_run` with a `line` callback built on the test's `tell`, and the check is the tally: 538, 538, 0. Add: after it, `T{` on Hera is an unknown word; `RS1` (defined by case `>R.basic`) is a word on Hera and still is after `COLD`. In `test_host_quit.c` add that `(CATCH)` and `(EMIT-HOOK)` are unknown words. Run and see them fail.
- [ ] **Step 2: `boot.c`.** Split `v4_boot_line` so that its loop takes where the output goes; `v4_boot_line` passes the console and POST's callback passes a buffer. After the capsules: `v4_post_run`; on a failure `PARITY:FAIL`, `POST: FAILED`, return 0. Then `v4_image_seal`, then `PARITY:V4_SYSTEM word_count=` (the count of FORTH's words that `PARITY:V4_NUCLEUS` already uses) ` dict_hash=`, then `PARITY:OK`, `POST: PASSED`. `V4_POST_AT_BOOT=0` still leaves POST out.
- [ ] **Step 3: The nucleus.** `EMIT` loses its first line and the `NONE:` label; `(DONE)` loses the store to `(EMIT-HOOK)` and the `(CATCH)` branch, so an error always ends `2 jump (FINISH)`; the two `header` lines, the two constants in `host_map.h` and their comments go.
- [ ] **Step 4:** `git rm capsules/v4/post79.4th`; remove it from `host_open` in `test_host_unit.c`. `v4/build/mkcapsule --lint capsules/` is clean.
- [ ] **Step 5: Run** `make -C v4 clean`, then `test`, `sanitize`, `hosted-check`. Update `hosted-check`'s greps in `v4/Makefile` if they name the POST capsule's line. All pass; the three hosted programs print the same `PARITY:V4_SYSTEM` line.
- [ ] **Step 6: Acceptance 4 on the product.** With one case's expected stack changed by hand in a scratch copy of `post_cases.c` built into a scratch hosted binary (not committed), the boot names the case, prints `PARITY:FAIL` and `POST: FAILED`, and gives no prompt.
- [ ] **Step 7: Commit** `feat(v4.0.0): POST is the kernel's -- the boot feeds the cases, and nothing of the harness is on the node` and push.
---
### Task 4: Bare metal, and the write-up
- [ ] **Step 1:** Three v4 boots with the typed session of step 6 and, added to it, `T{`, `RS1`, `COLD`, `RS1`, `HERE .`. Each log: POST 538 of 538, the `PARITY:V4_SYSTEM` line equal to hosted's, `T{` unknown, `RS1` printing `42 42` before and after `COLD`. If POST fails for want of dictionary room, stop and report (Review Focus 5).
- [ ] **Step 2:** `MESH.md` step 6b as done; `NUCLEUS.md` section 8's list; `v4/README.md` "POST"; `POST79.md` as regenerated.
- [ ] **Step 3: Commit** with the three logs, `feat(v4.0.0): POST is the kernel's -- bare metal, and written up`, and push.
+4 -2
View File
@@ -144,8 +144,10 @@ which block slot to fill.
1. The first line of each logical block must be `Block NNNN` (capital B,
single space, decimal integer).
2. Block numbers must be unique within a single capsule file, and must fall
in `[2048, 5120)`.
2. Block numbers must be unique within a single capsule file, and must be
2048 or more. Blocks 0–2047 are the VM's fast RAM and the only ones a
capsule may not claim; there is no upper bound (ruled 2026-10-05,
`docs/v4.0.0/NUCLEUS.md` §5.2 — previously `[2048, 5120)`).
3. Blocks are loaded in file order and executed top-to-bottom.
4. Each block can hold at most 16 content lines, each at most 64 characters
long (`validate_forth_blocks` in `tools/mkcapsule.c`, corrected 2026-09-19
+251 -99
View File
@@ -1,13 +1,13 @@
# ==============================================================================
# Makefile.starkernel — StarKernel UEFI Build System
# kernel/Makefile — StarKernel UEFI Build System
# Multi-architecture: amd64 (x86_64), aarch64 (ARM64), riscv64 (RISC-V 64)
#
# Quick start:
# make -f Makefile.starkernel — build for host arch
# make -f Makefile.starkernel ARCH=amd64 qemu — build + boot (x86_64)
# make -f Makefile.starkernel ARCH=aarch64 qemu — build + boot (aarch64)
# make -f Makefile.starkernel ARCH=riscv64 qemu — build + boot (riscv64)
# make -f Makefile.starkernel help — show all targets
# make -f kernel/Makefile — build for host arch
# make -f kernel/Makefile ARCH=amd64 qemu — build + boot (x86_64)
# make -f kernel/Makefile ARCH=aarch64 qemu — build + boot (aarch64)
# make -f kernel/Makefile ARCH=riscv64 qemu — build + boot (riscv64)
# make -f kernel/Makefile help — show all targets
# ==============================================================================
# ==============================================================================
@@ -60,11 +60,14 @@ $(eval $(call kconfig_bool,PARITY_MODE,0))
# StarForth VM integration (default: enabled)
$(eval $(call kconfig_bool,STARFORTH_ENABLE_VM,1))
# StarForth v4 at a single prompt in place of the v3 VM (default: off)
$(eval $(call kconfig_bool,STARFORTH_V4,0))
# Monolithic build — loader + kernel compiled together (default: enabled)
MONOLITHIC ?= 1
# Parallel build — use all available cores when not cleaning
# (adding -j when 'clean' is also a goal causes a race on include/version.h)
# (adding -j when 'clean' is also a goal causes a race on v3/include/version.h)
NPROC := $(shell nproc 2>/dev/null || echo 4)
ifeq ($(filter clean,$(MAKECMDGOALS)),)
MAKEFLAGS += -j$(NPROC)
@@ -106,9 +109,9 @@ KERNEL_OBJ_DIR := $(OBJ_DIR)/kernel
LOADER_EFI := $(BUILD_DIR)/starkernel_loader.efi
KERNEL_ELF := $(BUILD_DIR)/starkernel_kernel.elf
KERNEL_SRC := src/starkernel
KERNEL_INC := include/starkernel
STARFORTH_CONFIG_HEADER := include/starforth_config.h
KERNEL_SRC := kernel/src
KERNEL_INC := kernel/include/starkernel
STARFORTH_CONFIG_HEADER := v3/include/starforth_config.h
QEMU_LOG_DIR := logs
QEMU_ISO := $(BUILD_DIR)/starkernel.iso
@@ -129,17 +132,17 @@ ARTDISK ?= disk/artemis.img
# headroom above the current 9-device identity roster (Zuse + 8 minted identities)
# rather than matching it exactly, per Bob's standing ruling against hardcoding a bound
# to today's scale (FABRIC-3.md §VII.4). Override if a future test needs more, e.g.
# `make -f Makefile.starkernel qemu XHCI_PORTS=32`.
# `make -f kernel/Makefile qemu XHCI_PORTS=32`.
XHCI_PORTS ?= 16
# ZUSEDISK -- Zuse's own minted USB thumbdrive, attached by default so a
# plain `make qemu` lands already authenticated into the Zuse identity
# (Captain Bob, 2026-08-28). Attached at QEMU launch time via -drive/
# -device on the qemu-xhci controller each arch's qemu target already
# creates; xhci_bringup()'s initial port scan (src/starkernel/usb/xhci.c)
# creates; xhci_bringup()'s initial port scan (kernel/src/usb/xhci.c)
# is what makes an already-connected-at-launch device visible at all --
# xhci_poll_events() alone is purely hotplug-event-driven and would never
# see a device present before controller reset. Override to "" (empty) to
# boot without Zuse attached, e.g. `make -f Makefile.starkernel qemu ZUSEDISK=`.
# boot without Zuse attached, e.g. `make -f kernel/Makefile qemu ZUSEDISK=`.
ZUSEDISK ?= disk/thumbdrives/zuse-thumb-ident.img
# Precomputed (not inlined as $(if ...,...)) because the drive/device specs
# below are comma-heavy and GNU make's $(if) function splits its own
@@ -153,10 +156,10 @@ else
ZUSEDISK_QEMU_ARGS :=
endif
MKCAPSULE_SRC = tools/mkcapsule.c tools/pkcs8_ed25519.c \
src/starkernel/crypto/ed25519.c \
src/starkernel/crypto/fe25519.c \
src/starkernel/crypto/scalar25519.c \
src/starkernel/crypto/sha512.c
kernel/src/crypto/ed25519.c \
kernel/src/crypto/fe25519.c \
kernel/src/crypto/scalar25519.c \
kernel/src/crypto/sha512.c
MKCAPSULE_BIN = $(BUILD_DIR)/tools/mkcapsule
# Milestone 6 (Phase 8): the snakeoil intermediate's private key, generated
# offline outside this repo entirely (see FABRIC-2.md's Phase 8 §Milestone 6
@@ -169,6 +172,28 @@ CAPSULE_GENERATED = $(BUILD_DIR)/capsule_generated.c
CAPSULE_GENERATED_OBJ = $(BUILD_DIR)/capsule_generated.o
CAPSULE_GENERATED_KOBJ = $(KERNEL_OBJ_DIR)/capsule_generated.o
# ==============================================================================
# BOARD (boot_image)
# ==============================================================================
# BOARD names a directory under boards/ (ser5, raspi, milkv, ...). Its
# board.mk sets BOARD_ARCH, BOARD_BOOT (how the firmware finds us) and
# BOARD_IMAGE (the single file written to the boot medium). The root
# Makefile's `make boot_image TARGET=<NAME>` maps NAME to BOARD and passes
# ARCH=$(BOARD_ARCH); the kernel objects themselves are the ordinary
# build/$(ARCH)/kernel ones, shared with `make qemu`.
BOARD ?=
ifneq ($(strip $(BOARD)),)
ifeq ($(wildcard boards/$(BOARD)/board.mk),)
$(error Unknown BOARD='$(BOARD)': no boards/$(BOARD)/board.mk)
endif
include boards/$(BOARD)/board.mk
ifneq ($(BOARD_ARCH),$(ARCH))
$(error BOARD=$(BOARD) is ARCH=$(BOARD_ARCH), but ARCH=$(ARCH) was requested)
endif
BOARD_OUT := build/boards/$(BOARD)
endif
BOOT_IMAGE_SIZE_MIB ?= 128
# ==============================================================================
# TOOLCHAIN
# ==============================================================================
@@ -195,8 +220,8 @@ ifeq ($(ARCH),amd64)
# correctly. These flags come after COMMON_CFLAGS on the command line so
# they win for amd64 only; riscv64/aarch64 keep -fPIC.
ARCH_CFLAGS := -m64 -march=x86-64 -mno-red-zone -DARCH_AMD64 -fno-pic -fno-pie
LOADER_LINKER_SCRIPT := linker/starkernel-loader-amd64.ld
KERNEL_LINKER_SCRIPT := linker/starkernel-kernel-amd64.ld
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-amd64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-amd64.ld
else ifeq ($(ARCH),aarch64)
ifneq ($(shell which aarch64-linux-gnu-gcc 2>/dev/null),)
@@ -211,14 +236,16 @@ else ifeq ($(ARCH),aarch64)
$(error No ARM64 toolchain found. Install aarch64-linux-gnu-gcc or aarch64-none-elf-gcc)
endif
# aarch64 loader uses clang + lld-link (COFF target — GCC cannot produce aarch64 PE)
LOADER_CC := clang-18
# Debian/Ubuntu package only installs the versioned lld-link-18 on PATH by
# default (unversioned lld-link lives under /usr/lib/llvm-18/bin, which
# Any clang with the aarch64 COFF backend works; the version is not
# pinned (Ubuntu 26.04 ships clang-21, older hosts clang-18).
LOADER_CC := $(shell which clang 2>/dev/null || ls /usr/bin/clang-[0-9]* 2>/dev/null | sort -V | tail -1 || echo clang)
# Debian/Ubuntu package only installs the versioned lld-link-NN on PATH by
# default (unversioned lld-link lives under /usr/lib/llvm-NN/bin, which
# isn't on PATH unless explicitly prepended) — detect whichever resolves.
LOADER_LD := $(shell which lld-link 2>/dev/null || which lld-link-18 2>/dev/null || echo lld-link)
LOADER_LD := $(shell which lld-link 2>/dev/null || ls /usr/bin/lld-link-[0-9]* /usr/lib/llvm-*/bin/lld-link 2>/dev/null | sort -V | tail -1 || echo lld-link)
ARCH_CFLAGS := -march=armv8-a -mcpu=cortex-a72 -DARCH_AARCH64 -mno-outline-atomics
LOADER_LINKER_SCRIPT := linker/starkernel-loader-aarch64.ld
KERNEL_LINKER_SCRIPT := linker/starkernel-kernel-aarch64.ld
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-aarch64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-aarch64.ld
else ifeq ($(ARCH),riscv64)
ifneq ($(shell which riscv64-unknown-elf-gcc 2>/dev/null),)
@@ -233,8 +260,8 @@ else ifeq ($(ARCH),riscv64)
$(error No RISC-V toolchain found. Install riscv64-unknown-elf-gcc or riscv64-linux-gnu-gcc)
endif
ARCH_CFLAGS := -march=rv64gc -mabi=lp64d -DARCH_RISCV64
LOADER_LINKER_SCRIPT := linker/starkernel-loader-riscv64.ld
KERNEL_LINKER_SCRIPT := linker/starkernel-kernel-riscv64.ld
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-riscv64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-riscv64.ld
endif
# Default loader toolchain = kernel toolchain (overridden for aarch64 above)
@@ -260,13 +287,13 @@ COMMON_CFLAGS := \
-ffreestanding -nostdlib -fno-builtin \
-fPIC -fvisibility=hidden -fno-stack-protector -fshort-wchar \
$(ARCH_CFLAGS) \
-I$(KERNEL_INC) -Iinclude -Isrc \
-I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
-include $(STARFORTH_CONFIG_HEADER) \
-DPARITY_MODE=$(PARITY_MODE)
VMCORE_CFLAGS_COMMON := \
$(filter-out -I$(KERNEL_INC),$(COMMON_CFLAGS)) \
-Iinclude -I. -Isrc/word_source -Isrc/test_runner/include \
-Iv3/include -Ikernel/include -I. -Iv3/src/word_source -Iv3/src/test_runner/include \
-Wno-error=unused-parameter -Wno-error=shift-negative-value \
-Wno-error=sign-compare -Wno-error=missing-field-initializers
@@ -281,8 +308,8 @@ LOADER_BASE_CFLAGS := \
-mno-stack-arg-probe \
-march=armv8-a \
-DARCH_AARCH64 \
-I$(KERNEL_INC) -Iinclude -Isrc \
-Iinclude/starkernel/freestanding \
-I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
-Ikernel/include/starkernel/freestanding \
-include $(STARFORTH_CONFIG_HEADER) \
-DPARITY_MODE=$(PARITY_MODE) \
-DPLATFORM_TIME_NO_INLINE
@@ -308,7 +335,7 @@ ifeq ($(ARCH),aarch64)
else ifeq ($(ARCH),riscv64)
LOADER_LDFLAGS_PE := # ELF→objcopy pipeline in link rule
else
LOADER_LINKER_SCRIPT_PE := linker/starkernel-loader-amd64-pe.ld
LOADER_LINKER_SCRIPT_PE := kernel/linker/starkernel-loader-amd64-pe.ld
LOADER_LDFLAGS_PE := \
-T $(LOADER_LINKER_SCRIPT_PE) -m i386pep -nostdlib \
--enable-reloc-section --image-base 0 --subsystem 10 -e efi_main
@@ -429,7 +456,7 @@ LOADER_CFLAGS += $(VM_FEATURE_OVERRIDES)
endif
# SK_CMD — optional startup FORTH script injected before the interactive REPL.
# Usage: make -f Makefile.starkernel qemu SK_CMD="TIME-TICKS . BYE"
# Usage: make -f kernel/Makefile qemu SK_CMD="TIME-TICKS . BYE"
ifdef SK_CMD
KERNEL_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
LOADER_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
@@ -439,7 +466,7 @@ endif
# The loader reads this file from the EFI partition and parses it at boot.
# Supported flags: --doe --log-level=<debug|info|warn|error>
# --stack=<N>[MG] --heap=<N>[MG]
# Usage: make -f Makefile.starkernel qemu KERNEL_ARGS="--doe --log-level=info"
# Usage: make -f kernel/Makefile qemu KERNEL_ARGS="--doe --log-level=info"
KERNEL_ARGS ?=
# ==============================================================================
@@ -534,19 +561,19 @@ KERNEL_ASM := $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.S)
ifeq ($(STARFORTH_ENABLE_VM),1)
VM_ALL_SRCS := \
$(wildcard src/*.c) \
$(wildcard src/word_source/*.c) \
$(wildcard src/test_runner/*.c) \
$(wildcard src/test_runner/modules/*.c)
$(wildcard v3/src/*.c) \
$(wildcard v3/src/word_source/*.c) \
$(wildcard v3/src/test_runner/*.c) \
$(wildcard v3/src/test_runner/modules/*.c)
VM_EXCLUDE := \
src/main.c src/cli.c src/repl.c \
src/platform/% \
src/blkio_factory.c src/blkio_file.c \
src/log.c src/doe_metrics.c \
src/vm.c src/vm_core.c src/vm_bootstrap.c src/vm_runtime.c src/vm_time.c \
src/word_source/q48_16_words.c \
src/test_runner/modules/break_me_tests.c
v3/src/main.c v3/src/cli.c v3/src/repl.c \
v3/src/platform/% \
v3/src/blkio_factory.c v3/src/blkio_file.c \
v3/src/log.c v3/src/doe_metrics.c \
v3/src/vm.c v3/src/vm_core.c v3/src/vm_bootstrap.c v3/src/vm_runtime.c v3/src/vm_time.c \
v3/src/word_source/q48_16_words.c \
v3/src/test_runner/modules/break_me_tests.c
VM_CORE_SRCS := $(filter-out $(VM_EXCLUDE),$(VM_ALL_SRCS))
@@ -569,8 +596,32 @@ LOADER_EXTRA_SRCS := \
KERNEL_EXTRA_SRCS := $(LOADER_EXTRA_SRCS)
LOADER_VM_OBJS := $(patsubst src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
KERNEL_VM_OBJS := $(patsubst src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
LOADER_VM_OBJS := $(patsubst v3/src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
KERNEL_VM_OBJS := $(patsubst v3/src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
endif
# ------------------------------------------------------------------------------
# StarForth v4 (STARFORTH_V4=1): the golden model of the F18-derived engine,
# v4/src, and the nucleus image v4's own Makefile builds on this machine from
# v4/capsule (docs/v4.0.0/NUCLEUS.md). The node is the host node's size, with 64-bit cells, as every
# ISA this kernel boots on has (DECOMPOSITION.md D-5), so the image is the
# same file for all three.
# ------------------------------------------------------------------------------
ifeq ($(STARFORTH_V4),1)
V4_DEFS := -DSTARFORTH_V4=1 -Iv4/include \
-DV4_CELL_BITS=64 -DV4_NODE_WORDS=16384 -DV4_DATA_RING=30 -DV4_RET_RING=31
KERNEL_CFLAGS += $(V4_DEFS)
LOADER_CFLAGS += $(V4_DEFS)
V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c)
V4_IMAGE_C := $(BUILD_DIR)/v4_image_64.c
LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
# v4/system/boot.c is the boot the hosted v4 system runs too: nucleus, 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_image.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_image.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)
@@ -581,12 +632,14 @@ LOADER_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ARCH_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ASM)) \
$(LOADER_VM_OBJS) \
$(LOADER_V4_OBJS) \
$(CAPSULE_GENERATED_OBJ)
KERNEL_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_ASM)) \
$(KERNEL_VM_OBJS) \
$(KERNEL_V4_OBJS) \
$(CAPSULE_GENERATED_KOBJ)
# ==============================================================================
@@ -603,7 +656,7 @@ KERNEL_OBJS := \
# MAIN TARGETS
# ==============================================================================
all: include/version.h $(CAPSULE_GENERATED_OBJ) $(CAPSULE_GENERATED_KOBJ) $(LOADER_EFI) $(KERNEL_ELF)
all: v3/include/version.h $(CAPSULE_GENERATED_OBJ) $(CAPSULE_GENERATED_KOBJ) $(LOADER_EFI) $(KERNEL_ELF)
@echo "StarKernel built successfully for $(ARCH)"
@echo " Loader: $(LOADER_EFI)"
@echo " Kernel: $(KERNEL_ELF)"
@@ -611,13 +664,13 @@ all: include/version.h $(CAPSULE_GENERATED_OBJ) $(CAPSULE_GENERATED_KOBJ) $(LOAD
kernel: all
kernel-all:
@$(MAKE) -f Makefile.starkernel ARCH=amd64 TARGET=$(TARGET)
@$(MAKE) -f Makefile.starkernel ARCH=aarch64 TARGET=$(TARGET)
@$(MAKE) -f Makefile.starkernel ARCH=riscv64 TARGET=$(TARGET)
@$(MAKE) -f kernel/Makefile ARCH=amd64 TARGET=$(TARGET)
@$(MAKE) -f kernel/Makefile ARCH=aarch64 TARGET=$(TARGET)
@$(MAKE) -f kernel/Makefile ARCH=riscv64 TARGET=$(TARGET)
clean:
rm -rf build/$(ARCH)/$(TARGET)
rm -f include/version.h
rm -f v3/include/version.h
clean-kernel:
@echo "Cleaning all StarKernel build artifacts..."
@@ -636,8 +689,8 @@ FORCE:
# here after a hosted `make` run silently reuses that stale/wrong-flavored file
# and fails with "LITHOS_VERSION_STR undeclared" deep in kernel_main.c, instead
# of regenerating its own correct version.
include/version.h: FORCE
@mkdir -p include
v3/include/version.h: FORCE
@mkdir -p v3/include
@BUILD_TS=$$(date -Iseconds 2>/dev/null || echo "unknown"); \
printf '#ifndef STARFORTH_VERSION_H\n#define STARFORTH_VERSION_H\n\n' > $@; \
printf '#define STARFORTH_VERSION "%s"\n' "$(VERSION)" >> $@; \
@@ -653,7 +706,7 @@ include/version.h: FORCE
$(MKCAPSULE_BIN): $(MKCAPSULE_SRC)
@mkdir -p $(dir $@)
@echo "HOSTCC $(MKCAPSULE_SRC)"
@cc -std=c99 -Wall -Wextra -O2 -Iinclude -Itools -o $@ $(MKCAPSULE_SRC)
@cc -std=c99 -Wall -Wextra -O2 -Iv3/include -Ikernel/include -Itools -o $@ $(MKCAPSULE_SRC)
# Generate capsule_generated.c from capsules/
CAPSULE_SRCS := $(shell find $(CAPSULES_DIR) -type f ! -name '.*' 2>/dev/null)
@@ -695,12 +748,12 @@ $(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(LOADER_OBJ_DIR)
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
# Compile loader VM core sources
$(LOADER_OBJ_DIR)/vmcore/%.o: src/%.c include/version.h | $(LOADER_OBJ_DIR)
$(LOADER_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
ifeq ($(ARCH),aarch64)
@$(LOADER_CC) $(LOADER_CFLAGS) \
-Iinclude -I. -Isrc/test_runner/include \
-Iv3/include -Ikernel/include -I. -Iv3/src/test_runner/include \
-Wno-error=unused-parameter -Wno-error=shift-negative-value \
-Wno-error=sign-compare -Wno-error=missing-field-initializers \
-Wno-unused-function \
@@ -709,6 +762,44 @@ else
@$(LOADER_CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(LOADER_CFLAGS)) -c $< -o $@
endif
# StarForth v4: the capsule image, built on this machine by v4's Makefile, and
# the engine, compiled like any other kernel source.
ifeq ($(STARFORTH_V4),1)
# The nucleus is a capsule (capsules/v4/nucleus-64.f18), written by the same
# step that writes its description, so the capsule directory waits for it.
$(CAPSULE_GENERATED): $(V4_IMAGE_C)
$(V4_IMAGE_C): FORCE
@mkdir -p $(dir $@)
@echo " V4IMG v4/capsule -> $@"
@$(MAKE) --no-print-directory -C v4 CC=cc build/v4_image_64.c
@cmp -s v4/build/v4_image_64.c $@ || cp v4/build/v4_image_64.c $@
$(LOADER_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
endif
# Compile loader assembly sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@@ -722,10 +813,10 @@ $(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(KERNEL_OBJ_DIR)
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
# Compile kernel VM core sources
$(KERNEL_OBJ_DIR)/vmcore/%.o: src/%.c include/version.h | $(KERNEL_OBJ_DIR)
$(KERNEL_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(KERNEL_CFLAGS)) -Isrc/starkernel/vm -c $< -o $@
@$(CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(KERNEL_CFLAGS)) -Ikernel/src/vm -c $< -o $@
# Compile kernel assembly sources
$(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(KERNEL_OBJ_DIR)
@@ -802,7 +893,7 @@ OVMF_CODE := /usr/share/OVMF/OVMF_CODE_4M.fd
OVMF_VARS_RO := /usr/share/OVMF/OVMF_VARS_4M.fd
# QEMU_DISPLAY — window backend for the framebuffer console (gtk/sdl/none/...).
# Usage: make -f Makefile.starkernel qemu QEMU_DISPLAY=none # headless/CI
# Usage: make -f kernel/Makefile qemu QEMU_DISPLAY=none # headless/CI
QEMU_DISPLAY ?= gtk
# QEMU_EXTRA — extra raw qemu-system-* args appended verbatim to every arch's
@@ -1072,38 +1163,98 @@ else ifeq ($(ARCH),riscv64)
# fallback boot path EFI/BOOT/BOOT<ARCH>.EFI. Storage on real hardware is the
# USB BOT/xHCI path (already live), not virtio.
thumbdrive: all
@mkdir -p $(BUILD_DIR)
@if ! which sgdisk >/dev/null 2>&1; then echo "Error: sgdisk not found. Install gdisk (apt-get install gdisk)."; exit 1; fi
@if ! which mkfs.fat >/dev/null 2>&1; then echo "Error: mkfs.fat not found. Install dosfstools (apt-get install dosfstools)."; exit 1; fi
@case "$(ARCH)" in \
amd64) BOOTNAME="BOOTX64.EFI" ;; \
aarch64) BOOTNAME="BOOTAA64.EFI" ;; \
riscv64) BOOTNAME="BOOTRISCV64.EFI" ;; \
*) echo "Error: no thumbdrive EFI boot name for ARCH=$(ARCH)"; exit 1 ;; \
esac; \
# Geometry: 128 MiB disk (262144 sectors), GPT partition 1 spans sectors \
# 2048..262110 (the GPT last-usable sector for this disk size), i.e. \
# 260063 sectors. The FAT32 ESP image MUST match the partition size \
# exactly; a larger ESP overruns the disk (GPT grows corrupt) or \
# spills past the partition end, both of which made earlier builds \
# unbootable on hardware. \
DISK=$(BUILD_DIR)/starkernel-thumbdrive.img; \
ESP_SECTORS=260063; \
rm -f $$DISK $(BUILD_DIR)/thumbdrive-esp.img; \
dd if=/dev/zero of=$(BUILD_DIR)/thumbdrive-esp.img bs=512 count=$$ESP_SECTORS 2>/dev/null; \
mkfs.fat -F 32 -n "STARKERNEL" $(BUILD_DIR)/thumbdrive-esp.img >/dev/null 2>&1; \
mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI; \
mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI/BOOT; \
mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \
printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $(BUILD_DIR)/thumbdrive-startup.nsh; \
mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh ::/startup.nsh; \
dd if=/dev/zero of=$$DISK bs=1M count=128 2>/dev/null; \
sgdisk -n 1:2048:0 -t 1:ef00 -c 1:"EFI System" $$DISK >/dev/null 2>&1; \
dd if=$(BUILD_DIR)/thumbdrive-esp.img of=$$DISK bs=512 seek=2048 conv=notrunc 2>/dev/null; \
rm -f $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh; \
echo "Thumbdrive image: $$DISK"; \
echo "Write to a USB stick with: dd if=$$DISK of=/dev/sdX bs=4M status=progress"
@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BUILD_DIR)/thumbdrive-startup.nsh
@bash scripts/mkdiskimage.sh --out $(BUILD_DIR)/starkernel-thumbdrive.img --table gpt \
--size-mib 128 --label STARKERNEL \
$(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
$(BUILD_DIR)/thumbdrive-startup.nsh:/startup.nsh
@rm -f $(BUILD_DIR)/thumbdrive-startup.nsh
@echo "Write to a USB stick with: dd if=$(BUILD_DIR)/starkernel-thumbdrive.img of=/dev/sdX bs=4M status=progress"
# ==============================================================================
# BOOT IMAGES (one per board)
# ==============================================================================
# boot_image -- the single file written to a board's boot medium. Driven by
# BOARD (boards/<board>/board.mk); normally invoked from the repo root as
# make boot_image TARGET=SER5|RASPI|MILKV|ZYNQ7020
# Output: build/boards/<board>/$(BOARD_IMAGE).
#
# BOARD_BOOT selects the recipe:
# uefi-esp -- GPT disk, one FAT32 EFI System Partition holding the
# monolithic loader as EFI/BOOT/BOOT<ARCH>.EFI (+ startup.nsh
# for the UEFI shell, + starforth.cfg when KERNEL_ARGS is set).
# Real UEFI firmware (SER5) and U-Boot's bootefi (Milk-V) both
# boot it through the removable-media fallback path.
# rpi-native -- MBR disk, one FAT32 (LBA) partition holding config.txt,
# kernel_2712.img (kernel relinked at 0x80000 and flattened)
# and the stock BCM2712 DTB (+ cmdline.txt, which firmware
# copies into /chosen/bootargs, when KERNEL_ARGS is set).
ifeq ($(ARCH),amd64)
EFI_BOOTNAME := BOOTX64.EFI
else ifeq ($(ARCH),aarch64)
EFI_BOOTNAME := BOOTAA64.EFI
else ifeq ($(ARCH),riscv64)
EFI_BOOTNAME := BOOTRISCV64.EFI
endif
RPI5_NATIVE_LINKER_SCRIPT := kernel/linker/starkernel-native-rpi5.ld
RPI5_NATIVE_ELF := $(BUILD_DIR)/starkernel_rpi5.elf
RPI5_NATIVE_IMG := $(BUILD_DIR)/kernel_2712.img
$(RPI5_NATIVE_ELF): $(KERNEL_OBJS) $(RPI5_NATIVE_LINKER_SCRIPT) | $(BUILD_DIR)
@echo "LD (rpi5 native) $@"
@$(LD) -T $(RPI5_NATIVE_LINKER_SCRIPT) -nostdlib --build-id=none \
-e rpi5_native_start $(KERNEL_OBJS) -o $@
$(RPI5_NATIVE_IMG): $(RPI5_NATIVE_ELF)
@echo "OBJCOPY (raw) $@"
@$(OBJCOPY) -O binary $< $@
.PHONY: boot_image
ifeq ($(strip $(BOARD)),)
boot_image:
@echo "boot_image needs a board. From the repo root:"
@echo " make boot_image TARGET=SER5|RASPI|MILKV|ZYNQ7020"
@exit 1
else ifeq ($(BOARD_BOOT),uefi-esp)
boot_image: all
@mkdir -p $(BOARD_OUT)
@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BOARD_OUT)/startup.nsh
@rm -f $(BOARD_OUT)/starforth.cfg
$(if $(KERNEL_ARGS),@printf '%s\n' '$(KERNEL_ARGS)' > $(BOARD_OUT)/starforth.cfg)
@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table gpt \
--size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
$(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
$(BOARD_OUT)/startup.nsh:/startup.nsh \
$(if $(KERNEL_ARGS),$(BOARD_OUT)/starforth.cfg:/starforth.cfg)
@echo "$(BOARD_DESC)"
@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/sdX bs=4M conv=fsync status=progress"
else ifeq ($(BOARD_BOOT),rpi-native)
$(RPI5_DTB):
@mkdir -p $(dir $@)
@echo " FETCH $(RPI5_DTB_URL)"
@curl -fsSL -o $@.tmp $(RPI5_DTB_URL) || { rm -f $@.tmp; \
echo "Error: could not fetch the Pi 5 DTB. Copy bcm2712-rpi-5-b.dtb from a Raspberry Pi"; \
echo "firmware release and pass RPI5_DTB=/path/to/bcm2712-rpi-5-b.dtb"; exit 1; }
@mv $@.tmp $@
boot_image: v3/include/version.h $(RPI5_NATIVE_IMG) $(RPI5_DTB)
@mkdir -p $(BOARD_OUT)
@rm -f $(BOARD_OUT)/cmdline.txt
$(if $(KERNEL_ARGS),@printf '%s\n' '$(KERNEL_ARGS)' > $(BOARD_OUT)/cmdline.txt)
@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table mbr \
--size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
boards/$(BOARD)/config.txt:/config.txt \
$(RPI5_NATIVE_IMG):/kernel_2712.img \
$(RPI5_DTB):/bcm2712-rpi-5-b.dtb \
$(if $(KERNEL_ARGS),$(BOARD_OUT)/cmdline.txt:/cmdline.txt)
@echo "$(BOARD_DESC)"
@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/mmcblkX bs=4M conv=fsync status=progress"
else
boot_image:
@echo "Error: BOARD=$(BOARD) has BOARD_BOOT='$(BOARD_BOOT)', which has no boot_image recipe"
@exit 1
endif
# iso-usb — build a PURE UEFI isohybrid ISO for the Iso Image Writer workflow
# (GNOME Disks "Restore Disk Image...", or dd). Writes a single .iso to a USB
@@ -1150,7 +1301,7 @@ iso-usb: all
# yourself with Ctrl-A X or by closing the window).
# amd64/aarch64: serial → stdio (bidirectional, you can type into the REPL);
# riscv64: uses proper GPT (esp dir not supported)
qemu-esp: include/version.h $(LOADER_EFI) $(KERNEL_ELF)
qemu-esp: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
@rm -rf $(BUILD_DIR)/esp
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@@ -1206,7 +1357,7 @@ else ifeq ($(ARCH),riscv64)
endif
# qemu-gdb — launch QEMU with GDB stub on port 1234
qemu-gdb: include/version.h $(LOADER_EFI) $(KERNEL_ELF)
qemu-gdb: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTX64.EFI
@@ -1253,13 +1404,14 @@ info:
@echo "Loader output: $(LOADER_EFI)"
@echo "Kernel output: $(KERNEL_ELF)"
@echo "VM integration: $(STARFORTH_ENABLE_VM)"
@echo "StarForth v4: $(STARFORTH_V4)"
@echo "Monolithic: $(MONOLITHIC)"
help:
@echo "StarKernel Build System"
@echo "======================="
@echo ""
@echo "Usage: make -f Makefile.starkernel [ARCH=<arch>] [TARGET=<target>] <goal>"
@echo "Usage: make -f kernel/Makefile [ARCH=<arch>] [TARGET=<target>] <goal>"
@echo ""
@echo "Architectures (aliases accepted):"
@echo " ARCH=amd64 / x86_64 — x86-64 (default on x86 host)"
@@ -1289,8 +1441,8 @@ help:
@echo " help — show this message"
@echo ""
@echo "Examples:"
@echo " make -f Makefile.starkernel ARCH=amd64 qemu"
@echo " make -f Makefile.starkernel ARCH=aarch64 clean qemu"
@echo " make -f Makefile.starkernel ARCH=riscv64 clean qemu"
@echo " make -f Makefile.starkernel ARCH=x86_64 qemu # alias works"
@echo " make -f Makefile.starkernel kernel-all"
@echo " make -f kernel/Makefile ARCH=amd64 qemu"
@echo " make -f kernel/Makefile ARCH=aarch64 clean qemu"
@echo " make -f kernel/Makefile ARCH=riscv64 clean qemu"
@echo " make -f kernel/Makefile ARCH=x86_64 qemu # alias works"
@echo " make -f kernel/Makefile kernel-all"
@@ -0,0 +1,21 @@
/* capsule_blocks.h -- the block format of a .4th capsule payload.
*
* A .4th capsule is text: "Block <num>" header lines, each followed by that
* block's content lines (tools/mkcapsule.c, validate_forth_blocks). This is
* the one place that says what a header line is. It depends on no VM, so
* every loader of capsules can use it: the kernel's and the hosted v4
* system's (docs/v4.0.0/NUCLEUS.md 5.3).
*/
#ifndef STARKERNEL_CAPSULE_BLOCKS_H
#define STARKERNEL_CAPSULE_BLOCKS_H
#include <stdint.h>
/* Is the line that starts at p a "Block <num>" header? If so, returns 1
* with the number in *out_num and the start of the next line in *out_after;
* otherwise returns 0 and changes nothing. `end` is one past the payload's
* last byte. */
int capsule_block_header(const uint8_t *p, const uint8_t *end,
uint32_t *out_num, const uint8_t **out_after);
#endif /* STARKERNEL_CAPSULE_BLOCKS_H */

Some files were not shown because too many files have changed in this diff Show More