Compare commits
175
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d09af043 | ||
|
|
83694b18be | ||
|
|
d5cd64605d | ||
|
|
50be766e7b | ||
|
|
88554666d4 | ||
|
|
7eb545f387 | ||
|
|
8967c93241 | ||
|
|
0d91608f39 | ||
|
|
6015c87157 | ||
|
|
10f5ffd00b | ||
|
|
2cf37aab4f | ||
|
|
380f1cc6ec | ||
|
|
2b587206ee | ||
|
|
ec817a8264 | ||
|
|
353783726d | ||
|
|
1c720ab2c7 | ||
|
|
482cc75faf | ||
|
|
075385ab63 | ||
|
|
dfabfa46f6 | ||
|
|
5150155281 | ||
|
|
59727f762a | ||
|
|
d343001dea | ||
|
|
ee2bbf4ecf | ||
|
|
cbe1cd5e44 | ||
|
|
9efe8aad3a | ||
|
|
6e1ac157c6 | ||
|
|
a425f738a4 | ||
|
|
193523d873 | ||
|
|
bc6deab3f5 | ||
|
|
18adf59091 | ||
|
|
eda6577a53 | ||
|
|
4bdb210a99 | ||
|
|
2bf958ce3a | ||
|
|
c2f4b9808a | ||
|
|
b04ae55ec5 | ||
|
|
9bfd5071d2 | ||
|
|
7f4d946d2e | ||
|
|
bcfd6556a8 | ||
|
|
6d90375da7 | ||
|
|
7c06bdfdb6 | ||
|
|
d5b7235464 | ||
|
|
26a0748455 | ||
|
|
4a505a154d | ||
|
|
0e761cb117 | ||
|
|
a7991f2757 | ||
|
|
8cacfb663c | ||
|
|
348eed7fa1 | ||
|
|
f9c034cadd | ||
|
|
630d03fa4e | ||
|
|
a041b401ea | ||
|
|
42610e844f | ||
|
|
9af442f793 | ||
|
|
2c5427d9c1 | ||
|
|
beb7ded96c | ||
|
|
ae17a1ccf0 | ||
|
|
a68ea4841f | ||
|
|
11e135a8c3 | ||
|
|
c7b61b5680 | ||
|
|
bd65b5b165 | ||
|
|
a02a7905cd | ||
|
|
7a8b528d3b | ||
|
|
25fc5fd5e3 | ||
|
|
e8e8ea13ea | ||
|
|
ad3efdda65 | ||
|
|
085d0881de | ||
|
|
d4b1b4ff91 | ||
|
|
8df6c16766 | ||
|
|
5f1f60fc84 | ||
|
|
01c447f9ab | ||
|
|
ab7a9bf06f | ||
|
|
ff53ec4bb4 | ||
|
|
e692759a96 | ||
|
|
bd996a1106 | ||
|
|
0273308b78 | ||
|
|
c80b4c8ed7 | ||
|
|
6dc71758da | ||
|
|
b9b6f0c6fa | ||
|
|
f6b3ec54f4 | ||
|
|
b128a4d6db | ||
|
|
79db64c4e2 | ||
|
|
e88032b7e8 | ||
|
|
2abaf51aee | ||
|
|
2930349bbf | ||
|
|
5d6043e37e | ||
|
|
ed6b11ad88 | ||
|
|
506b645726 | ||
|
|
294e69463a | ||
|
|
903321e548 | ||
|
|
d4bd6e9601 | ||
|
|
c1bbcaaba4 | ||
|
|
384a6c1cd2 | ||
|
|
8c540b0305 | ||
|
|
a1afb44598 | ||
|
|
c8dc14897c | ||
|
|
0fbe1432b7 | ||
|
|
ab272cc2ad | ||
|
|
e7d686c7a2 | ||
|
|
c02505db69 | ||
|
|
dc7e37ba5c | ||
|
|
644bfc0a25 | ||
|
|
ebffa6082d | ||
|
|
0da7e32a0b | ||
|
|
361dcd1148 | ||
|
|
b3d2d56d11 | ||
|
|
b5d644d498 | ||
|
|
806f876ee0 | ||
|
|
dac7ffac92 | ||
|
|
bbd1b4047f | ||
|
|
44ffd261bb | ||
|
|
26a6a16537 | ||
|
|
880560fc00 | ||
|
|
c10d3a9cca | ||
|
|
01b5bbb23f | ||
|
|
b137678691 | ||
|
|
e178a76c36 | ||
|
|
7b42a9b6c1 | ||
|
|
443439c3d2 | ||
|
|
31fcdcd0a3 | ||
|
|
7b710aa322 | ||
|
|
861f800b7f | ||
|
|
2c16183788 | ||
|
|
03d8d991ef | ||
|
|
481d484e93 | ||
|
|
84c7711763 | ||
|
|
cb66db1107 | ||
|
|
7c5be22799 | ||
|
|
e50cc1f73a | ||
|
|
030ca63639 | ||
|
|
13335dcc9c | ||
|
|
40178098e9 | ||
|
|
7e3932a4ad | ||
|
|
7a21f07ad5 | ||
|
|
638aeb6636 | ||
|
|
6a019b8968 | ||
|
|
2e4d1330ea | ||
|
|
f030edcd08 | ||
|
|
13ec4b1b6c | ||
|
|
8235078b3b | ||
|
|
1ce705783c | ||
|
|
e117333a87 | ||
|
|
a2a4a85d38 | ||
|
|
f4bfe7de02 | ||
|
|
a2d21d5cda | ||
|
|
29cf2688d2 | ||
|
|
9e03bdab19 | ||
|
|
8f37e15f8a | ||
|
|
54e1cdafb1 | ||
|
|
ab3b708540 | ||
|
|
d0accdbb88 | ||
|
|
167fdc3973 | ||
|
|
981f4180ce | ||
|
|
d948e0c1a3 | ||
|
|
4f493fa317 | ||
|
|
5ac52f19d6 | ||
|
|
6a371b850e | ||
|
|
092036c97a | ||
|
|
93d09840da | ||
|
|
3b9d1e4337 | ||
|
|
41f59afeb3 | ||
|
|
9a391eb0ad | ||
|
|
067f317c47 | ||
|
|
1f98034311 | ||
|
|
a8b70e88d3 | ||
|
|
f238f02afa | ||
|
|
3c709c115b | ||
|
|
357cb5b4ac | ||
|
|
2fcc468ecb | ||
|
|
6302dcb50e | ||
|
|
26c1117ccd | ||
|
|
b7d9ed5425 | ||
|
|
81049da268 | ||
|
|
089ab2160e | ||
|
|
2406158668 | ||
|
|
2008c4596a | ||
|
|
7306872848 |
+1
-1
@@ -14,7 +14,7 @@ Checks: >
|
||||
|
||||
WarningsAsErrors: [ ]
|
||||
|
||||
HeaderFilterRegex: '^src/.*|^include/.*'
|
||||
HeaderFilterRegex: '^v3/(src|include)/.*|^kernel/(src|include)/.*'
|
||||
|
||||
AnalyzeTemporaryDtors: false
|
||||
FormatStyle: none
|
||||
|
||||
+28
-28
@@ -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
@@ -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
|
||||
|
||||
@@ -74,3 +74,4 @@ tools/kconfig/.qconf.*
|
||||
# tools/capsule-reserved.txt (human-authored) and tools/patches/ (decision
|
||||
# record) are NOT ignored — those are tracked deliberately.
|
||||
/tools/capsule-claims.txt
|
||||
/.directory
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
+55
-41
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-22T22:29:26Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-10-08T00:35:37Z -->
|
||||
<!-- 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, 8005, 8006 | `0x1c5ea12a9341e796` | 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,20 @@
|
||||
| 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` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8001 | `v4:hera.4th` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8002 | `v4:hera.4th` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8003 | `v4:hera.4th` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8004 | `v4:hera.4th` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8005 | `v4:hera.4th` | `0x1c5ea12a9341e796` | ok |
|
||||
| 8006 | `v4:hera.4th` | `0x1c5ea12a9341e796` | 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.*
|
||||
|
||||
@@ -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 ;
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
( The nodes this one has had born: for each its number and )
|
||||
( its place in the fabric. Sixteen at most. )
|
||||
CREATE (KIDS) 32 ALLOT VARIABLE (KIDS#) 0 (KIDS#) !
|
||||
: (KID@) ( i -- addr ) 2 * (KIDS) + ;
|
||||
: (NOTE) ( number place -- )
|
||||
(KIDS#) @ 16 < IF
|
||||
(KIDS#) @ (KID@) SWAP OVER 1+ ! ! 1 (KIDS#) +!
|
||||
ELSE DROP DROP THEN ;
|
||||
( which of them has that number: its place in the list, or -1 )
|
||||
: (WHICH) ( number -- i | -1 )
|
||||
-1 SWAP (KIDS#) @ DUP IF 0 DO
|
||||
DUP I (KID@) @ = IF SWAP DROP I SWAP THEN
|
||||
LOOP ELSE DROP THEN DROP ;
|
||||
Block 8003
|
||||
( 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 8004
|
||||
S" (SEAL)" (TELL)
|
||||
(KID) @ (KID#) @ NODE-PARITY
|
||||
(KID#) @ (KID) @ (NOTE)
|
||||
(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 8005
|
||||
( 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) ;
|
||||
Block 8006
|
||||
( n -- KILL: node n is removed, whatever it was doing. )
|
||||
( This node forgets the way to it, and every other node it )
|
||||
( has had born is told that n is gone: each forgets its way )
|
||||
( to n, and one that is waiting for n's answer stops. )
|
||||
( docs/v4.0.0/MESH.md 7b.5. As v3's KILL, by number. )
|
||||
: (UNLIST) ( i -- )
|
||||
(KIDS#) @ 1- DUP (KIDS#) ! (KID@) DUP @ SWAP 1+ @
|
||||
ROT (KID@) SWAP OVER 1+ ! ! ;
|
||||
: KILL ( n -- )
|
||||
DUP (WHICH) DUP 0< IF
|
||||
DROP DROP ." KILL: no such node" CR -1 NODE-ERROR ! THEN
|
||||
DUP (KID@) 1+ @ NODE-KILL (UNLIST)
|
||||
DUP NO-ROUTE
|
||||
(KIDS#) @ DUP IF 0 DO DUP I (KID@) @ GONE LOOP ELSE DROP THEN
|
||||
DROP ;
|
||||
Binary file not shown.
@@ -12,6 +12,8 @@ Block 4016
|
||||
( >BODY-then-store, so a pinned CONSTANT isn't tamper-proof. )
|
||||
( Read with ZUSE-PUBKEY@ / ZUSE-CERT-INSTALLED? -- both C )
|
||||
( primitives, read-only; the seed has no FORTH access at all. )
|
||||
( ZUSE-ELIGIBILITY-ADD denied by default -- see block 4017. )
|
||||
0 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
|
||||
|
||||
Block 4017
|
||||
( ACL-ZUSE-BOOT ( -- ) re-invokable: capsule_zuse_boot.c )
|
||||
@@ -23,6 +25,8 @@ Block 4017
|
||||
: ACL-ZUSE-BOOT ( -- )
|
||||
ZUSE-CERT-INSTALLED? IF
|
||||
ZUSE-AUTHENTICATE
|
||||
1 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
|
||||
['] ZUSE-ELIGIBILITY-ADD ACL-PIN
|
||||
LOG-INFO" zuse: activated"
|
||||
ELSE
|
||||
LOG-INFO" zuse: NOT activated -- no cert installed"
|
||||
|
||||
+1
-1
@@ -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`.
|
||||
|
||||
|
||||
@@ -0,0 +1,787 @@
|
||||
# StarForth Primitive Word Reference
|
||||
|
||||
This reference covers every **C-implemented primitive** word that StarForth registers. It was built from
|
||||
`admin/LithosAnanake` at commit `6302dcb` (2026-09-23). It lists only words registered in C
|
||||
through `register_word()` or `vm_create_word()`. Words defined in FORTH inside capsules (`*.4th`) are out of scope.
|
||||
|
||||
Sources:
|
||||
|
||||
- `src/word_registry.c`: `register_forth79_words()` registers the core set in every VM.
|
||||
- `src/word_source/*.c`: one file per module.
|
||||
- `src/starkernel/capsule/mama_forth_words.c`: kernel-only Hera (Mama) and child-VM words.
|
||||
- `src/starkernel/repl.c` and `src/starkernel/doe_log.c`: kernel-only REPL and DoE words.
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
| Item | Meaning |
|
||||
|---|---|
|
||||
| Cell | `cell_t` is `int64_t`, so every cell is 64 bits and signed. |
|
||||
| Flag | TRUE is `-1` (all bits set) and FALSE is `0`. Words that take a flag treat any non-zero value as true. |
|
||||
| `addr` | A **VM address**: a byte offset into the VM's 5 MB linear memory (`VM_MEMORY_SIZE`), not a host pointer. |
|
||||
| `c-addr u` | A string given as its address and length. |
|
||||
| `d`, `ud` | A double-cell number made of two cells, with the **high cell on top**. |
|
||||
| `xt` | An execution token. In StarForth this is the `DictEntry*` of the word. |
|
||||
| `q` | A Q48.16 fixed-point value in one cell (`1.0` = `65536`). |
|
||||
| `"name"` | The word parses a name from the input stream after it. |
|
||||
| `( R: ... )` | The effect on the return stack. |
|
||||
| **IMM** | The word is IMMEDIATE, so it runs even while compiling. |
|
||||
| **CO** | The word is compile-only and sets `vm->error` if used outside a definition. |
|
||||
| **K** | The word is registered only in the kernel build (`__STARKERNEL__`). |
|
||||
| **H** | The word is registered only in the hosted build (the Linux or macOS binary). |
|
||||
|
||||
Errors: a primitive does not throw. On stack underflow or overflow, a bad address, or division by zero it sets
|
||||
`vm->error = 1` and logs a message.
|
||||
|
||||
Stack limits: the data stack and return stack hold 1024 cells each (`STACK_SIZE`). A word name can be at most 31
|
||||
characters (`WORD_NAME_MAX`).
|
||||
|
||||
Shadowing: when a later module registers a name again, the newer entry wins lookups. For example, `MOD`, `/MOD`,
|
||||
`*/` and `*/MOD` are registered by the arithmetic module and again by the mixed-arithmetic module, so the
|
||||
mixed-arithmetic versions are the active ones.
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Stack](#1-stack)
|
||||
2. [Return stack](#2-return-stack)
|
||||
3. [Memory](#3-memory)
|
||||
4. [Arithmetic](#4-arithmetic)
|
||||
5. [Logic and comparison](#5-logic-and-comparison)
|
||||
6. [Mixed-precision arithmetic](#6-mixed-precision-arithmetic)
|
||||
7. [Double-cell numbers](#7-double-cell-numbers)
|
||||
8. [Number formatting and output](#8-number-formatting-and-output)
|
||||
9. [Strings, parsing, and input](#9-strings-parsing-and-input)
|
||||
10. [Terminal I/O](#10-terminal-io)
|
||||
11. [Blocks and mass storage](#11-blocks-and-mass-storage)
|
||||
12. [Dictionary space](#12-dictionary-space)
|
||||
13. [Dictionary manipulation](#13-dictionary-manipulation)
|
||||
14. [Vocabularies](#14-vocabularies)
|
||||
15. [System](#15-system)
|
||||
16. [Line editor](#16-line-editor)
|
||||
17. [Defining words and the compiler](#17-defining-words-and-the-compiler)
|
||||
18. [Control flow](#18-control-flow)
|
||||
19. [StarForth extensions](#19-starforth-extensions)
|
||||
20. [Word-level ACL](#20-word-level-acl)
|
||||
21. [Physics: benchmark and diagnostics](#21-physics-benchmark-and-diagnostics)
|
||||
22. [Physics: pipelining diagnostics](#22-physics-pipelining-diagnostics)
|
||||
23. [Physics: freeze, heat, and decay](#23-physics-freeze-heat-and-decay)
|
||||
24. [Dictionary heat optimisation](#24-dictionary-heat-optimisation)
|
||||
25. [Logging](#25-logging)
|
||||
26. [Q48.16 fixed-point math](#26-q4816-fixed-point-math)
|
||||
27. [Inference engine (SSM, L8, and Bayes)](#27-inference-engine-ssm-l8-and-bayes)
|
||||
28. [DEFER and IS](#28-defer-and-is)
|
||||
29. [Framebuffer (Hestia only)](#29-framebuffer-hestia-only)
|
||||
30. [Keyboard](#30-keyboard)
|
||||
31. [TrueType text](#31-truetype-text)
|
||||
32. [REPL scrollback](#32-repl-scrollback)
|
||||
33. [Kernel REPL and DoE hooks](#33-kernel-repl-and-doe-hooks)
|
||||
34. [Hera (Mama) and child-VM words](#34-hera-mama-and-child-vm-words)
|
||||
35. [Hosted lifecycle stubs](#35-hosted-lifecycle-stubs)
|
||||
36. [Implementation quirks to know](#36-implementation-quirks-to-know)
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack
|
||||
`src/word_source/stack_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `DROP` | `( x -- )` | Discards the top cell. |
|
||||
| `DUP` | `( x -- x x )` | Copies the top cell. |
|
||||
| `?DUP` | `( x -- x x \| 0 -- 0 )` | Copies the top cell only when it is non-zero. The usual idiom is `?DUP IF ... THEN`. |
|
||||
| `SWAP` | `( x1 x2 -- x2 x1 )` | Swaps the top two cells. |
|
||||
| `OVER` | `( x1 x2 -- x1 x2 x1 )` | Copies the second cell to the top. |
|
||||
| `ROT` | `( x1 x2 x3 -- x2 x3 x1 )` | Moves the third cell to the top. |
|
||||
| `-ROT` | `( x1 x2 x3 -- x3 x1 x2 )` | Moves the top cell down to third place. This is the reverse of `ROT`. |
|
||||
| `DEPTH` | `( -- n )` | Pushes the number of cells that were on the data stack before `DEPTH` ran. |
|
||||
| `PICK` | `( xn … x0 n -- xn … x0 xn )` | Copies the n-th cell to the top. **0-based:** `0 PICK` is `DUP` and `1 PICK` is `OVER`. An error occurs if `n < 0` or `n ≥ depth`. |
|
||||
| `ROLL` | `( … n -- … )` | Moves a cell to the top and closes the gap. **Non-standard:** `n` counts from the *bottom* of the stack (1-based), so `1 ROLL` moves the deepest cell to the top. `0 ROLL` does nothing. See [§36](#36-implementation-quirks-to-know). |
|
||||
|
||||
## 2. Return stack
|
||||
`src/word_source/return_stack_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `>R` | `( x -- ) ( R: -- x )` | Moves a cell from the data stack to the return stack. Inside a definition, balance it with `R>` before `;` or `EXIT`. |
|
||||
| `R>` | `( -- x ) ( R: x -- )` | Moves a cell from the return stack back to the data stack. |
|
||||
| `R@` | `( -- x ) ( R: x -- x )` | Copies the top of the return stack without removing it. |
|
||||
|
||||
## 3. Memory
|
||||
`src/word_source/memory_words.c`. Every address is a VM byte offset and is checked against the VM's memory bounds.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `@` | `( addr -- x )` | Fetches the cell at `addr`. |
|
||||
| `!` | `( x addr -- )` | Stores `x` at `addr`. |
|
||||
| `C@` | `( addr -- c )` | Fetches the byte at `addr`, zero-extended. |
|
||||
| `C!` | `( c addr -- )` | Stores the low 8 bits of `c` at `addr`. |
|
||||
| `+!` | `( n addr -- )` | Adds `n` to the cell at `addr`. |
|
||||
| `-!` | `( n addr -- )` | Subtracts `n` from the cell at `addr`. |
|
||||
| `2@` | `( addr -- x-lo x-hi )` | Fetches two cells: the low cell from `addr` and the high cell from `addr+8`, leaving the high cell on top. |
|
||||
| `2!` | `( x-lo x-hi addr -- )` | Stores two cells: the low cell at `addr` and the high cell at `addr+8`. |
|
||||
| `FILL` | `( addr u c -- )` | Fills `u` bytes starting at `addr` with the byte `c`. |
|
||||
| `MOVE` | `( src dst u -- )` | Copies `u` bytes from `src` to `dst`. It is safe when the ranges overlap because it uses memmove semantics. |
|
||||
| `ERASE` | `( addr u -- )` | Sets `u` bytes to zero. |
|
||||
| `CELLS` | `( n -- n*8 )` | Scales a cell count to a byte count. |
|
||||
|
||||
## 4. Arithmetic
|
||||
`src/word_source/arithmetic_words.c`. All arithmetic is signed 64-bit, and division truncates toward zero as in C.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `+` | `( n1 n2 -- n1+n2 )` | Adds the two cells. |
|
||||
| `-` | `( n1 n2 -- n1-n2 )` | Subtracts `n2` from `n1`. |
|
||||
| `*` | `( n1 n2 -- n1*n2 )` | Multiplies the two cells. The result wraps modulo 2⁶⁴. |
|
||||
| `/` | `( n1 n2 -- n1/n2 )` | Divides, truncating toward zero. Division by zero sets an error. |
|
||||
| `MOD` | `( n1 n2 -- rem )` | Pushes the remainder of `n1 / n2`, which has the sign of `n1`. This name is shadowed by §6. |
|
||||
| `/MOD` | `( n1 n2 -- rem quot )` | Pushes the remainder and the quotient, with the quotient on top. This name is shadowed by §6. |
|
||||
| `*/` | `( n1 n2 n3 -- n1*n2/n3 )` | Multiplies and then divides using a wide intermediate. This name is shadowed by §6. |
|
||||
| `*/MOD` | `( n1 n2 n3 -- rem quot )` | Like `*/`, but also leaves the remainder. This name is shadowed by §6. |
|
||||
| `1+` `1-` | `( n -- n±1 )` | Increments or decrements by 1. |
|
||||
| `2+` `2-` | `( n -- n±2 )` | Adds or subtracts 2. |
|
||||
| `2*` | `( n -- n*2 )` | Shifts left by one bit. |
|
||||
| `2/` | `( n -- n/2 )` | Shifts right by one bit, keeping the sign (arithmetic shift). |
|
||||
| `ABS` | `( n -- \|n\| )` | Pushes the absolute value. |
|
||||
| `NEGATE` | `( n -- -n )` | Pushes the two's-complement negation. |
|
||||
| `MIN` `MAX` | `( n1 n2 -- n3 )` | Pushes the smaller or the larger value, compared as signed numbers. |
|
||||
|
||||
## 5. Logic and comparison
|
||||
`src/word_source/logical_words.c`. Comparison words return a proper flag of `-1` or `0`.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `AND` `OR` `XOR` | `( x1 x2 -- x3 )` | Bitwise AND, OR, and XOR. |
|
||||
| `NOT` | `( x -- flag )` | **FORTH-79 logical NOT:** `0` gives `TRUE` and any other value gives `FALSE`. This is *not* a bitwise complement; use `INVERT` for that. |
|
||||
| `INVERT` | `( x -- ~x )` | Bitwise complement (from FORTH-83). |
|
||||
| `LSHIFT` | `( x u -- x<<u )` | Logical shift left by `u` bits. |
|
||||
| `RSHIFT` | `( x u -- x>>u )` | Logical (unsigned) shift right by `u` bits. |
|
||||
| `0=` | `( n -- flag )` | True if `n` is 0. |
|
||||
| `0<` | `( n -- flag )` | True if `n` is negative. |
|
||||
| `0>` | `( n -- flag )` | True if `n` is positive. |
|
||||
| `0<>` | `( n -- flag )` | True if `n` is not 0. |
|
||||
| `=` `<>` | `( n1 n2 -- flag )` | Tests for equality or inequality. |
|
||||
| `<` `>` `<=` `>=` | `( n1 n2 -- flag )` | Signed comparisons of `n1` against `n2`. |
|
||||
| `U<` `U>` | `( u1 u2 -- flag )` | Unsigned comparisons. |
|
||||
| `WITHIN` | `( n lo hi -- flag )` | True if `lo ≤ n < hi`, using the standard half-open range. |
|
||||
| `TRUE` | `( -- -1 )` | Pushes the canonical true flag. |
|
||||
| `FALSE` | `( -- 0 )` | Pushes the canonical false flag. |
|
||||
|
||||
## 6. Mixed-precision arithmetic
|
||||
`src/word_source/mixed_arithmetic_words.c`. A double here means two full 64-bit cells (128 bits).
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `M+` | `( d n -- d' )` | Adds a signed single to a double and propagates the carry into the high cell. |
|
||||
| `M-` | `( d n -- d' )` | Subtracts a signed single from a double and propagates the borrow. |
|
||||
| `M*` | `( n1 n2 -- d )` | Multiplies 64×64 into a full 128-bit signed product, using `__int128` internally. The result can be printed with `D.`. |
|
||||
| `M/MOD` | `( d n -- rem quot )` | Divides a 128-bit double by a single, leaving the quotient on top. It uses bit-serial long division, so it works in the freestanding kernel without libgcc. |
|
||||
| `MOD` | `( n1 n2 -- rem )` | **Active version.** Computes `n1 % n2`. Division by zero sets an error. |
|
||||
| `/MOD` | `( n1 n2 -- rem quot )` | **Active version.** Leaves the remainder under the quotient. Division by zero sets an error. |
|
||||
| `*/` | `( n1 n2 n3 -- n4 )` | **Active version.** Computes `(n1*n2)/n3` with a wide intermediate, so `n1*n2` does not overflow. Division by zero sets an error. Typical use is scaling, for example `x 355 113 */`. |
|
||||
| `*/MOD` | `( n1 n2 n3 -- rem quot )` | **Active version.** Like `*/`, but also leaves the remainder under the quotient. |
|
||||
|
||||
## 7. Double-cell numbers
|
||||
`src/word_source/double_words.c`. In every stack picture, `d` stands for the pair `( lo hi )` with the high cell on top.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `S>D` | `( n -- d )` | Sign-extends a single to a double. |
|
||||
| `D+` `D-` | `( d1 d2 -- d3 )` | Double add and subtract, with carry or borrow. |
|
||||
| `DNEGATE` | `( d -- -d )` | Negates a double. |
|
||||
| `DABS` | `( d -- \|d\| )` | Pushes the absolute value of a double. |
|
||||
| `DMAX` `DMIN` | `( d1 d2 -- d3 )` | Pushes the larger or smaller double, compared as signed values. |
|
||||
| `D<` | `( d1 d2 -- flag )` | Signed less-than on doubles. |
|
||||
| `D=` | `( d1 d2 -- flag )` | Equality on doubles. |
|
||||
| `D0=` | `( d -- flag )` | True if the double is zero. |
|
||||
| `D0<` | `( d -- flag )` | True if the double is negative. |
|
||||
| `D2*` | `( d -- d*2 )` | Shifts a double left by one bit across both cells. |
|
||||
| `D2/` | `( d -- d/2 )` | Shifts a double right by one bit (arithmetic shift) across both cells. |
|
||||
| `2DROP` | `( x1 x2 -- )` | Drops a cell pair. |
|
||||
| `2DUP` | `( x1 x2 -- x1 x2 x1 x2 )` | Duplicates a cell pair. |
|
||||
| `2SWAP` | `( p1 p2 -- p2 p1 )` | Swaps two cell pairs. |
|
||||
| `2OVER` | `( p1 p2 -- p1 p2 p1 )` | Copies the second pair to the top. |
|
||||
| `2ROT` | `( p1 p2 p3 -- p2 p3 p1 )` | Rotates three cell pairs. |
|
||||
| `2>R` | `( x1 x2 -- ) ( R: -- x1 x2 )` | Moves a pair to the return stack. |
|
||||
| `2R>` | `( -- x1 x2 ) ( R: x1 x2 -- )` | Moves a pair back from the return stack. |
|
||||
| `2R@` | `( -- x1 x2 ) ( R: x1 x2 -- x1 x2 )` | Copies a pair from the return stack. |
|
||||
|
||||
## 8. Number formatting and output
|
||||
`src/word_source/format_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `.` | `( n -- )` | Prints a signed number in the current `BASE`, followed by a space. |
|
||||
| `.R` | `( n width -- )` | Prints a signed number right-aligned in a field `width` characters wide. |
|
||||
| `U.` | `( u -- )` | Prints an unsigned number followed by a space. |
|
||||
| `U.R` | `( u width -- )` | Prints an unsigned number right-aligned. |
|
||||
| `D.` | `( d -- )` | Prints a signed double. |
|
||||
| `D.R` | `( d width -- )` | Prints a signed double right-aligned. |
|
||||
| `.S` | `( -- )` | Prints the data stack without changing it. This is the main debugging aid. |
|
||||
| `?` | `( addr -- )` | Prints the cell at `addr`; it is the same as `@ .`. |
|
||||
| `DUMP` | `( addr u -- )` | Prints a hex and ASCII dump of `u` bytes starting at `addr`. |
|
||||
| `<#` | `( -- )` | Starts pictured numeric output by resetting the conversion buffer. |
|
||||
| `#` | `( ud -- ud' )` | Converts one digit (`ud mod BASE`) into the buffer. It also accepts a single signed cell and converts its magnitude. |
|
||||
| `#S` | `( ud -- 0 0 )` | Converts digits until the value is zero, always producing at least one digit. It accepts a single cell the same way `#` does. |
|
||||
| `HOLD` | `( c -- )` | Inserts the character `c` into the pictured output buffer. |
|
||||
| `SIGN` | `( n -- )` | Inserts `-` if `n` is negative. |
|
||||
| `#>` | `( ud -- c-addr u )` | Ends conversion and leaves the string. It is tolerant: it pops `ud` only if one is present. |
|
||||
| `BASE` | `( -- addr )` | Pushes the address of the number-conversion radix variable. |
|
||||
| `DECIMAL` `HEX` `OCTAL` | `( -- )` | Sets `BASE` to 10, 16, or 8. |
|
||||
|
||||
Example: `: .$ ( n -- ) <# # # 46 HOLD #S #> TYPE ;` prints `1234` as `12.34`.
|
||||
|
||||
## 9. Strings, parsing, and input
|
||||
`src/word_source/string_words.c`. The comparison and search words below also accept a counted string in place of an
|
||||
`addr u` pair; they detect it when the first byte at `addr` equals `u`.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `COUNT` | `( c-addr1 -- c-addr2 u )` | Converts a counted string (length byte followed by characters) into an address and length. |
|
||||
| `EXPECT` | `( addr u -- )` | Reads up to `u` characters from the terminal into `addr` and stores the count read in `SPAN`. |
|
||||
| `SPAN` | `( -- addr )` | Pushes the address of the variable that holds the count from the last `EXPECT`. |
|
||||
| `QUERY` | `( -- )` | Reads a line into `TIB` and resets `>IN`. |
|
||||
| `TIB` | `( -- addr )` | Pushes the address of the terminal input buffer. |
|
||||
| `>IN` | `( -- addr )` | Pushes the address of the offset into the current input source. |
|
||||
| `SOURCE` | `( -- addr u )` | Pushes the current input buffer and its length. |
|
||||
| `WORD` | `( c -- c-addr )` | Skips leading `c` characters, parses up to the next `c`, and returns a counted string. The usual form is `BL WORD`. |
|
||||
| `BL` | `( -- 32 )` | Pushes the ASCII space character. |
|
||||
| `S"` **IMM** | `( "ccc<">" -- c-addr u )` | In interpret mode, stores the string at `HERE` and pushes it. When compiling, it compiles `(s")` followed by the inline text. |
|
||||
| `(s")` | `( -- c-addr u )` | Runtime for a compiled `S"`: reads the inline `[len][chars][pad]` block and skips the IP past it. The compiler inserts it; you do not call it directly. |
|
||||
| `[']` **IMM** | `( "name" -- xt )` | While compiling, compiles the xt of `name` as a literal. In interpret mode it behaves like `'`. |
|
||||
| `LITERAL` `[LITERAL]` | – | Placeholders that do nothing. `LITERAL` is re-registered in §17 (the working version); `[LITERAL]` has no replacement and still does nothing. |
|
||||
| `CONVERT` | `( d1 addr1 -- d2 addr2 )` | Accumulates the digits at `addr1+1…` into `d1` and stops at the first non-digit. This is a simplified version. |
|
||||
| `NUMBER` | `( c-addr -- n flag )` | Converts a counted string to a number. Only base 10 is supported, and `flag` shows whether it succeeded. |
|
||||
| `ENCLOSE` | `( addr c -- addr n1 n2 n3 )` | The classic FIG parser: gives the offsets of the start of the token, the delimiter after it, and the next character. |
|
||||
| `-TRAILING` | `( addr u -- addr u' )` | Removes trailing spaces from the length. |
|
||||
| `CMOVE` | `( src dst u -- )` | Copies bytes upward from low to high addresses. It is safe for overlapping ranges when `dst ≤ src`. |
|
||||
| `CMOVE>` | `( src dst u -- )` | Copies bytes downward from high to low addresses. It is safe for overlapping ranges when `dst > src`. |
|
||||
| `COMPARE` | `( a1 u1 a2 u2 -- n )` | Compares two strings case-sensitively and returns `-1`, `0`, or `1`. |
|
||||
| `SEARCH` | `( a1 u1 a2 u2 -- a3 u3 flag )` | Finds string 2 inside string 1. If found, it returns the tail starting at the match and `-1`. If not, it returns string 1 unchanged and `0`. |
|
||||
| `SCAN` | `( addr u c -- addr' u' )` | Advances to the first occurrence of `c`. If there is none, it returns the end of the string and `0`. |
|
||||
| `SKIP` | `( addr u c -- addr' u' )` | Skips leading occurrences of `c`. |
|
||||
| `BLANK` | `( addr u -- )` | Fills `u` bytes with spaces. |
|
||||
|
||||
## 10. Terminal I/O
|
||||
`src/word_source/io_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `EMIT` | `( c -- )` | Prints one character. |
|
||||
| `CR` | `( -- )` | Prints a newline. |
|
||||
| `KEY` | `( -- c )` | Waits for a character and pushes it. |
|
||||
| `?TERMINAL` | `( -- flag )` | True if a key is waiting. It does not block. |
|
||||
| `TYPE` | `( c-addr u -- )` | Prints `u` characters. |
|
||||
| `SPACE` | `( -- )` | Prints one space. |
|
||||
| `SPACES` | `( n -- )` | Prints `n` spaces. |
|
||||
| `."` **IMM** | `( "ccc<">" -- )` | In interpret mode, prints the string immediately. When compiling, it compiles `(do-string)` and the inline text. |
|
||||
| `(do-string)` | `( -- )` | Runtime for a compiled `."`: prints the inline string and skips the IP past it. The compiler inserts it; you do not call it directly. |
|
||||
|
||||
## 11. Blocks and mass storage
|
||||
`src/word_source/block_words.c`. A block is 1024 bytes, viewed as 16 lines of 64 characters. Block numbers are
|
||||
LBNs (logical block numbers) in one address space that spans every attached device.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `BLOCK` | `( u -- addr )` | Returns the VM address of the buffer holding block `u`, reading it from disk if needed. It does not mark the buffer dirty. |
|
||||
| `BUFFER` | `( u -- addr )` | Assigns a buffer to block `u` *without* reading from disk and marks it dirty. Use it when you will overwrite the whole block. |
|
||||
| `UPDATE` | `( -- )` | Marks the current (`SCR`) block dirty and syncs it to the C block layer. |
|
||||
| `SAVE-BUFFERS` | `( -- )` | Writes every dirty buffer to disk. |
|
||||
| `EMPTY-BUFFERS` | `( -- )` | Discards every buffer **without** writing it and zeroes the user block window. |
|
||||
| `FLUSH` | `( -- )` | Runs `SAVE-BUFFERS` and then invalidates all buffers. |
|
||||
| `LOAD` | `( u -- )` | Sets `SCR` to `u` and interprets the 1024 bytes of block `u` as FORTH source. Block 0 cannot be loaded. |
|
||||
| `THRU` | `( u1 u2 -- )` | Loads blocks `u1` through `u2`, including both ends. |
|
||||
| `-->` | `( -- )` | Inside a block being loaded, continues interpreting at the next block. |
|
||||
| `LIST` | `( u -- )` | Sets `SCR` to `u` and prints the block. |
|
||||
| `SCR` | `( -- addr )` | Pushes the address of the variable holding the block number last listed or loaded. |
|
||||
| `BLK-CONFIRM-FORMAT` | `( lbn -- )` | Commits the container format of the device that owns `lbn`. Until this has run, the block layer **refuses every write** to that device. Only the owner (for example Artemis) should call it, and only after checking the disk contents are safe to touch. |
|
||||
| `RELOCATE-BLOCK` | `( home target -- )` | Moves the contents of `home` to `target` and redirects all later access to `home` through `target`. It is a mechanical primitive: it does not check whether `target` is free or owned by the caller. |
|
||||
| `BLK-ACL-ALLOW@` | `( blk -- allow )` | Reads the cached allow/deny flag of a block's ACL. |
|
||||
| `BLK-ACL-ALLOW!` | `( allow blk -- )` | Sets a block's cached allow/deny flag. |
|
||||
| `BLK-ACL-TTL@` | `( blk -- ttl )` | Reads a block's ACL TTL countdown. |
|
||||
| `BLK-ACL-TTL!` | `( ttl blk -- )` | Sets a block's ACL TTL countdown. |
|
||||
| `BLK-OWNER@` | `( blk -- fp )` | Pushes the 8-byte owner fingerprint as the raw bits of one cell. There is no `BLK-OWNER!`: ownership is set only in C, during MINT or birth. |
|
||||
| `BLK-ATTACH` | `( dev-ptr -- ok? )` | Registers an already-open `blkio_dev_t*`, passed as a raw pointer cell, in the unified LBN space. The pointer is trusted without checks. Artemis's USB-attach handler uses it. |
|
||||
|
||||
## 12. Dictionary space
|
||||
`src/word_source/dictionary_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `HERE` | `( -- addr )` | Pushes the next free byte in the dictionary. |
|
||||
| `ALIGN` | `( -- )` | Rounds `HERE` up to the next 8-byte cell boundary. |
|
||||
| `ALLOT` | `( n -- )` | Reserves `n` bytes at `HERE`. A negative `n` gives space back. |
|
||||
| `,` | `( x -- )` | Compiles one cell at `HERE` and advances `HERE`. |
|
||||
| `C,` | `( c -- )` | Compiles one byte. |
|
||||
| `2,` | `( x-lo x-hi -- )` | Compiles two cells, low cell first. |
|
||||
| `PAD` | `( -- addr )` | Pushes the address of a 512-byte scratch buffer near the top of memory. It is safe for temporary strings. |
|
||||
| `SP@` | `( -- n )` | Pushes the data stack pointer *index* (`dsp`). An empty stack gives `-1` and one item gives `0`. |
|
||||
| `SP!` | `( n -- )` | Restores the stack pointer index. It can only shrink the stack, never grow it. |
|
||||
| `LATEST` | `( -- addr )` | Pushes the address of the most recent definition. |
|
||||
|
||||
## 13. Dictionary manipulation
|
||||
`src/word_source/dictionary_manipulation_words.c`. Header-field words work on raw header addresses. They exist for
|
||||
FIG and FORTH-79 compatibility; take care with them.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `'` | `( "name" -- xt )` | Parses `name` and pushes its xt. It is not IMMEDIATE; inside a definition, use `[']`. |
|
||||
| `FIND` | `( "name" -- xt \| 0 )` | **Parses from the input stream**, not from a counted string on the stack. It pushes the entry, or `0` if the word is not found (a miss is not an error). For a counted string already in memory, use `(FIND)` from §14. |
|
||||
| `SMUDGE` | `( -- )` | **CO.** Toggles the smudge (hidden) bit on the latest word. |
|
||||
| `HIDDEN` | `( -- )` | **CO.** Sets the latest word's hidden bit (unlike `SMUDGE`, it does not toggle). |
|
||||
| `>BODY` | `( xt -- addr )` | Pushes the address of the parameter (data) field. |
|
||||
| `>NAME` | `( xt -- nfa )` | Pushes the name field. |
|
||||
| `NAME>` | `( nfa -- xt )` | Goes from the name field to the xt. |
|
||||
| `>LINK` | `( xt -- lfa )` | Pushes the link field. |
|
||||
| `LINK>` | `( lfa -- xt )` | Follows the link to the next (older) word. |
|
||||
| `CFA` `LFA` `NFA` `PFA` | `( addr -- addr' )` | FIG-style field-address conversions (code, link, name, and parameter fields). |
|
||||
| `TRAVERSE` | `( addr n -- addr' )` | Moves across a name field forward (`n=1`) or backward (`n=-1`). |
|
||||
| `INTERPRET` | `( -- )` | Runs the text interpreter on the rest of the current input. |
|
||||
|
||||
## 14. Vocabularies
|
||||
`src/word_source/vocabulary_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `VOCABULARY` | `( "name" -- )` | Creates a vocabulary. Running `name` later makes it the `CONTEXT` (search) vocabulary. |
|
||||
| `DEFINITIONS` | `( -- )` | Sets `CURRENT` to `CONTEXT`, so new definitions go into the vocabulary being searched. |
|
||||
| `CONTEXT` | `( -- addr )` | Pushes the address of the search-vocabulary pointer. |
|
||||
| `CURRENT` | `( -- addr )` | Pushes the address of the definition-vocabulary pointer. |
|
||||
| `FORTH` | `( -- )` | Makes the root `FORTH` vocabulary the context. |
|
||||
| `ORDER` | `( -- )` | Prints the search order (`CONTEXT`, then `FORTH`) and `CURRENT`. |
|
||||
| `(FIND)` | `( c-addr -- c-addr 0 \| xt 1 \| xt -1 )` | Looks up a counted string in `CONTEXT` and then in `FORTH`. It returns `1` for an IMMEDIATE word, `-1` for a normal word, and `0` if not found. |
|
||||
|
||||
Example: `VOCABULARY GRAPHICS GRAPHICS DEFINITIONS : BOX ... ; FORTH DEFINITIONS`
|
||||
|
||||
## 15. System
|
||||
`src/word_source/system_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `(` **IMM** | `( "ccc<)>" -- )` | Starts a comment that runs to the closing `)`. |
|
||||
| `\` **IMM** | `( "ccc<eol>" -- )` | Starts a comment that runs to the end of the line. |
|
||||
| `EXECUTE` | `( xt -- )` | Runs the word identified by `xt`. |
|
||||
| `NOP` | `( -- )` | Does nothing. |
|
||||
| `QUIT` **IMM** | `( -- ) ( R: … -- )` | Clears the return stack and the error flag and returns to the outer interpreter. The data stack is kept. It is refused (sets an error) inside a definition. |
|
||||
| `ABORT` | `( … -- )` | Clears both stacks and returns to the interpreter. It is **not** reported as an error. |
|
||||
| `ABORT"` **IMM** | `( flag "ccc<">" -- )` | If `flag` is non-zero, prints the message and runs `ABORT`. It works in both interpret and compile mode. |
|
||||
| `(ABORT")` | `( flag addr u -- )` | Runtime for a compiled `ABORT"`. The compiler inserts it; you do not call it directly. |
|
||||
| `COLD` | `( -- )` | Clears both stacks and the error flag, returns to interpret mode, and moves `HERE` back to 1024 if it is higher. Dictionary headers are **not** removed; this is a minimal cold start. |
|
||||
| `WARM` | `( -- )` | Clears both stacks and the error flag and returns to interpret mode. `HERE` and the dictionary are kept. |
|
||||
| `BYE` | `( -- )` | Leaves this VM. In a child VM it halts the VM and returns to the parent's REPL. On Hera, the kernel's `BYE` from §34 takes precedence. |
|
||||
| `REBOOT` | `( c-addr u -- )` | Sets the boot arguments and does a cold reset. Interpret mode only. |
|
||||
| `SAVE-SYSTEM` | `( -- )` | Takes a simple snapshot of the start of VM memory. |
|
||||
| `WORDS` | `( -- )` | Lists the words in the current vocabulary. |
|
||||
| `VLIST` | `( -- )` | Gives a detailed word listing. |
|
||||
| `SEE` | `( "name" -- )` | Decompiles and shows a definition. |
|
||||
| `PAGE` | `( -- )` | Clears the screen. |
|
||||
| `79-STANDARD` | `( -- flag )` | Pushes `-1` when FORTH-79 compliance mode is on. |
|
||||
|
||||
## 16. Line editor
|
||||
`src/word_source/editor_words.c`. The editor works on block `SCR` as 16 lines of 64 characters.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `L` | `( u -- )` | Prints line `u` (0–15) of the current screen. |
|
||||
| `S` | `( c-addr len u -- )` | Replaces line `u` with the string, padding with spaces or truncating to 64 characters. |
|
||||
| `SHOW` | `( -- )` | Prints the whole screen with line numbers. |
|
||||
| `EDIT` | `( u -- )` | Opens a minimal stdin/stdout line-editor shell on block `u`. |
|
||||
|
||||
## 17. Defining words and the compiler
|
||||
`src/word_source/defining_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `:` **IMM** | `( "name" -- )` | Starts a colon definition and switches to compile mode. The new word stays hidden until `;`. |
|
||||
| `;` **IMM** | `( -- )` | Compiles `EXIT`, ends the definition, reveals the word, and returns to interpret mode. |
|
||||
| `CREATE` | `( "name" -- )` | Makes a header whose runtime pushes its data-field address (`HERE` aligned to a cell). It allocates **no** space, so follow it with `ALLOT` or `,`. |
|
||||
| `VARIABLE` | `( "name" -- )` | Makes a word that pushes the address of one newly allocated cell. |
|
||||
| `CONSTANT` | `( x "name" -- )` | Makes a word that pushes `x`. |
|
||||
| `DOES>` **IMM** | `( -- )` | Inside a defining word, ends the create part. Words later made by that defining word run the code after `DOES>` with their body address on the stack. |
|
||||
| `IMMEDIATE` **IMM** | `( -- )` | Marks the latest definition IMMEDIATE. |
|
||||
| `STATE` | `( -- addr )` | Pushes the address of the compile-state cell (0 means interpreting). |
|
||||
| `[` **IMM** | `( -- )` | Switches to interpret mode inside a definition. |
|
||||
| `]` **IMM** | `( -- )` | Switches to compile mode. |
|
||||
| `LITERAL` **IMM** | `( x -- )` | Compiles `x` so that it is pushed at runtime. Typical use: `[ 6 7 * ] LITERAL`. |
|
||||
| `LIT` | `( -- x )` | Runtime for literals: pushes the next inline cell. The compiler inserts it; you do not call it directly. |
|
||||
| `COMPILE` **IMM** | `( "name" -- )` | Legacy form: compiles a call to `name`. |
|
||||
| `[COMPILE]` **IMM** | `( "name" -- )` | Compiles `name` even when it is IMMEDIATE. |
|
||||
| `FORGET` | `( "name" -- )` | Removes `name` and every newer word and moves `HERE` back. Words below `FENCE` cannot be forgotten. |
|
||||
| `FENCE` | `( -- )` | Moves the `FORGET` boundary up to the current top of the dictionary. A capsule calls it after loading to protect its own words. |
|
||||
| `does_rt` | – | Internal `DOES>` helper that switches the new child word to DODOES. It is registered only so the threaded code can refer to it; do not call it. |
|
||||
|
||||
Example: `: ARRAY ( n "name" -- ) CREATE CELLS ALLOT DOES> ( i -- addr ) SWAP CELLS + ;`
|
||||
|
||||
## 18. Control flow
|
||||
`src/word_source/control_words.c`. Every structure word is **IMM** and **CO**. Branch offsets are in bytes. Up to 64
|
||||
structures can be nested at compile time (`CF_STACK_MAX`).
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `IF` | `( flag -- )` | Runs the following code only if `flag` is non-zero. It compiles `(0BRANCH)`. |
|
||||
| `ELSE` | `( -- )` | Starts the code that runs when the `IF` flag was zero. |
|
||||
| `THEN` | `( -- )` | Ends an `IF` or `IF … ELSE` structure. |
|
||||
| `BEGIN` | `( -- )` | Marks the start of a loop. |
|
||||
| `UNTIL` | `( flag -- )` | Loops back to `BEGIN` while `flag` is zero. |
|
||||
| `AGAIN` | `( -- )` | Loops back to `BEGIN` unconditionally. Leave with `EXIT` or `ABORT`. |
|
||||
| `WHILE` | `( flag -- )` | In `BEGIN … WHILE … REPEAT`, leaves the loop when `flag` is zero. |
|
||||
| `REPEAT` | `( -- )` | Jumps back to `BEGIN` and resolves the exit of `WHILE`. |
|
||||
| `DO` | `( limit start -- ) ( R: -- limit index )` | Starts a counted loop that always runs at least once. |
|
||||
| `?DO` | `( limit start -- )` | Like `DO`, but skips the loop body when `start = limit`. |
|
||||
| `LOOP` | `( -- )` | Adds 1 to the index and loops while `index < limit`. |
|
||||
| `+LOOP` | `( n -- )` | Adds `n` to the index. For `n ≥ 0` it continues while `index < limit`; for `n < 0` it continues while `index ≥ limit`. |
|
||||
| `LEAVE` | `( -- )` | Exits the innermost `DO` loop immediately: it sets the index to the limit and jumps past `LOOP`. |
|
||||
| `I` | `( -- index )` | Pushes the index of the innermost loop. It is not IMMEDIATE. |
|
||||
| `J` | `( -- index )` | Pushes the index of the next outer loop. |
|
||||
| `UNLOOP` | `( -- ) ( R: limit index -- )` | Drops the loop parameters. Use it before `EXIT` inside a `DO` loop. |
|
||||
| `EXIT` | `( -- )` | Returns from the current colon definition. Using it in interpret mode is an error. |
|
||||
| `CASE` | `( x -- x )` | Starts a case structure. |
|
||||
| `OF` | `( x v -- \| x )` | If `x = v`, drops both and runs the clause; otherwise keeps `x` and skips to the next `OF`. |
|
||||
| `ENDOF` | `( -- )` | Ends an `OF` clause and jumps to `ENDCASE`. |
|
||||
| `ENDCASE` | `( x -- )` | Drops the selector and resolves every `ENDOF` jump. |
|
||||
| `(BRANCH)` | `( -- )` | Runtime: unconditional relative branch. The compiler inserts it; you do not call it directly. |
|
||||
| `(0BRANCH)` | `( flag -- )` | Runtime: branches when `flag` is 0. The compiler inserts it; you do not call it directly. |
|
||||
| `(DO)` `(?DO)` | `( limit start -- )` | Runtime for loop entry. The compiler inserts them; you do not call them directly. |
|
||||
| `(LOOP)` `(+LOOP)` | `( -- )` / `( n -- )` | Runtime for loop increment and test. The compiler inserts them; you do not call them directly. |
|
||||
| `(LEAVE)` | `( -- )` | Runtime for `LEAVE`: sets index to limit. The compiler inserts it; you do not call it directly. |
|
||||
|
||||
Examples:
|
||||
```forth
|
||||
: COUNTDOWN ( n -- ) BEGIN DUP . 1- DUP 0= UNTIL DROP ;
|
||||
: TABLE ( -- ) 5 0 DO 5 0 DO I J * 4 .R LOOP CR LOOP ;
|
||||
: COLOR ( n -- ) CASE 0 OF ." red" ENDOF 1 OF ." green" ENDOF ." ?" ENDCASE ;
|
||||
```
|
||||
|
||||
## 19. StarForth extensions
|
||||
`src/word_source/starforth_words.c`. These words are registered in both `FORTH` and the `STARFORTH` vocabulary.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `ENTROPY@` | `( xt -- n )` | Pushes the `execution_heat` counter of a word. The name says "entropy", but the value is execution heat. Registered only in the `STARFORTH` vocabulary. |
|
||||
| `ENTROPY!` | `( n xt -- )` | Sets a word's `execution_heat` counter. Registered only in the `STARFORTH` vocabulary. |
|
||||
| `WORD-ENTROPY` | `( -- )` | Prints the execution heat of every word. |
|
||||
| `RESET-ENTROPY` | `( -- )` | Sets every heat counter to zero. |
|
||||
| `TOP-WORDS` | `( n -- )` | Prints the `n` hottest words. |
|
||||
| `(-` | `( "ccc<)>" -- )` | A comment that marks metadata blocks to extract into `init.4th`. It consumes input up to `)`. |
|
||||
| `INIT` | `( -- )` | Reads `./capsules/core/init.4th`, copies its blocks from block 1 onward, and runs them. |
|
||||
| `VERSION` | `( -- )` | Prints `StarForth v<ver> <arch> <variant> <timestamp>`. |
|
||||
| `SEED` | `( n -- )` | Seeds the PRNG so random sequences can be reproduced. |
|
||||
| `RANDOM` | `( lo hi -- n )` | Pushes a pseudo-random number in `[lo, hi]`, including both ends. |
|
||||
| `WAIT` | `( n -- )` | Waits `n` heartbeat ticks by calling `vm_tick()` `n` times. It counts heartbeats, not wall-clock time, so it behaves the same on amd64, aarch64, and riscv64. |
|
||||
| `HEARTBEAT-TICKS@` | `( -- n )` | Pushes the canonical heartbeat tick count (Loop #7). The project uses this as its clock. It is read-only. |
|
||||
| `ZUSE-AUTHENTICATE` | `( -- )` | Sets `zuse_session = 1`. The write happens only in C. |
|
||||
| `ZUSE-SESSION?` | `( -- flag )` | True if `ZUSE-AUTHENTICATE` has run during this boot. It is read-only. |
|
||||
| `ZUSE-PUBKEY@` | `( i -- u )` | Pushes 8-byte little-endian chunk `i` (0–3) of Zuse's Ed25519 **public** key. An out-of-range `i` pushes 0 and sets an error. The private seed is never exposed. |
|
||||
| `ZUSE-CERT-INSTALLED?` | `( -- flag )` | True once the one-time certificate fuse has been blown. |
|
||||
|
||||
## 20. Word-level ACL
|
||||
`src/word_source/acl_words.c`. Every word takes an `xt`, obtained with `'` or `[']`. Writes are silently ignored for
|
||||
a **pinned** word.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `ACL-MODE@` | `( xt -- mode )` | Pushes the enforcement mode: 0 is STRICT (the decision is permanent) and 1 is TTL (the decision is rechecked when the countdown expires). |
|
||||
| `ACL-MODE!` | `( mode xt -- )` | Sets the enforcement mode. |
|
||||
| `ACL-TTL@` | `( xt -- n )` | Pushes the TTL countdown. At 0 in TTL mode, the interpreter calls `acl_recheck()`. |
|
||||
| `ACL-TTL!` | `( n xt -- )` | Sets the TTL, clamped to `[0, UINT32_MAX]`. |
|
||||
| `ACL-ALLOW@` | `( xt -- flag )` | Pushes the cached decision: `-1` means allowed and `0` means denied. |
|
||||
| `ACL-ALLOW!` | `( flag xt -- )` | Sets the cached decision; any non-zero value means allowed. |
|
||||
| `ACL-PINNED?` | `( xt -- flag )` | True if the ACL fields are pinned and therefore immutable. |
|
||||
| `ACL-PIN` | `( xt -- )` | Pins the word. **This is one-way**: no FORTH word can unpin it. |
|
||||
| `ACL-HEAT@` | `( xt -- heat )` | Pushes the execution heat. `ACL.4th` uses it to calibrate TTLs. |
|
||||
| `ACL-WORD-ID` | `( xt -- id )` | Pushes the word's stable `word_id`. It never changes, so it is safe to use as a table index. |
|
||||
| `ACL-INHERIT` | `( src-xt dst-xt -- )` | Copies the mode from `src` to `dst` and resets `dst`: unpinned, TTL 0, allowed. It is written in C because only C may clear a pin. |
|
||||
| `ACL-INIT-PRIMITIVES` | `( -- )` | For every unpinned word, sets TTL to 0, allow to 1, and mode to TTL. `ACL-BOOT` calls it. |
|
||||
|
||||
## 21. Physics: benchmark and diagnostics
|
||||
`src/word_source/physics_benchmark_words.c`. These are interactive diagnostics that print to the console.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `BENCH-DICT-LOOKUP` | `( iterations -- )` | Benchmarks dictionary lookup and records Q48.16 latencies. Use at least 10,000 iterations; 100,000 is the standard run and 1,000,000 is a stress test. |
|
||||
| `PHYSICS-CACHE-STATS` | `( -- )` | Prints hot-words cache statistics. |
|
||||
| `PHYSICS-TOGGLE-CACHE` | `( -- )` | Turns the hot-words cache on or off, for A/B testing. |
|
||||
| `PHYSICS-RESET-STATS` | `( -- )` | Resets the cache statistics. |
|
||||
| `PHYSICS-BUILD-INFO` | `( -- )` | Prints the variant's build configuration. |
|
||||
| `PHYSICS-BAYESIAN-REPORT` | `( addr -- )` | Prints a Bayesian comparison of the current cache statistics against the baseline stored at `addr`. |
|
||||
|
||||
> `physics_diagnostic_words.c` also defines `PHYSICS-WORD-METRICS`, `PHYSICS-CALC-KNOBS`, `PHYSICS-BURN ( n -- )`
|
||||
> and `PHYSICS-SHOW-FEEDBACK`, but nothing calls `register_physics_diagnostic_words()`, so **none of them are in
|
||||
> the dictionary** at this commit.
|
||||
|
||||
## 22. Physics: pipelining diagnostics
|
||||
`src/word_source/physics_pipelining_diagnostic_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `PIPELINING-SHOW-STATS` | `( "name" -- )` | Prints the word-to-word transition metrics of `name`. |
|
||||
| `PIPELINING-SHOW-TOP-TRANSITIONS` | `( "name" n -- )` | Prints the `n` words that most often follow `name`. |
|
||||
| `PIPELINING-ANALYZE-WORD` | `( "name" -- )` | Prints a full analysis of one word's transitions, with hints for reading them. |
|
||||
| `PIPELINING-STATS` | `( -- )` | Prints pipelining statistics aggregated across the whole dictionary. |
|
||||
| `PIPELINING-RESET-ALL` | `( -- )` | Clears all transition metrics. |
|
||||
| `PIPELINING-ENABLE` | `( -- )` | Placeholder. Pipelining is switched on or off at compile time. |
|
||||
|
||||
## 23. Physics: freeze, heat, and decay
|
||||
`src/word_source/physics_freeze_words.c`. Words are named by `c-addr u` strings, for example `S" DUP" HEAT@`. An
|
||||
unknown name is not an error.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `FREEZE-WORD` | `( c-addr u -- )` | Sets `WORD_FROZEN` on the word, so Loop #3 decay stops lowering its heat. |
|
||||
| `UNFREEZE-WORD` | `( c-addr u -- )` | Clears `WORD_FROZEN` and leaves `WORD_PINNED` alone. |
|
||||
| `FROZEN?` | `( c-addr u -- flag )` | True if the word is frozen. An unknown word gives `0`. |
|
||||
| `HEAT!` | `( heat c-addr u -- )` | Writes `execution_heat` directly, bypassing Loops #1 and #3. For testing only. |
|
||||
| `HEAT@` | `( c-addr u -- heat )` | Reads `execution_heat`. An unknown word gives `0`. |
|
||||
| `SHOW-HEAT` | `( c-addr u -- )` | Prints `NAME: HEAT (frozen) (pinned)`. |
|
||||
| `ALL-HEATS` | `( -- )` | Prints up to 1024 words sorted by heat, hottest first. |
|
||||
| `DECAY-RATE@` | `( -- q )` | Pushes the base decay rate per µs (`DECAY_RATE_PER_US_Q16`) in Q48.16. |
|
||||
| `FREEZE-CRITICAL` | `( -- )` | Freezes 21 core words: `DUP DROP SWAP OVER ROT @ ! C@ C! EXECUTE IF THEN ELSE DO LOOP BEGIN UNTIL REPEAT . EMIT CR`. |
|
||||
|
||||
## 24. Dictionary heat optimisation
|
||||
`src/word_source/dictionary_heat_diagnostic_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `HEAT-PERCENTILES` | `( -- p25 p50 p75 )` | Pushes the current heat percentile thresholds, with the 75th on top. |
|
||||
| `LOOKUP-STRATEGY@` | `( -- n )` | Pushes the lookup strategy: 0 is naive (newest-first linear scan) and 1 is heat-aware (hot bucket first). |
|
||||
| `LOOKUP-STRATEGY!` | `( n -- )` | Sets the strategy. Only 0 or 1 is accepted; other values are silently ignored. |
|
||||
| `REORG-BUCKETS` | `( -- )` | Re-sorts the lookup buckets by heat and refreshes the percentiles immediately, without waiting for the heartbeat. |
|
||||
| `SHOW-HEAT-OPTIMIZATION` | `( -- )` | Prints the strategy, the percentiles, and the hot, warm, and cool zones. |
|
||||
| `COMPARE-LOOKUPS` | `( iterations -- )` | Benchmarks naive against heat-aware lookup and prints the speedup. It restores the original strategy afterwards. |
|
||||
|
||||
## 25. Logging
|
||||
`src/word_source/log_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `LOG-ERROR` `LOG-WARN` `LOG-INFO` `LOG-TEST` `LOG-DEBUG` | `( -- level )` | Push the log level constants. |
|
||||
| `LOG-LEVEL!` | `( level -- )` | Sets the active log filter, clamped to `[LOG-ERROR, LOG-DEBUG]`. |
|
||||
| `LOG-LEVEL@` | `( -- level )` | Pushes the current log level. |
|
||||
| `LOG-ERROR"` `LOG-WARN"` `LOG-INFO"` `LOG-TEST"` `LOG-DEBUG"` **IMM** | `( "ccc<">" -- )` | Log a literal string at that level. In interpret mode the string is logged immediately; when compiling, a runtime word and the inline string are compiled. |
|
||||
| `LOG-ERROR-STR` `LOG-WARN-STR` `LOG-INFO-STR` `LOG-TEST-STR` `LOG-DEBUG-STR` | `( c-addr u -- )` | Log a string taken from the stack at that level. |
|
||||
| `(do-log-error)` `(do-log-warn)` `(do-log-info)` `(do-log-test)` `(do-log-debug)` | `( -- )` | Runtimes for the compiled `LOG-*"` words. The compiler inserts them; you do not call them directly. |
|
||||
| `(LOG-APPEND-RAW)` **K** | `( level timestamp c-addr u -- )` | Appends a raw entry to the kernel log ring, attributed to the calling VM (or `HADES`). It reports errors on the console, not through `log_message`, to avoid recursion. |
|
||||
|
||||
Example: `: CHECK ( n -- ) 0< IF LOG-WARN" negative input" THEN ;`
|
||||
|
||||
## 26. Q48.16 fixed-point math
|
||||
`src/word_source/q48_words.c`. A value is `n × 65536`. The underlying type is **unsigned** `uint64_t`; see
|
||||
[§36](#36-implementation-quirks-to-know) for what that means for negative values.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `Q.+` `Q.-` | `( q1 q2 -- q3 )` | Add and subtract. |
|
||||
| `Q.*` | `( q1 q2 -- q3 )` | Multiplies, computing `(a*b) >> 16`. |
|
||||
| `Q./` | `( q1 q2 -- q3 )` | Divides, computing `(a << 16) / b`. **Division by zero returns 0** and sets no error. |
|
||||
| `Q.ABS` | `( q -- \|q\| )` | Absolute value, treating the top bit as a sign bit. |
|
||||
| `Q.NEG` | `( q -- -q )` | Two's-complement negation. |
|
||||
| `Q.LOG` | `( q -- ln q )` | Natural logarithm by Newton-Raphson. Requires `q > 0`. |
|
||||
| `Q.EXP` | `( q -- e^q )` | Exponential by Taylor series. |
|
||||
| `Q.SQRT` | `( q -- √q )` | Square root by Newton-Raphson. |
|
||||
| `Q.SIN` `Q.COS` | `( q -- q' )` | Sine and cosine of an angle in radians. The argument is reduced to [-π, π] and then a Taylor series is applied. |
|
||||
| `Q.FROM-INT` | `( n -- q )` | Converts an integer to Q48.16 as `n << 16`. **A negative `n` becomes 0.** |
|
||||
| `Q.TO-INT` | `( q -- n )` | Converts to an integer as `q >> 16`, truncating. |
|
||||
| `Q.1` | `( -- 65536 )` | Pushes 1.0. |
|
||||
| `Q.0` | `( -- 0 )` | Pushes 0.0. |
|
||||
| `Q.SCALE` | `( -- 65536 )` | Pushes the scale factor; the same value as `Q.1`. |
|
||||
| `Q.=` | `( q1 q2 -- flag )` | Equality. |
|
||||
| `Q.<` `Q.>` | `( q1 q2 -- flag )` | Comparison, done **unsigned**. |
|
||||
| `Q.0=` | `( q -- flag )` | True if the value is zero. |
|
||||
| `Q.MAX` `Q.MIN` | `( q1 q2 -- q3 )` | Maximum and minimum, compared **unsigned**. |
|
||||
| `Q.PRINT` | `( q -- )` | Prints the value as `int.fffff ` with five fractional digits. |
|
||||
|
||||
Example: `3 Q.FROM-INT Q.SQRT Q.PRINT` prints approximately `1.732`.
|
||||
|
||||
## 27. Inference engine (SSM, L8, and Bayes)
|
||||
`src/word_source/inference_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `INFER-RUN` | `( -- )` | Runs the full inference engine on this VM's rolling window and dictionary heat, and caches the results. |
|
||||
| `INFER-WINDOW@` | `( -- u )` | Pushes the last inferred optimal window width. |
|
||||
| `INFER-DECAY@` | `( -- q )` | Pushes the last inferred decay slope. |
|
||||
| `INFER-VARIANCE@` | `( -- q )` | Pushes the last inferred variance. |
|
||||
| `INFER-FIT@` | `( -- q )` | Pushes the last fit quality. |
|
||||
| `INFER-EARLY-EXIT@` | `( -- flag )` | True if the last run exited early. |
|
||||
| `Q.VARIANCE` | `( addr u -- q )` | Pushes the variance of `u` uint64 cells at `addr`. |
|
||||
| `INFER-DECAY-SLOPE` | `( addr u -- q )` | Fits a decay slope to the array by linear regression. |
|
||||
| `INFER-WINDOW-WIDTH` | `( addr u -- n )` | Computes the optimal window width for the array. |
|
||||
| `WINDOW-DIVERSITY` | `( -- u )` | Pushes the number of distinct words in the rolling window. |
|
||||
| `L8-MODE` | `( -- n )` | Pushes the current L8 (legacy 16-mode) Jacquard selection. |
|
||||
| `L8-UPDATE` | `( entropy cv temporal stability -- )` | Feeds four Q48.16 metrics to `ssm_l8_update()`. `stability` is on top of the stack. |
|
||||
| `L8-APPLY` | `( -- )` | Applies the currently selected L8 mode. |
|
||||
| `L8-TABLE-FORCE` | `( idx -- )` | Forces the adaptive 128-config table to `idx & 127` as though the UCB bandit had picked it, and applies it. Unlike `L8-UPDATE` and `L8-APPLY`, this choice is not overwritten at the next heartbeat trial. Use it for DoE campaigns. |
|
||||
| `BAYES-CACHE-MEAN` `BAYES-CACHE-LOWER` `BAYES-CACHE-UPPER` | `( -- q )` | Push the posterior mean latency and the 95 % credible bounds for hot-words cache hits. |
|
||||
| `BAYES-BUCKET-MEAN` `BAYES-BUCKET-LOWER` `BAYES-BUCKET-UPPER` | `( -- q )` | Push the same three values for bucket searches. |
|
||||
|
||||
## 28. DEFER and IS
|
||||
`src/word_source/defer_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `DEFER` | `( "name" -- )` | Creates a vectored word. Running it before an action has been set with `IS` sets `vm->error`. |
|
||||
| `IS` | `( xt "name" -- )` | Sets the action of `name`. It is refused unless `name` was created by `DEFER`. |
|
||||
| `DEFER@` | `( "name" -- xt )` | Pushes the current action of a deferred word. |
|
||||
|
||||
Example: `DEFER GREET : HI ." hi" ; ' HI IS GREET GREET`
|
||||
|
||||
## 29. Framebuffer (Hestia only)
|
||||
`src/word_source/framebuffer_words.c`. These words are registered only in the Hestia VM, by `capsule_birth_baby()`,
|
||||
and not by `register_forth79_words()`.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `PLOT` | `( x y color -- )` | Writes one raw pixel. The origin is top-left and Y increases downward. |
|
||||
| `FB-WIDTH` | `( -- n )` | Pushes the framebuffer width in pixels. |
|
||||
| `FB-HEIGHT` | `( -- n )` | Pushes the framebuffer height in pixels. |
|
||||
|
||||
## 30. Keyboard
|
||||
`src/word_source/keyboard_words.c`. These are diagnostics for the console fabric. On a platform without the device,
|
||||
each word pushes `0`.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `KBD-SCAN` | `( -- sc -1 \| 0 )` | amd64 i8042: pops one raw XT scancode from the queue, if there is one. |
|
||||
| `KBD-DEBUG` | `( -- isr spurious )` | amd64: pushes the i8042 interrupt count and the spurious-interrupt count. |
|
||||
| `VKBD-EVENT` | `( -- code value -1 \| 0 )` | riscv64 and aarch64 virtio-input: pops one Linux-style `EV_KEY` code and value. |
|
||||
| `VKBD-DEBUG` | `( -- isr )` | riscv64 and aarch64: pushes the virtio-input interrupt count. |
|
||||
| `KEY-EVENT` | `( -- keycode pressed -1 \| 0 )` | The unified event on every architecture: pops one keycode with its pressed (1) or released (0) state. |
|
||||
| `ALT+TAB` | `( -- )` | Switches the console between text and graphics, the same as the physical Alt+Tab. |
|
||||
|
||||
## 31. TrueType text
|
||||
`src/word_source/ttf_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `TTF-TEXT` **K** | `( c-addr u x y size color -- )` | Draws the string with the TrueType renderer at pixel `(x, y)` in the given size and color. |
|
||||
|
||||
## 32. REPL scrollback
|
||||
`src/word_source/scroll_words.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `SCROLL-BACK` **K** | `( n -- )` | Scrolls the REPL view back `n` lines. |
|
||||
| `SCROLL-FWD` **K** | `( n -- )` | Scrolls the REPL view forward `n` lines, toward the live output. |
|
||||
|
||||
## 33. Kernel REPL and DoE hooks
|
||||
`src/starkernel/repl.c` and `src/starkernel/doe_log.c`
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `HB-ON` **K** | `( -- )` | Turns on per-tick DoE instrumentation. |
|
||||
| `HB-OFF` **K** | `( -- )` | Turns off per-tick DoE instrumentation. |
|
||||
| `BLK-ATTACH-ACK` **K** | `( dev-ptr ok? -- )` | The acknowledgement Artemis sends to Hera after `BLK-ATTACH`, delivered with `VM-EXEC`. On success, Hera runs the deferred Zuse or WIREBIND attach. |
|
||||
| `KH-BLK-ATTACH-SEND` **K** | `( c-addr u -- ok? )` | Sends a payload to Hera through kernel-Hermes. The sender and receiver are worked out in C, so the caller cannot spoof them. A refused send is logged. |
|
||||
| `KH-ELEVATE-SEND` **K** | `( c-addr u -- ok? )` | The same mechanism for elevation requests. Nothing calls it at present. |
|
||||
|
||||
## 34. Hera (Mama) and child-VM words
|
||||
`src/starkernel/capsule/mama_forth_words.c`. These are **K** only. Hera receives every word in this section, in
|
||||
both `FORTH` and the `MAMA` vocabulary. Child VMs receive only `STOP EXEC USE BIRTH CAPSULE-BIRTH VM-EXEC VM-CALL
|
||||
VM-HEAT VM-ERROR? SWITCH-MARK-WORK` and the `STADIUM-*` words. VM names are matched without regard to case.
|
||||
|
||||
### VM lifecycle
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `BIRTH` | `( c-addr u -- )` | Births a VM from its capsule (`S" Artemis"` loads `artemis:init.4th`). If the VM is already live, nothing happens. `Hera` is rejected. |
|
||||
| `KILL` | `( c-addr u -- )` | Destroys a VM. Hera cannot be killed, and killing a dead VM does nothing. |
|
||||
| `START` | `( c-addr u -- )` | Enters the VM's REPL and blocks until it runs `STOP` or `BYE`. It refuses a VM that is LIVE, DEAD, or STILLBORN. |
|
||||
| `STOP` | `( -- )` | Halts the current VM, so its REPL loop returns. |
|
||||
| `USE` | `( c-addr u -- )` | Redirects console input to the named VM without nesting the C stack, and changes the prompt to `[Name]`. `S" Hera" USE` switches back to Hera. |
|
||||
| `EXEC` | `( c-addr u -- )` | Runs a named capsule inside the current VM. |
|
||||
| `EJECT` | `( -- )` | Cleanly detaches the identity attached through USB home blocks: it flushes the user VM and kills it, or logs Zuse out. |
|
||||
| `CONNECT-HERMES` | `( -- )` | Enters Hermes's REPL, birthing Hermes first if needed. |
|
||||
| `CONNECT-ARTEMIS` | `( -- )` | Enters Artemis's REPL, birthing Artemis first if needed. |
|
||||
| `BYE` | `( -- )` | **On Hera:** reaps every child and then cold-resets the machine. |
|
||||
|
||||
### Cross-VM execution (compudynamics)
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `VM-EXEC` | `( cmd-a cmd-u name-a name-u -- )` | Injects a command into the named VM and runs it immediately without blocking. Example: `S" DOE-WORK" S" Hermes" VM-EXEC`. |
|
||||
| `VM-CALL` | `( cmd-a cmd-u name-a name-u -- n )` | Like `VM-EXEC`, then pops the target's top of stack onto the caller's stack. If the target left nothing, it pushes 0 and sets an error. |
|
||||
| `VM-STEP` | `( c-addr u -- )` | Gives the named VM one REPL turn: one prompt, one line, then it returns. |
|
||||
| `VM-HEAT` | `( c-addr u -- q )` | Pushes the VM's `execution_heat_q48`. An unknown VM gives 0 and prints nothing. |
|
||||
| `VM-ERROR?` | `( c-addr u -- flag )` | True if the VM has `vm->error` set. An unknown VM gives `0`. |
|
||||
| `VM-COUNT` | `( -- n )` | Pushes the number of registered VMs. |
|
||||
| `VM-CONSERVED?` | `( -- flag )` | True if the total fleet heat is within ε of `Q.1`. |
|
||||
| `VM-PHYSICS-STATUS` | `( -- )` | Prints the fleet physics report. |
|
||||
| `SWITCH-MARK-WORK` | `( c-addr u -- )` | Marks a VM as having work, which makes it eligible for a context switch. The message path calls it, and it ignores bad names silently. |
|
||||
| `MAMA-VM-ID` | `( -- 0 0 )` | Pushes Hera's 128-bit VM ID, which is all zeros. |
|
||||
| `NAME>XT` | `( c-addr u -- xt \| 0 )` | Looks up a name held in a data buffer. A miss gives `0`. |
|
||||
|
||||
### Capsules
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `CAPSULE-COUNT` | `( -- n )` | Pushes the number of entries in the capsule directory. |
|
||||
| `CAPSULE@` | `( idx -- desc \| 0 )` | Pushes the descriptor for the capsule at `idx`. |
|
||||
| `CAPSULE-HASH@` | `( desc -- hash )` | Pushes the capsule's content hash. |
|
||||
| `CAPSULE-FLAGS@` | `( desc -- flags )` | Pushes the capsule's flags. |
|
||||
| `CAPSULE-LEN@` | `( desc -- len )` | Pushes the capsule's payload length. |
|
||||
| `CAPSULE-BIRTH` | `( id -- vmid-lo vmid-hi )` | Births an unnamed VM from a production (p) capsule and pushes its 128-bit ID. On failure both cells are all ones. |
|
||||
| `CAPSULE-RUN` | `( id -- )` | Runs an experiment (e) capsule on Hera. |
|
||||
| `CAPSULE-TEST` | `( -- )` | Prints a message confirming the capsule system is running. |
|
||||
| `WORKER-BIRTH` | `( cap-a cap-u name-a name-u -- ok? )` | Births a named, `VM-EXEC`-addressable worker from any p-capsule, with no identity attached. |
|
||||
| `UNATTENDED-BIRTH` | `( cap-a cap-u name-a name-u -- ok? )` | Births a VM and installs the verified identity whose `UNATTENDED-ID-UUID` and `UNATTENDED-ID-CERT` the capsule defined. |
|
||||
| `CONSOLE-ATTACH` | `( name-a name-u -- ok? )` | Pairs a new console VM with the live VM registered as `<name>~user`. |
|
||||
|
||||
### Identity, Zuse, and diagnostics
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `MINT` | `( fn-a fn-u un-a un-u em-a em-u ph-a ph-u restrict? -- ok? )` | Mints an identity onto the attached USB drive. The full name and username are required; pass an empty string for email or phone to leave them out. A non-zero `restrict?` selects the locked-down personality that allows only FORTH-79 and FORTH-83 words. |
|
||||
| `MINT-SCRATCH` | same as `MINT` | Mints onto the scratch device instead, prints the UUID, and pushes 1 or 0. |
|
||||
| `MINT-SCRATCH-EMIT` | `( -- ok? )` | Prints the last scratch mint as FORTH source (`CREATE UNATTENDED-ID-UUID` / `-CERT` byte lists) for copying by hand. It refuses (pushes 0) if no mint has succeeded. |
|
||||
| `ZUSE-ELIGIBILITY-ADD` | `( c-addr -- ok? )` | Adds the 32-byte Ed25519 public key at `c-addr` to Zuse's elevation list. |
|
||||
| `ZUSE-ELIGIBLE?` | `( c-addr -- flag )` | Checks whether a key is on the list. It fails closed. |
|
||||
| `ELEVATE-PUBKEY-UNPACK` | `( pk0 pk1 pk2 pk3 buf -- )` | Rebuilds a 32-byte public key from four cells. It is the inverse of `ZUSE-PUBKEY@`. |
|
||||
| `RUNCAP-TEST` | `( c-addr u -- ok? rc )` | Diagnostic: runs `capsule_runcap_birth()` against the current home-blocks drive. |
|
||||
| `PAIR-TEST` | `( c-addr u -- ok? )` | Diagnostic: births a console VM and a `<name>~user` VM as a pair. |
|
||||
|
||||
### Stadium (heat accounting)
|
||||
Heat values are Q48.16. Each word works only on the calling VM's own quota.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `STADIUM-ADMIT` | `( identity heat behaviour -- cell \| -1 )` | Admits a patron. `behaviour` must be 0–3. |
|
||||
| `STADIUM-EVICT` | `( cell -- flag )` | Reaps the patron in `cell`. It is refused if the cell is out of range, not resident, pinned, or blocked by what it contains. |
|
||||
| `STADIUM-HEAT@` | `( cell -- heat )` | Reads a resident cell's heat. A cell that is not the caller's gives 0. |
|
||||
| `STADIUM-HEAT!` | `( heat cell -- )` | Writes a cell's heat, pulling the difference from the reservoir or pushing it back. An increase the reservoir cannot cover is silently refused. |
|
||||
| `STADIUM-RES@` | `( -- heat )` | Pushes the VM's reservoir balance. |
|
||||
| `STADIUM-RES-PULL` | `( qty -- got )` | Pulls up to `qty` from the reservoir and pushes the amount actually taken. |
|
||||
| `STADIUM-RES-PUSH` | `( heat -- )` | Credits heat back to the reservoir. |
|
||||
| `STADIUM-WORD-HEAT` | `( -- heat )` | Pushes the total heat held by this VM's word-execution residents. |
|
||||
|
||||
## 35. Hosted lifecycle stubs
|
||||
`src/word_source/lifecycle_words_hosted.c`. These are **H** only; `main.c` registers them. Each word only logs
|
||||
`"<WORD> <name> (hosted)"`. They exist so that capsule scripts also parse in hosted builds.
|
||||
|
||||
| Word | Stack | Description |
|
||||
|---|---|---|
|
||||
| `BIRTH` `KILL` `PAUSE` `RESUME` `USE` | `( c-addr u -- )` | No-op stubs that only write a log line. |
|
||||
|
||||
---
|
||||
|
||||
## 36. Implementation quirks to know
|
||||
|
||||
These behaviours differ from what a FORTH-79 or ANS programmer would expect. Each was checked against the C source.
|
||||
|
||||
1. **`ROLL` counts from the bottom of the stack.** With `1 2 3` on the stack, `1 ROLL` gives `2 3 1`; ANS gives
|
||||
`1 3 2`. The test suite (`stack_words_test.c`) asserts the current behaviour, so it looks intended. Portable code
|
||||
should use `SWAP` and `ROT`.
|
||||
2. **`PICK` is 0-based,** as in ANS. FORTH-79's `PICK` was 1-based.
|
||||
3. **`FIND` parses the input stream.** It does not take a counted string. Use `(FIND)` for a counted string or
|
||||
`NAME>XT` (kernel only) for a name in a buffer.
|
||||
4. **The Q48.16 type is unsigned.** `Q.<`, `Q.>`, `Q.MIN`, `Q.MAX` and `Q.PRINT` treat a negative Q value as a huge
|
||||
positive one, and `Q.FROM-INT` turns a negative integer into 0. `Q.ABS` and `Q.NEG` do treat the top bit as a sign.
|
||||
5. **`Q./` by zero returns 0 without setting an error,** while the integer `/`, `MOD`, and `*/` all set `vm->error`.
|
||||
6. **`[LITERAL]` does nothing,** and `LITERAL` works only because §17 registers it again after the placeholder.
|
||||
7. **`MOD`, `/MOD`, `*/`, and `*/MOD` are registered twice.** The mixed-arithmetic versions (§6) are the ones used.
|
||||
8. **Four `PHYSICS-*` diagnostic words are not registered.** `PHYSICS-WORD-METRICS`, `PHYSICS-CALC-KNOBS`,
|
||||
`PHYSICS-BURN` and `PHYSICS-SHOW-FEEDBACK` are defined in C but never added to the dictionary.
|
||||
9. **`does_rt` is a visible dictionary entry.** It is an internal helper; do not call it.
|
||||
10. **The `STARFORTH` vocabulary registers its words twice** (once in `FORTH`, once in `STARFORTH`). Hera does the
|
||||
same with `MAMA`. As a result, those names appear twice in `WORDS` output.
|
||||
@@ -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 $@"
|
||||
@@ -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 |
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
$if(highlighting-macros)$
|
||||
$highlighting-macros$
|
||||
$endif$
|
||||
@@ -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
|
||||
@@ -1,37 +1,26 @@
|
||||
# FABRIC-3.5.md — the Tripod/kernel reshuffle
|
||||
|
||||
**Status: REOPENED 2026-09-19, by direct instruction ("reopen 3.5 and write it up as a gap
|
||||
analysis section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set. The design
|
||||
phase remains closed; §XXXI adds findings, not new design.**
|
||||
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** Per §XXVI.5's own close
|
||||
condition ("this document closes when the tag exists"). Produced the full design ruling for
|
||||
the Tripod/kernel reshuffle: Hermes moves from a FORTH VM into the kernel as kernel-Hermes; the
|
||||
Tripod becomes Hera/Artemis/Hestia; every design question this document opened is ruled (§XXX.7
|
||||
/ §XLI's punch list). Execution against that design is recorded in `FABRIC-3.6.md`, not here —
|
||||
all five phases (0–5) closed there, ending in the `v2.1.0` tag this header names. **Nothing is
|
||||
carried forward and no successor is created** — this document is not part of the
|
||||
`FABRIC-0 → -1 → -2 → -3` chain (§XXVI.5), so closing it hands nothing to a successor.
|
||||
**`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare metal boot);
|
||||
nothing here ever superseded it.
|
||||
|
||||
> **Prior status, kept rather than overwritten: DESIGN PHASE CLOSED as of 2026-09-19, by
|
||||
> direct instruction ("close the document for now"). Not yet archival.** That close stood for
|
||||
> the duration of the sweep and its substance is unchanged — every design question was and
|
||||
> remains ruled. The reopen is recorded rather than the close deleted, per this series' own
|
||||
> rule against silently rewriting a prior state.
|
||||
|
||||
**What is closed, and what is deliberately not.** Every design question this document opened is
|
||||
ruled — see §XXX.7. **No code has been written and no code is authorized.** The execution
|
||||
sequence (§XXVI.6, as amended by §XXX.7) is untouched: the surgical strip, the build, the
|
||||
Isabelle/HOL pass, the documentation sweep, the SBOM, the merge to `master`, and the `v2.1.0`
|
||||
tag all remain ahead.
|
||||
|
||||
**This is therefore a narrower close than §XXVI.5 specified**, and the difference is stated
|
||||
rather than glossed. §XXVI.5 ruled that this document closes *when the tag exists*, in
|
||||
`FABRIC-2.md`'s CLOSED/ARCHIVAL form, naming the tag it closed at. **The tag does not exist**,
|
||||
so claiming that close would be a label running ahead of the real state — the exact error
|
||||
`FABRIC-3.md` §I.2 corrected when it rolled `LITHOS_VERSION` back, and the same standard §XXIX
|
||||
applied to the LTS question. **The archival close specified by §XXVI.5 still stands and still
|
||||
happens at `v2.1.0`.** This header is the provisional one the instruction's own "for now" asks
|
||||
for.
|
||||
|
||||
**Nothing is carried forward and no successor is created.** As §XXVI.5 establishes, this
|
||||
document is not part of the `FABRIC-0 → -1 → -2 → -3` chain, so closing it hands nothing to a
|
||||
successor. **`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare
|
||||
metal boot); nothing here supersedes it. The live artifact from this document is its punch list
|
||||
(§XXVI.6 / §XXX.7) — that, not this header, is what the build works from.
|
||||
|
||||
**Open by design, not by omission:** items 10–17 of §XXVI.6 are execution, not decisions.
|
||||
> **Prior status headers, kept rather than overwritten, per this series' own rule against
|
||||
> silently rewriting a prior state:**
|
||||
>
|
||||
> **REOPENED 2026-09-19**, by direct instruction ("reopen 3.5 and write it up as a gap analysis
|
||||
> section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set.
|
||||
>
|
||||
> **DESIGN PHASE CLOSED as of 2026-09-19**, by direct instruction ("close the document for
|
||||
> now"). Not yet archival at that point — the execution sequence (surgical strip, build,
|
||||
> Isabelle pass, doc sweep, SBOM, merge, tag) was still entirely ahead. That gap is now closed:
|
||||
> `FABRIC-3.6.md`'s Phases 0–5 are the execution this header once described as pending.
|
||||
Three things recorded here are explicitly *outside* this reshuffle and still need their own
|
||||
authorization — the `.claude/CLAUDE.md`/`MANIFEST.md` documentation reconciliation (§XXVI.1),
|
||||
the `src/*.c.bak` hygiene question (§XXII.5), and the stray `refs/heads/v2.0.1` branch and
|
||||
@@ -1,6 +1,24 @@
|
||||
# FABRIC-3.6.md — the Tripod/kernel reshuffle: execution log
|
||||
|
||||
> ## START HERE — session handoff
|
||||
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** All five phases (0–5) closed —
|
||||
Category A strip, Hestia relocation/birth, kernel-Hermes build (allocator, arena, message
|
||||
types), Stage-cutover of every FORTH-owned message type, the Category B strip (Hermes VM +
|
||||
`messaging.4th` removed), the Isabelle pass, the documentation sweep, `make sbom`, the version
|
||||
bump, and the merge to `master` + tag. **This document is closed; the reshuffle it tracked is
|
||||
done, not merely planned.** Per §XXVI.5 (cited here, ruled in `FABRIC-3.5.md`, also closed at
|
||||
this same tag): closing this document hands nothing to a successor and does not touch
|
||||
`FABRIC-3.md`, which remains open and authoritative for its own topic (bare metal boot).
|
||||
|
||||
**The `START HERE` section immediately below is kept as historical record of how this
|
||||
execution began — it describes a session about to start work, not the current state.** Do not
|
||||
follow its "first action: task 0.0" instruction; every task it points at is closed. If future
|
||||
work touches this fleet again (a Phase 8 PKI elevation entrypoint, further hardware bring-up,
|
||||
etc.), it gets its own new document, not a reopening of this one — matching the discipline
|
||||
`FABRIC-3.5.md`'s own close just followed.
|
||||
|
||||
---
|
||||
|
||||
> ## START HERE — session handoff (historical — reshuffle complete, see status above)
|
||||
>
|
||||
> **If you have just been told "go build it", read this section, then `FABRIC-3.5.md`'s §XLI,
|
||||
> then start at task 0.0 below. Do not re-derive the design — it is settled.**
|
||||
@@ -1345,7 +1363,7 @@ intermediate states.
|
||||
build its own entrypoint rather than resuming `SEND-ELEVATE-REQUEST`. No further action
|
||||
this phase.
|
||||
|
||||
## Phase 5 — Close-out
|
||||
## Phase 5 — Close-out — **COMPLETE, tag `v2.1.0` cut 2026-09-22**
|
||||
|
||||
**Note on commit granularity:** tasks 5.1–5.4 landed as one commit (`a4ad14a`), a deliberate
|
||||
deviation from the per-task-commit discipline this document has otherwise used since Phase 0.
|
||||
@@ -1511,8 +1529,27 @@ opposite of what per-task commits are for. Tasks 5.5–5.7 return to one-commit-
|
||||
closed `2026-09-18T21:28:26Z` — 20 seconds later, before any of this reshuffle's actual
|
||||
work existed on the branch. Already closed, already not merged, blocks nothing. Nothing
|
||||
to resolve beyond recording what it is.
|
||||
- [ ] **5.6** — Merge to `master`; tag `v2.1.0`.
|
||||
- [ ] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
|
||||
- [x] **5.6** — Merge to `master`; tag `v2.1.0`.
|
||||
2026-09-22 · **Closed, explicit approval obtained first (this is a shared-branch
|
||||
operation on the sole production line — the plan authorizing it is not a standing
|
||||
grant, per `.claude/CLAUDE.md`'s own scope rule).** `master` was a strict ancestor of
|
||||
this branch (`git merge-base --is-ancestor origin/master HEAD` — true, 0 commits unique
|
||||
to `master`, 85 unique to this branch), so this was a clean fast-forward, not a merge
|
||||
commit: `git push origin HEAD:refs/heads/master` (`e56974e..8edbd3c`), never checking out
|
||||
`master` locally, so the uncommitted `FABRIC-3.md` WIP on this branch's working tree was
|
||||
never touched. Tagged `v2.1.0` (annotated, `8edbd3c`) and pushed. `refs/heads/v2.0.1`
|
||||
(task 5.5's finding — fully merged, safe to delete) also deleted this same pass, explicit
|
||||
approval obtained first.
|
||||
- [x] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
|
||||
2026-09-22 · **Closed.** `FABRIC-3.5.md`'s provisional "design phase closed, not yet
|
||||
archival" header replaced with the real CLOSED/ARCHIVAL form, naming `v2.1.0`, per its
|
||||
own §XXVI.5 spec — prior status headers kept underneath, not deleted, matching this
|
||||
series' own no-silent-rewrite rule. This document (`FABRIC-3.6.md`) closed the same way:
|
||||
a CLOSED/ARCHIVAL status banner added above the `START HERE` section, which is kept as
|
||||
historical record of how the session began rather than removed. Neither closure triggers
|
||||
the `FABRIC-0 → -1 → -2 → -3` carry-forward chain (both documents are standalone topic
|
||||
documents, not part of it) and neither touches `FABRIC-3.md`, which stays open for its
|
||||
own topic. **Phase 5, and the Tripod/kernel reshuffle itself, are complete.**
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint
|
||||
|
||||
**Status: OPEN — design only, no code written or authorized.**
|
||||
|
||||
**CORRECTION (2026-09-22, before any code was written against this document): §2's central
|
||||
claim — that the old `SEND-ELEVATE-REQUEST` passed a raw cross-VM address into `ELEVATE-GRANT`'s
|
||||
`waddr` — is wrong.** Found while starting Part A's implementation: reading the actual deleted
|
||||
source (`git show 3e201c8^:capsules/common/messaging.4th`, block 5040) shows
|
||||
`SEND-ELEVATE-REQUEST` copied the target word's **name as literal character bytes** (via
|
||||
`ELEVATE-REQ-APPEND`'s `CMOVE`) into a scratch buffer, building the text `S" <name-text>" <pk0>
|
||||
<pk1> <pk2> <pk3> ELEVATE-GRANT`, and sent *that whole string* to Hera. When Hera's own
|
||||
interpreter runs `S" <name-text>"`, it allocates a fresh string **in Hera's own memory** and
|
||||
pushes Hera's own valid address — no numeric cross-VM address ever appears anywhere in this
|
||||
flow. §2 was written from `ELEVATE-GRANT`'s signature alone, assuming the caller forwarded a raw
|
||||
address, without first reading how the caller actually built its message. It didn't.
|
||||
|
||||
**What is still real, much narrower than originally claimed:** if a future caller ever spliced
|
||||
*attacker-influenced* text into the name field without checking for an embedded `"` character,
|
||||
that could break out of the `S" ... "` literal early and inject arbitrary FORTH source, executed
|
||||
with Hera's privilege. That's an input-validation discipline question for whoever writes the new
|
||||
caller (validate: no embedded `"`, or just always use a compile-time-fixed literal name, never a
|
||||
runtime-supplied one) — not an architectural cross-VM-memory defect requiring the buffer/message
|
||||
redesign §3 originally called for. **§3's proposed mechanism (Hera-side fixed receive buffer,
|
||||
kernel-constructed integer-literal-only command) is not needed** — the original text-copy
|
||||
design was already safe against the bug as actually diagnosed. Kept below, struck through, for
|
||||
traceability, per this series' own rule against silently rewriting a prior state.
|
||||
|
||||
Successor to
|
||||
`FABRIC-3.5.md`/`FABRIC-3.6.md` (both CLOSED/ARCHIVAL at `v2.1.0`) for exactly one topic: the
|
||||
Ed25519-challenge-response elevation entrypoint that `.claude/CLAUDE.md`'s ACL section names as
|
||||
Phase 8, the last open item in the word-level ACL system. This is a **new document**, not a
|
||||
reopening of `FABRIC-3.6.md` — that document's own close header says future fleet work gets its
|
||||
own document, and this is that.
|
||||
|
||||
**Provenance.** Written 2026-09-22, immediately after `FABRIC-3.6.md`'s close, from a design
|
||||
conversation with Captain Bob about a security concern he raised directly: how to rebuild the
|
||||
elevation entrypoint that Phase 4's Category B strip left dangling, without reopening a hole.
|
||||
The design below was proposed, and Captain Bob asked for it in writing here rather than left
|
||||
only in session memory.
|
||||
|
||||
---
|
||||
|
||||
## 1. What's dangling, and why
|
||||
|
||||
`FABRIC-3.6.md` Phase 4 (Category B strip, 2026-09-22) deleted `capsules/common/messaging.4th`
|
||||
after every FORTH-owned message type had been cut over to kernel-Hermes. One casualty was
|
||||
collateral, not intended: `SEND-ELEVATE-REQUEST` (`messaging.4th` block 5040) was the only
|
||||
caller of both `KH-ELEVATE-SEND` (`src/starkernel/repl.c`) and, transitively, `ELEVATE-GRANT`
|
||||
(`capsules/zuse-eligibility.4th`, blocks 4021–4022). All three still exist in the tree.
|
||||
`ELEVATE-GRANT` is still loaded at boot (`capsules/init.4th:18`). Nothing can call it any more.
|
||||
|
||||
Captain Bob's decision at the time (`FABRIC-3.6.md`'s own Phase 4 entry): leave it unreachable,
|
||||
don't patch a caller back in as part of that strip. Phase 8 builds its own entrypoint instead of
|
||||
resuming this one. **This document is that entrypoint's design.**
|
||||
|
||||
<details>
|
||||
<summary>Original §2/§3 (WRONG — see the correction at the top of this document; kept for
|
||||
traceability, not current design)</summary>
|
||||
|
||||
### 2. The security hole in the old mechanism — found before any code was written
|
||||
|
||||
`ELEVATE-GRANT`'s signature, unchanged since it was written:
|
||||
|
||||
```
|
||||
ELEVATE-GRANT ( waddr wu pk0 pk1 pk2 pk3 -- )
|
||||
```
|
||||
|
||||
`waddr`/`wu` are an address/length pair meant to point at the string naming the word to elevate.
|
||||
`pk0`–`pk3` are the caller's Ed25519 pubkey, packed 8 bytes per cell (`ELEVATE-PUBKEY-UNPACK`,
|
||||
`mama_forth_words.c`).
|
||||
|
||||
**The old `SEND-ELEVATE-REQUEST` computed `waddr` in the *sending* VM's own address space, but
|
||||
`ELEVATE-GRANT` always executes on Hera** (`ELEVATE-GRANT always runs on Hera` — `repl.c`'s own
|
||||
comment on `KH-ELEVATE-SEND`, still there). A word's name string lives in the sending VM's
|
||||
memory. `ELEVATE-GRANT` dereferences `waddr` in Hera's memory. Those are not the same address
|
||||
space by construction — `vaddr_t` is per-VM.
|
||||
|
||||
**Consequence:** whoever controls `waddr` controls what bytes `NAME>XT` reads and resolves as a
|
||||
word name, in Hera's dictionary, not the caller's. This is not "the string might be malformed" —
|
||||
it is a primitive for making Hera's own `ELEVATE-GRANT` grant `ACL-ALLOW!`/`ACL-TTL!` on
|
||||
*whatever dictionary entry the attacker's chosen `waddr` happens to land on*, regardless of what
|
||||
word name the caller claims to be requesting elevation for. A caller who can influence `waddr`
|
||||
at all — not forge a signature, not defeat `zuse_eligibility_is_member()`, just choose a number
|
||||
— has a privilege-escalation primitive against the fleet governor.
|
||||
|
||||
This was never exploited (the entrypoint has had zero live callers since the file that called it
|
||||
was deleted), and is reported here as a design defect found by inspection, not a live incident.
|
||||
|
||||
### 3. The fix: never cross an address, only ever cross bytes
|
||||
|
||||
This project already solved the general version of this problem once, this same session
|
||||
(`FABRIC-3.6.md` tasks 3.8/3.9, the payload-aliasing fix): a kernel-Hermes message's payload
|
||||
must be **copied into the message's own storage**, never a pointer into the sender's memory that
|
||||
might be reused or freed before the receiver drains it. `SkHermesMessage.payload_buf`
|
||||
(`include/starkernel/vm/kernel_hermes.h:160`, `SK_HERMES_CHUNK_MAX_PAYLOAD` = 1024 bytes) is
|
||||
exactly that fix, already built, already proven on all three architectures.
|
||||
|
||||
**The elevation entrypoint's hole is the same defect one level up: an address crossing a
|
||||
boundary it isn't valid on the other side of.** The fix generalizes directly:
|
||||
|
||||
1. **Never send `waddr`/`wu` across the kernel-Hermes boundary.** Send the pubkey (32 bytes,
|
||||
already the right shape for `payload_buf`) and the target word's **name, as literal bytes**,
|
||||
copied inline into the message payload — not an address, the actual characters. This is
|
||||
already how `CONSOLE-CMD-EVENT`'s payload works (a command string's bytes, not a pointer to
|
||||
one), so this isn't a new pattern, it's applying the existing one to the one caller that
|
||||
still passed a raw address.
|
||||
|
||||
2. **On receipt, kernel-Hermes's C drain-checkpoint copies those name bytes into a small,
|
||||
fixed, kernel-owned buffer that already lives in Hera's own VM memory** — a receive-side
|
||||
mirror of the existing send-side pattern (`g_kh_elevate_buf`, `repl.c:445`, is the
|
||||
already-built precedent for "a static buffer this mechanism owns"; this needs its Hera-side
|
||||
counterpart). The buffer's address is a compile-time constant, known to the kernel, never
|
||||
computed from anything the caller supplied.
|
||||
|
||||
3. **The FORTH command handed to `vm_interpret()` on Hera references only that fixed buffer's
|
||||
address and length as plain integer literals.** Both are always kernel-controlled. Neither is
|
||||
ever derived from caller input. `ELEVATE-GRANT` itself does not change — same signature, same
|
||||
`zuse_eligibility_is_member()` check, same `ACL-ALLOW!`/`ACL-TTL!` grant. Policy logic stays
|
||||
in FORTH, per `ACL.4th`'s own rule (no new C primitive for policy) — this fix is entirely
|
||||
about how bytes get from one VM to another, not about who is allowed to grant what.
|
||||
|
||||
**Why this closes the hole structurally, not by validation:** there is no string to sanitize and
|
||||
no address to bounds-check, because the interpreted command never contains anything an attacker
|
||||
touched except opaque data bytes that get copied, never dereferenced as a pointer, by the
|
||||
receiving side. The class of bug (cross-address-space pointer confusion) becomes impossible by
|
||||
construction, the same way `payload_buf` made use-after-free impossible by construction rather
|
||||
than by careful lifetime tracking.
|
||||
|
||||
</details>
|
||||
|
||||
## 2 (corrected). What the old mechanism actually did, and the one real gap in it
|
||||
|
||||
Re-read from the actual deleted source (`git show 3e201c8^:capsules/common/messaging.4th`,
|
||||
blocks 5039–5040): `SEND-ELEVATE-REQUEST ( pk3 pk2 pk1 pk0 waddr wu -- )` used `waddr`/`wu` only
|
||||
to `CMOVE` the target word's **name bytes**, as text, into a scratch buffer
|
||||
(`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`) it owned — building the literal string `S"
|
||||
<name-text>" <pk0> <pk1> <pk2> <pk3> ELEVATE-GRANT` entirely in the *sending* VM's own memory.
|
||||
Only that finished string — not `waddr` itself — went to `KH-ELEVATE-SEND` and across to Hera.
|
||||
When Hera's interpreter runs `S" <name-text>"`, Hera's own `S"` allocates a fresh string **in
|
||||
Hera's own memory** and pushes Hera's own valid address. `waddr`/`wu` never cross the VM
|
||||
boundary as numbers at any point — only as copied character content. There is no cross-VM
|
||||
pointer dereference anywhere in this flow.
|
||||
|
||||
**The one real, much narrower gap:** the name text is spliced into `S" ... "` with no check for
|
||||
an embedded `"` character. If a future caller ever passed attacker-influenced text as the name
|
||||
(none ever did — the word had zero live callers), a `"` in the name would close the string
|
||||
literal early and let the rest of the name execute as raw FORTH source, with Hera's privilege.
|
||||
This is a caller-discipline / input-validation question, not an architectural defect: either
|
||||
always use a compile-time-fixed name literal at the call site (no runtime input, no risk at
|
||||
all), or validate for an embedded `"` before building the command if a name ever does need to
|
||||
come from something less trusted than the call site's own source code.
|
||||
|
||||
**Net effect on Phase 8 v1's scope:** Part A, as originally conceived in §3 above, is not
|
||||
needed. If a `SEND-ELEVATE-REQUEST` replacement is ever built, it can follow the original
|
||||
text-copy design as-is, with the one-line `"`-check added if and only if the name is ever
|
||||
runtime-supplied rather than a fixed literal. No kernel-Hermes/`repl.c` changes required. Part B
|
||||
(`capsules/zuse.4th`, gating `ZUSE-ELIGIBILITY-ADD`) stands on its own, independently verified,
|
||||
unaffected by this correction.
|
||||
|
||||
## 4. What Phase 8 actually needs to build
|
||||
|
||||
Corrected per §2's re-read above. Concretely, when Phase 8 next picks this up:
|
||||
|
||||
- If a caller into `ELEVATE-GRANT` is ever needed again, rebuild it close to the original
|
||||
`SEND-ELEVATE-REQUEST` shape (`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`/text-copy into `S" ...
|
||||
"`) — it was already safe. Add the one-line embedded-`"` check only if the name is ever
|
||||
runtime-supplied rather than a call-site literal. No `kernel_hermes.c`/`kernel_hermes.h`/
|
||||
`repl.c` changes needed for this.
|
||||
- `ELEVATE-GRANT` unchanged either way.
|
||||
- **Part B is done** (`capsules/zuse.4th`, committed and three-arch verified this session,
|
||||
2026-09-22) — `ZUSE-ELIGIBILITY-ADD` denied by default, granted only inside `ACL-ZUSE-BOOT`'s
|
||||
authenticated branch. This closes the actual "grant yourself eligibility with no real drive at
|
||||
all" path — a real, independently-confirmed gap, unaffected by this correction.
|
||||
- **Defending against a cloned drive — settled, 2026-09-23: not going to happen, by design.**
|
||||
Today, WIREBIND/MINT trust whatever identity is stored on an attached thumbdrive with no
|
||||
challenge at all (confirmed by grep: no `ed25519_sign`/`ed25519_verify` call anywhere in
|
||||
`capsule_wirebind.c` or `capsule_mint.c`), and the private key seed itself is stored in
|
||||
plaintext on the drive, read in the same devblock as the pubkey/cert. **This is accepted, not
|
||||
a gap.** Captain Bob, directly: *"nothing like a pin or a password or secret code or any
|
||||
bullshit... Everybody has secrets. There's only the drive."* Physical possession of the drive
|
||||
is the entire, deliberate credential model — a byte-for-byte clone being equivalent to the
|
||||
real drive is the accepted design, not a defect to close. A PIN/passphrase second factor was
|
||||
built, live-tested on all three architectures, and fully reverted before commit
|
||||
(`/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`, now marked rejected; memory
|
||||
`feedback_no_knowledge_factor_identity`) — **do not revisit a knowledge-factor approach here.**
|
||||
Any future work in this space needs a fundamentally different mechanism (not something typed
|
||||
and known) or stays an accepted limitation.
|
||||
- **Phase 8 v3, 2026-09-23 — done, a distinct and narrower concern from the item above.** The
|
||||
"accepted limitation" above is about a drive image copied *outside* StarshipOS entirely (e.g.
|
||||
imaged on an external computer) — that's still accepted, unchanged by this item. Captain Bob
|
||||
separately asked to close a narrower, different threat: **cloning a device's block content
|
||||
from *within* StarshipOS's own console**, using its own stock, unpinned words
|
||||
(`<src> BLOCK <dst> BUFFER 1024 MOVE`/`RELOCATE-BLOCK`). That's now closed — `MOVE`/`CMOVE`/
|
||||
`CMOVE>`/`RELOCATE-BLOCK` all refuse a same-VM, cross-device copy, verified live on all three
|
||||
architectures. See `/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`'s "Phase 8 v3"
|
||||
section for the full design and verification record. The identity record itself
|
||||
(seed/pubkey/cert) was already unreachable from FORTH before this — this closes the one real
|
||||
gap the research found: ordinary block content, not the identity record.
|
||||
|
||||
## 5. What this document is not
|
||||
|
||||
Not a reopening of `FABRIC-3.6.md`, not a change to anything currently built, not an
|
||||
authorization to write code. Per this series' own convention: design here, execution gets its
|
||||
own document when the work actually starts, the same relationship `FABRIC-3.5.md` had to
|
||||
`FABRIC-3.6.md`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 |
|
||||
@@ -0,0 +1,257 @@
|
||||
# StarForth v4.0.0 — Justification
|
||||
|
||||
This document records why StarForth v4.0.0 exists, what it changes, and why each major decision was
|
||||
made. The specification is `DECOMPOSITION.md`. Per project practice, this document is written before
|
||||
any v4 code.
|
||||
|
||||
---
|
||||
|
||||
## 1. The problem with v3
|
||||
|
||||
StarForth v3 is a successful software machine. It boots on amd64, aarch64, and riscv64, runs the
|
||||
Tripod fleet on LithosAnanke, and has held K≡1.0 across 38,400+ experimental runs. But it was designed
|
||||
for a large host, and it shows:
|
||||
|
||||
- **More than 300 C primitives.** Most are not primitive in any hardware sense. Double-cell arithmetic,
|
||||
string handling, comparisons, pictured output, and Q48.16 transcendentals are all expressible in a
|
||||
handful of machine operations.
|
||||
- **64-bit cells and 5 MB of linear memory per VM.** Reasonable on a PC; far too large for a node in a
|
||||
fabric.
|
||||
- **Diagnostics of its own implementation.** A significant block of words (hot-words cache statistics,
|
||||
lookup strategies, pipelining metrics, Bayesian cache models) measures v3's software dictionary, not
|
||||
the computation the dictionary performs.
|
||||
- **Hermes in software.** Message routing, per-message ACL checks, and TTL expiry are C code executed by
|
||||
a CPU that is also doing everything else.
|
||||
|
||||
None of this is wrong for a hosted or bare-metal OS. It is wrong for silicon. The project's direction
|
||||
is now an FPGA embodiment, and eventually an ASIC, and v3 cannot be carried there by porting.
|
||||
|
||||
## 2. What v4 is
|
||||
|
||||
StarForth v4 is a Forth machine designed to be the same thing in software and in hardware:
|
||||
|
||||
1. **A 32-instruction core ISA** derived from Chuck Moore's F18, the node of the GA144. Every core word
|
||||
is a mnemonic; every mnemonic is one 5-bit opcode.
|
||||
2. **Everything else is capsule code**, compiled from those 32 instructions, or a message to a node that
|
||||
owns a service, or a memory-mapped register, or retired.
|
||||
3. **A mesh of small nodes** that talk to their four neighbours through blocking ports. Hermes becomes
|
||||
the network itself: routing, ACL checks, and TTL expiry move into logic in every node's router.
|
||||
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.
|
||||
5. **A power-aware governor** built from a multi-level Rolling Window of Truth, controlling timing only.
|
||||
|
||||
v4 is a new implementation, not a refactor. v3 remains the reference system for LithosAnanke until v4
|
||||
reaches parity.
|
||||
|
||||
## 3. Why Moore's F18 instruction set
|
||||
|
||||
**It is proven minimal.** Moore spent decades removing instructions from his stack machines. The F18's
|
||||
32 opcodes are the result: enough to build a complete Forth, nothing that can be composed from the
|
||||
rest. There is no multiply, no divide, no compare, and no `SWAP`; each is a short sequence (for example
|
||||
`SWAP` is `over push push drop pop pop`).
|
||||
|
||||
**It fits a 32-bit word exactly.** 32 opcodes need 5 bits. Six slots fill 30 bits of a 32-bit
|
||||
instruction word, with 2 spare. One fetch feeds six instructions.
|
||||
|
||||
**It matches the project's formal-verification plan.** Proving 32 instruction semantics in Isabelle/HOL
|
||||
is a bounded task. Proving 300 C primitives is not. Every higher word then inherits correctness from its
|
||||
definition, which is itself a checkable object.
|
||||
|
||||
**It matches the dictionary-shrink plan that was already underway.** The existing POST suite, which
|
||||
exercises every dictionary word, was to be used as a regression gate while C primitives were replaced by
|
||||
colon definitions. v4 carries that plan to its end point: the surviving primitives are the ISA.
|
||||
|
||||
**It comes with a mesh precedent.** The GA144 places 144 F18 nodes on one die, each talking to its
|
||||
neighbours through blocking ports. v4 adopts that topology directly.
|
||||
|
||||
## 4. Why a mesh, and why Hermes goes into the fabric
|
||||
|
||||
v3's Tripod is several VMs sharing one CPU, with Hermes arbitrating messages between them in software.
|
||||
The mesh replaces time-sharing with space: each VM role runs on its own node or group of nodes,
|
||||
concurrently.
|
||||
|
||||
Moving Hermes into the fabric has three consequences:
|
||||
|
||||
- **The message semantics become hardware.** Per-message ACL checks and unconditional TTL expiry, which
|
||||
v3 already treats as rules rather than options, become router logic that cannot be bypassed.
|
||||
- **Contention disappears as a scheduling problem.** A blocking port is flow control. There is no
|
||||
scheduler to write, which honours the existing design goal of avoiding one.
|
||||
- **The SOS mechanism generalises.** Any node can emit an `SOS` packet. A node whose router fails can
|
||||
only stop forwarding, which its neighbours detect as blocked ports; this is the hardware form of v3's
|
||||
"Hermes raises a semaphore while sinking" rule.
|
||||
|
||||
## 5. Why cell width becomes a parameter
|
||||
|
||||
The Zynq-7000's processing system is a Cortex-A9, a 32-bit ARMv7-A core. Rather than maintain a separate
|
||||
32-bit fork, v4 makes cell width a build parameter of one VM (32 or 64). This has three benefits:
|
||||
|
||||
1. **The ARM becomes a real StarForth host**, not just a bootloader, at 32 bits.
|
||||
2. **Mesh nodes use 32-bit cells**, roughly halving stack and ALU cost in the fabric.
|
||||
3. **It opens a second invariance axis.** K≡1.0 has been shown invariant across amd64, aarch64, and
|
||||
riscv64. If it also holds across cell widths, the conservation law is shown not to depend on word
|
||||
size either. That is a stronger claim than ISA invariance alone.
|
||||
|
||||
The physics does not shrink with the cell. Heat and K arithmetic remain 64-bit (`int64_t` in C99 on
|
||||
every host; double cells on a 32-bit node). Changing only the payload width keeps the experiment clean:
|
||||
any difference in K can be attributed to cell width and not to lost precision.
|
||||
|
||||
## 6. Why the physics splits into "what" and "when"
|
||||
|
||||
The fabric can measure real power: the Zynq's XADC reads on-die temperature and supply voltages, and a
|
||||
current sensor on the core rail gives true power draw. This makes "heat" a physical quantity rather than
|
||||
a metaphor.
|
||||
|
||||
Physical measurements are noisy and never reproducible run to run. If they controlled which code
|
||||
executes, parity hashes would break and formal proofs of behaviour would become impossible. v4
|
||||
therefore splits the physics:
|
||||
|
||||
| Layer | Driven by | Controls | Property |
|
||||
| --- | --- | --- | --- |
|
||||
| **Virtual heat** | Instruction retirement and call counts | What executes (selection, promotion, eviction) | Deterministic and provable |
|
||||
| **Physical power** | XADC and rail current | When things happen (clock gating, node sleep, message pacing) | Adaptive, never affects results |
|
||||
|
||||
This settles a question left open in the original FPGA concept: whether compudynamic feedback into the
|
||||
control unit should affect only timing or also the execution path. The answer is both, through separate
|
||||
channels: logic chooses *what*, physics chooses *when*.
|
||||
|
||||
## 7. Why the governor is a multi-level Rolling Window of Truth
|
||||
|
||||
The timing governor uses the project's own Rolling Window of Truth mechanism at three timescales:
|
||||
|
||||
| Window | Timescale | Governs |
|
||||
| --- | --- | --- |
|
||||
| Short | microseconds | Clock gating on one node |
|
||||
| Medium | milliseconds | Node sleep and wake |
|
||||
| Long | seconds | Thermal trend and mesh-wide message pacing |
|
||||
|
||||
Positive feedback (rising message load) wakes neighbouring nodes and raises the clock. Negative feedback
|
||||
(rising temperature or power) throttles pacing and puts cool nodes to sleep. Each level reacts much more
|
||||
slowly than the one below it, so the loops do not fight; hysteresis at each level prevents flapping at
|
||||
thresholds. Hard limits (thermal ceiling, minimum clock) sit outside the adaptive layer as fixed logic.
|
||||
|
||||
A small neural network is a later candidate. Because the governor only controls timing, a poor governor
|
||||
costs power or speed and never correctness, so it is a safe place to experiment. The DoE recorder
|
||||
(below) produces exactly the training data such a network would need, so the two approaches can be
|
||||
compared on identical workloads.
|
||||
|
||||
## 8. Division of labour on the Zynq
|
||||
|
||||
| Component | Runs on | Role |
|
||||
| --- | --- | --- |
|
||||
| Mesh nodes | Fabric | All StarForth execution, the anti-clock, heat counters, routers |
|
||||
| Governor | Fabric | Multi-level RWT, single clock domain, cycle-exact |
|
||||
| Host node | ARM (32-bit) | Boot and bitstream load, compiler capsule, console bridge, DoE recorder |
|
||||
|
||||
The anti-clock stays in the fabric because it is defined as a pure function of the execution stream and
|
||||
must live where execution happens. The heartbeat's adaptive loop stays in the fabric because a loop
|
||||
crossing the PS–PL boundary would inherit ARM-side jitter (caches, interrupts, bus latency).
|
||||
|
||||
The ARM's recorder role keeps measurement separate from the thing being measured: the fabric pushes
|
||||
DoE rows into a FIFO, the ARM drains them to storage, and if the ARM falls behind rows are dropped
|
||||
rather than execution stalled. This is the fabric form of the planned `HB-ON`/`HB-OFF` disk recording.
|
||||
|
||||
## 9. Why the compiler lives on the host node
|
||||
|
||||
GA144 nodes have 64 words of RAM and 64 of ROM, and arrayForth compiles on a host. v4 follows the same
|
||||
split. The outer interpreter, dictionary, vocabularies, and defining words form the compiler capsule,
|
||||
which runs on the host node. Mesh nodes receive compiled code. Large capsules stay in DDR and are
|
||||
streamed to nodes as needed, so capsule size is not limited by node memory.
|
||||
|
||||
This is also why so many v3 words become CC rather than CAP in `DECOMPOSITION.md`: they are compiler
|
||||
machinery, not computation.
|
||||
|
||||
## 10. Development path
|
||||
|
||||
Each stage is checked against the one before it. Nothing proceeds on trust.
|
||||
|
||||
1. **Hosted golden model.** A C99 implementation of the v4 ISA and node model, with cell width, node
|
||||
count, and node memory as parameters. The POST suite, rewritten against v4 capsules, must pass at
|
||||
both 32 and 64 bits, and K≡1.0 must hold.
|
||||
2. **Hosted mesh.** Several golden-model nodes wired through simulated ports, running the Tripod roles
|
||||
as nodes. The 144-node configuration is exercised here, since the host is not limited by fabric size.
|
||||
3. **Co-simulation.** The node RTL is compiled with Verilator and run in lockstep with the golden model.
|
||||
After every instruction, stacks, registers, and heat counters are compared. The first mismatch
|
||||
identifies the faulty mnemonic exactly.
|
||||
4. **FPGA.** A 2×2 mesh on the PZ7020, then the largest grid that fits. The bitstream only has to match
|
||||
the co-simulation.
|
||||
5. **ASIC.** A single v4 node, not the mesh, as a proof of silicon through an open-source shuttle
|
||||
(currently Tiny Tapeout on IHP's SG13G2 130 nm open PDK). The same RTL is reused; block RAM is
|
||||
replaced by the process's SRAM macros.
|
||||
|
||||
## 11. Scaling beyond the PZ7020
|
||||
|
||||
Node count, node memory, and cell width are parameters, and the mesh is generated by a loop over rows
|
||||
and columns, so a larger board changes numbers, not design.
|
||||
|
||||
| Part | Approximate resources | Estimated nodes |
|
||||
| --- | --- | --- |
|
||||
| Zynq-7020 | ~53K LUTs, 140 BRAM36 | ~8–16 |
|
||||
| Zynq-7045 | ~218K LUTs, 545 BRAM36 | ~50–70 |
|
||||
| Zynq UltraScale+ (e.g. Kria K26) | ~117K LUTs, 144 BRAM36, 64 UltraRAM | ~30–40, with much larger node memory |
|
||||
| Larger UltraScale+ / Versal | Several hundred K LUTs and up | A full 144 |
|
||||
|
||||
Node counts are estimates. The first hardware measurement to take is the LUT cost of one node plus its
|
||||
router on the 7020; every other board's capacity follows from that number.
|
||||
|
||||
UltraScale+ parts also change the host: their Cortex-A53 cores are aarch64, so the host node can run
|
||||
64-bit StarForth while the mesh runs 32-bit cells, which the cell-width parameter already supports.
|
||||
Larger meshes will need registered router-to-router links to close timing, and the free edition of
|
||||
Vivado supports only smaller devices, so tool licensing must be checked before choosing a board.
|
||||
|
||||
## 12. Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| Capsule-level arithmetic is much slower than v3's C primitives on a hosted build. | Accepted. v4's measure of performance is the fabric, where each instruction is one cycle. The hosted build is a correctness oracle. |
|
||||
| Word addressing makes byte and string operations expensive. | D-1 in `DECOMPOSITION.md` keeps the choice open; colorForth's packed, pre-parsed source is a proven alternative for text. |
|
||||
| K≡1.0 may behave differently at 32-bit cell width. | That is an experimental result either way, and the hosted golden model finds it before any hardware exists. |
|
||||
| Hand-traced definitions contain errors. | Every CAP definition is a POST target against the v3 C primitive it replaces. |
|
||||
| The mesh does not fit the 7020 at a useful size. | Measure one node first; the design scales to larger parts unchanged. |
|
||||
|
||||
## 13. Relationship to intellectual property
|
||||
|
||||
v4 strengthens rather than replaces the existing claims. The Jacquard Selector, the Rolling Window of
|
||||
Truth, and the Steady State Machine all survive, now as hardware structures. The new elements a filing
|
||||
could draw on are: compudynamic heat as a zero-cost side effect of instruction retirement; the split of
|
||||
deterministic virtual heat (selection) from physical power (timing); per-packet ACL and TTL enforcement
|
||||
in a mesh router; and conservation invariance across cell width. Whether any of these belong in the
|
||||
LithosAnanke filing is a question for counsel.
|
||||
|
||||
## 14. Definition of done for v4.0.0
|
||||
|
||||
- The 32-instruction ISA is specified, with every open decision in `DECOMPOSITION.md` §3 settled.
|
||||
- The hosted golden model passes the rewritten POST suite at 32-bit and 64-bit cell widths.
|
||||
- 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.
|
||||
+1608
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Step 6c: Refusals and Waits — 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:** No message is lost without its sender being told, and no node waits for ever.
|
||||
|
||||
**Architecture:** Two message types, NACK and GONE, handled in the nucleus where messages are taken in, passed on and waited for (`v4/capsule/core.v4`, `quit.v4`). A node that drops a message records a NACK it owes and sends it when idle. `AWAIT` ends in an error on a NACK or GONE about the node it waits for, or on text from the console. Hera keeps a table of the nodes she has had born and gets `KILL`. The lone-node boot hands a waiting node the next console line.
|
||||
|
||||
**Tech Stack:** the v4 nucleus dialect, FORTH (`capsules/v4/hera.4th`), C99 (`v4/system/boot.c`, tests), `make -C v4`, `make -f kernel/Makefile`.
|
||||
|
||||
**Spec:** `docs/v4.0.0/MESH.md` section 7b; acceptance in step 6c of section 10.
|
||||
|
||||
## 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: a statement about what v3 or v4 does today is checked in the code, and where it can be, tried on the binary, before it is relied on.
|
||||
- No bad code is released: a defect found on the way is fixed, test first, and reported.
|
||||
- No stubs or stand-ins. Do not change v3.
|
||||
- Message types: 4 NACK, 5 GONE; each has one word of text, a node's number, and a length of 4.
|
||||
- Errors: 19 "Message refused", 20 "Node gone", 21 "Interrupted".
|
||||
- New cells, word addresses relative to `BUF0_W` in `v4/tests/host_map.h`: `(REFUSED)` at -7, `(OWED#)` at -8, `(OWED)` at -112 (8 pairs: to, about), `(AWAITING)` is the existing `(AWAIT-FROM)`, which is 0 when the node is not waiting.
|
||||
- The queue keeps 36 cells that only types 4 and 5 may use (four of them: seven words, the port, one word of text).
|
||||
- `capsules/v4/hera.4th`: every block at most 16 lines of at most 64 characters; `v4/build/mkcapsule --lint capsules/` clean.
|
||||
- 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 NACK that meets a full node.** Expected: dropped, counted in `(LOST)`, no NACK owed for it, nothing loops. Test in Task 1.
|
||||
2. **Two nodes each owing the other a NACK while both have full queues.** Expected: neither waits for ever; section 7a's rule still holds. Test in Task 1.
|
||||
3. **A GONE that does not come from the centre.** Expected: ignored; the way to that node is kept. Test in Task 1.
|
||||
4. **Console text for another node passing through a node that is waiting.** Expected: it does not interrupt the node it passes through; only text for that node does. Test in Task 2.
|
||||
5. **`KILL` of a node Hera never had born, of herself, or twice.** Expected: an error with a message, nothing removed. Test in Task 3.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: NACK and GONE in the nucleus
|
||||
|
||||
**Files:** Modify `v4/capsule/core.v4` (`(TAKE-KEEP)`, the queue's room, the errors list), `v4/capsule/quit.v4` (`(IDLE)`, `(PASS-ON)`, `AWAIT`, the messages for 19 to 21, `NO-ROUTE`, headers for `(REFUSED)`), `v4/tests/host_map.h`, `v4/tools/mkimage.c`, `v4/include/v4/message.h` (the two types), `v4/tests/test_host_mesh.c`
|
||||
|
||||
**Interfaces — Produces:** `V4_MSG_NACK 4`, `V4_MSG_GONE 5`; nucleus words `(OWE) ( to about -- )`, `(PAY) ( -- )` sends one owed NACK if there is one, `NO-ROUTE ( node -- )` forgets the way to a node; variable `(REFUSED)`; `AWAIT ( node -- how )` as before when the answer comes, and otherwise ends in error 19, 20 or 21.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests** in `test_host_mesh.c` (three nodes in a line, 10 with the console, 11, 12):
|
||||
- *No way:* node 10 sends text to node 77, which nobody has a way to and for which 10's way is "toward 11": 11's `(LOST)` rises by one, and 10's `(REFUSED)` rises by one.
|
||||
- *No room:* node 12 is made busy (a word that loops a counted number of times without reading) while 10 sends it messages until its queue is full; the next is refused: 12's `(LOST)` rises and 10's `(REFUSED)` rises by one, and when 12 is free every message it did take is done in order.
|
||||
- *A waiting sender:* 10 does `: W S" 1 DROP" 77 SEND 77 AWAIT ; W` and its line ends "Message refused" with an error; 10 then answers the next line.
|
||||
- *GONE:* a GONE about 12 arriving at 11 from the port toward 10, 11's centre here, makes 11 forget the way to 12 (`12 (PORT-FOR)` gives the default); one arriving from 12's side is ignored (Review Focus 3). A node waiting on 12 ends "Node gone".
|
||||
- *A NACK meets a full node* (Review Focus 1), and *two full nodes owing each other* (Review Focus 2): run to quiet; `(LOST)` accounts for every message not delivered; every node is back waiting at its ports.
|
||||
- *More refusals than room* (acceptance 7): nine messages refused at one node in a burst: eight NACKs are owed and sent, the ninth is counted in `(LOST)`.
|
||||
- [ ] **Step 2: Run** `make -C v4 run-64-test_host_mesh.c`: the new checks fail (nothing sends a NACK).
|
||||
- [ ] **Step 3: Implement.**
|
||||
- `(TAKE-KEEP)`: an ordinary message is kept only if it leaves the 36 reserved cells free; types 4 and 5 may use them. On no room: an ordinary message calls `(OWE)` with its `from` and `to`; types 4 and 5 are only counted.
|
||||
- `(OWE)`: append the pair at `(OWED)` if `(OWED#)` is below 8; otherwise add one to `(LOST)`.
|
||||
- `(IDLE)`: before it reads its ports, `(PAY)`: if anything is owed, send the oldest as a message of type 4, length 4, one word, by `(PORT-FOR)` of its `to` (the default way if none; no way at all: count it and go on).
|
||||
- `(PASS-ON)` with no way: `(OWE)` for an ordinary message, count for types 4 and 5.
|
||||
- `(IDLE)` for this node, type 4: add one to `(REFUSED)`. Type 5: if it came by the port of `(ROUTE-DEFAULT)`, `NO-ROUTE` for the node named. Both are then let go.
|
||||
- `AWAIT`: sets `(AWAIT-FROM)`; in its loop, a message for this node of type 4 or 5 whose word of text is the awaited node clears `(AWAIT-FROM)` and raises 19 or 20 (a GONE only by the centre's port; it also does `NO-ROUTE`). On the answer, or any ending, `(AWAIT-FROM)` is 0.
|
||||
- Errors 19, 20, 21 in `(RAISED)` and in `core.v4`'s list.
|
||||
- [ ] **Step 4: Run** the mesh test at 64 and 32 bits and under the sanitizers; then `make -C v4 -k test`. 0 failures; `test_host_unit.c` still 538 of 538.
|
||||
- [ ] **Step 5: Commit** `feat(v4.0.0): a refused message is told to its sender -- NACK and GONE` and push.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: A line from the console breaks a wait
|
||||
|
||||
**Files:** `v4/capsule/quit.v4` (`AWAIT`), `v4/system/boot.c`, `v4/Makefile` (`hosted-check`), `v4/tests/test_host_mesh.c`
|
||||
|
||||
- [ ] **Step 1: Failing tests.** In the mesh test: node 12 loops for ever; node 10 does `12 AWAIT`; a line typed at the console, `65 EMIT`, ends 10's wait with "Interrupted" and an error, and then prints `A`. Text from the console for node 11, passing through 10 while 10 waits, does not end 10's wait and is done by 11 once 10 is free (Review Focus 4). In `hosted-check`: `printf '5 AWAIT\n1 2 + .\n'` prints `Interrupted`, then `3`.
|
||||
- [ ] **Step 2: Run** and see them fail (the hosted program ends on `5 AWAIT`).
|
||||
- [ ] **Step 3: Implement.** In `AWAIT`'s loop a message of type 1 for this node whose `from` is the node's `(CONSOLE)` is kept with the messages waiting, as any other is, and then the wait ends in error 21. In `boot.c`, a node reading "any port" in the middle of a line, with nothing to give it, returns a new result, `V4_BOOT_LINE_WAITING`; `v4_boot_line` called again with the next line sends it, which interrupts. `v4/tools/hosted.c` and `kernel/src/v4/sk_v4.c` treat `WAITING` as "read the next line", printing nothing.
|
||||
- [ ] **Step 4: Run** the mesh test, `make -C v4 -k test`, `sanitize`, `hosted-check`.
|
||||
- [ ] **Step 5: Commit** `feat(v4.0.0): a line from the console breaks a wait` and push.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Hera's table and KILL
|
||||
|
||||
**Files:** `capsules/v4/hera.4th`, `v4/tests/test_host_unit.c`
|
||||
|
||||
- [ ] **Step 1: Failing tests** in the unit of five:
|
||||
- *Killed* (acceptance 3): node 11 does `: W 14 AWAIT ; W` (it is not wired to 14); Hera does `14 KILL`; 11's line ends "Node gone"; afterwards `14 (PORT-FOR)` on 11, 12 and 13 gives each its default way, and on Hera gives 0; a `SEND` to 14 from 12 comes back refused.
|
||||
- *Stuck* (acceptance 4): node 13 loops for ever; node 12 waits on it; Hera does `13 KILL`; 12's wait ends.
|
||||
- *Hera's own wait* (acceptance 5): node 12 loops for ever; the console types `12 AWAIT` to Hera and then `12 KILL`: the first ends "Interrupted", the second is run, and 12 is gone.
|
||||
- Review Focus 5: `99 KILL`, `10 KILL`, and `14 KILL` a second time each print a message and end in an error; `born_count` and the nodes there are unchanged.
|
||||
- [ ] **Step 2: Run** and see them fail (`KILL` is not a word).
|
||||
- [ ] **Step 3: Implement** in `hera.4th`, in a new block: a table of 16 pairs filled by `BIRTH`; `(PLACE) ( n -- place | -1 )`; `KILL ( n -- )`: not in the table, or this node's own number: `." KILL: no such node"` and error -1; otherwise `NODE-KILL`, the pair removed, `NO-ROUTE`, and for every other pair a message of type 5 about *n* by `SEND`'s way. The words for sending a message of a given type with one word of text are the nucleus's (Task 1).
|
||||
- [ ] **Step 4: Run** the unit test, `make -C v4 -k test`, `sanitize`, the capsule lint.
|
||||
- [ ] **Step 5: Commit** `feat(v4.0.0): Hera kills a node by number and tells the others it is gone` and push.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Bare metal, and the write-up
|
||||
|
||||
- [ ] **Step 1:** Three v4 boots with the typed session of the last step and, added to it, `5 AWAIT`, `1 2 + .`. Each log: POST 538 of 538; `Interrupted`; `3`; the `PARITY:V4_SYSTEM` line equal to hosted's.
|
||||
- [ ] **Step 2:** `MESH.md` step 6c as done, with what is not as intended yet; the note under section 4.1 about `AWAIT`; `v4/README.md`.
|
||||
- [ ] **Step 3: Commit** with the three logs and push.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Step 6d: The Wait — 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:** A node that is stuck holds up no other node.
|
||||
|
||||
**Architecture:** The engine gains a wait: a node offers the first word of a message on one or more ports and sleeps until an offer is taken or a word comes for it; the fabric does the handing over in one step. The nucleus begins every message through it (`(GATE)`), pays what it owes through it (`(PAY)`), and so never writes to a node that is not ready and never goes round looking. The lower-number rule, the looking-again, and the engine's console release are removed.
|
||||
|
||||
**Tech Stack:** C99 (`v4/src`, `v4/system/boot.c`, tests), the v4 nucleus dialect (`v4/capsule/core.v4`, `quit.v4`), `make -C v4`, `make -f kernel/Makefile`.
|
||||
|
||||
**Spec:** `docs/v4.0.0/MESH.md` section 7c; acceptance in 7c.5.
|
||||
|
||||
## 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>`.
|
||||
- No bad code is released: a defect found on the way is fixed, test first, and reported. No stubs or stand-ins. Do not change v3.
|
||||
- The F18 is borrowed ideas, not a rule: a difference from it is noted in a line, not held for ruling. A difference from v3 is still shown first.
|
||||
- Port addresses, word address `base` = port 0: `base + V4_PORTS` any; `+ 1` which port; `+ 2` writers; `+ 3` readers; `base + V4_PORTS + 4 + k` offer on port k; `base + 2 * V4_PORTS + 4` the wait.
|
||||
- "Which port" after the wait: `k` (0 .. `V4_PORTS - 1`) a word came from port k; `V4_PORTS + k` the offer on port k was taken, and the fetch gave 0.
|
||||
- Errors: 18 "No one on that port", 21 "Interrupted". Both exist.
|
||||
- Every test is watched failing before the code that passes it is written. A test that passes at once is shown to fail with the code taken out, and that is said in the ledger.
|
||||
- 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 and committed. Never delete a log.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Two NACKs owed, the first to a stuck node, the second to a node that is waiting for it.** Expected: the second is paid; the node that waits gets "Message refused". Test in Task 2.
|
||||
2. **An offer that could be taken in the same step a word comes for the node.** Expected: the word comes first, the offer is withdrawn, and nothing is handed over twice. Test in Task 1.
|
||||
3. **Three nodes in a ring, each offering to the next.** Expected: every message arrives; all three come to rest. Test in Task 1.
|
||||
4. **Console text for another node reaching a node whose line is waiting to begin a message.** Expected: it is kept and does not interrupt; only text for that node does. Test in Task 2.
|
||||
5. **An offer to a neighbour that is asleep, then woken.** Expected: the offer stands while it sleeps and is taken when it wakes and reads. Test in Task 1.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: The wait in the engine
|
||||
|
||||
**Files:** Modify `v4/include/v4/node.h`, `v4/src/node.c`, `v4/src/fabric.c`, `v4/include/v4/fabric.h`; Test `v4/tests/test_fabric.c`.
|
||||
|
||||
**Interfaces — Produces:**
|
||||
- `#define V4_PORT_OFFER (V4_PORTS + 4u)` (index of the offer on port 0), `#define V4_PORT_WAIT (2u * V4_PORTS + 4u)`.
|
||||
- Node fields: `unsigned offers;` (bit k: a word is offered on port k), `v4_cell offer[V4_PORTS];`, `int waiting;` (blocked in the wait).
|
||||
- `int v4_node_in_wait(const v4_node *n);` 1 if blocked in the wait with nothing given.
|
||||
- `int v4_node_offer_taken(v4_node *n, unsigned k);` the offer on port k is taken: the node is given 0 from `V4_PORTS + k`, every offer withdrawn. Returns 0 if it had none there.
|
||||
- `int v4_node_offer_as_write(v4_node *n);` for a host with one node whose devices always take: if the node is in the wait with an offer, it becomes a blocked write of that word on the lowest offered port (`asking`, `request`, `ask_port`), and `v4_node_port_served` then completes the wait as `v4_node_offer_taken` would. Returns 1 if it did.
|
||||
- Nothing is removed in this task.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests** at the end of `test_fabric.c`, each a small assembled programme as the existing ones are (`loaded`, `LIT`, `O`, `ND`):
|
||||
- *Taken by a reader:* a stores 7 to its offer on port 0 and fetches the wait, storing the fetched value at `OUT` and "which port" at `OUT + 1`; b reads its port 3. After settling: b has 7; a has 0 and `V4_PORTS + 0`.
|
||||
- *Taken by a device:* the same with a device that takes on port 0: the device got 7.
|
||||
- *A stuck neighbour:* b counts for ever. After 200 steps a is `v4_node_in_wait`, has executed nothing since (its `es` clock is unchanged over the last 100 steps), and b's readers word shows a as waiting to read.
|
||||
- *Withdrawn by an incoming word (Review Focus 2):* a offers to stuck b on port 0; c, wired to a's port 2, writes 9. a gets 9 and "which port" 2; its `offers` is 0; b was given nothing.
|
||||
- *Offer and reader in the same step:* a offers to b; b reads; c writes to a, all arranged to block in the same step: a gets c's word, b is given nothing that step, c is served, and after a offers again b gets the word exactly once.
|
||||
- *Two offers facing:* a and b each offer to the other. The one added first has `V4_PORTS + k`; the other has the word and the port it came by; neither has an offer left.
|
||||
- *A ring (Review Focus 3):* a offers to b, b to c, c to a; each on waking with a word reads no more, and on its offer being taken stops. All three come to rest; each word offered was either delivered once or is still offered by a node in the wait, and no word was delivered twice.
|
||||
- *Asleep (Review Focus 5):* b asleep and reading: a's offer stands for 100 steps; `v4_fabric_wake`, and it is taken.
|
||||
- *An empty port:* a offers on a port with nothing wired. With no error attached it stays in the wait. With `v4_fabric_gone_error(&f, 18)`, `v4_node_error_attach` and `v4_node_fault_attach`: one step, error 18 is raised, `offers` is 0.
|
||||
- *The lone-node helper:* a node alone, in the wait with offers on ports 1 and 3: `v4_node_offer_as_write` gives `asking`, `ask_port == 1`, `request` the word offered there; `v4_node_port_served` then leaves it given 0 from `V4_PORTS + 1` and not waiting.
|
||||
- [ ] **Step 2: Run** `make -C v4 run-64-test_fabric.c`. Expected: it does not compile, naming `v4_node_in_wait` and `V4_PORT_OFFER` as undeclared. Then add the two `#define`s, the fields and the three declarations only, with bodies that return 0, and run again. Expected: it compiles and every new check fails (nothing is ever handed over). Those bodies are replaced in Step 3; none is left.
|
||||
- [ ] **Step 3: Implement.**
|
||||
|
||||
`node.h`, in `v4_node` after `since_look`:
|
||||
```c
|
||||
unsigned offers; /* bit k: a word is offered on port k (the wait: MESH.md 7c) */
|
||||
v4_cell offer[V4_PORTS];
|
||||
int waiting; /* non-zero: blocked in the wait */
|
||||
int offer_asked; /* non-zero: `asking` stands for the offer on `ask_port` (v4_node_offer_as_write) */
|
||||
```
|
||||
`node.c`:
|
||||
```c
|
||||
int v4_node_port_index(const v4_node *n, v4_cell addr)
|
||||
{
|
||||
if (n->port < 0 || addr < n->port || addr > n->port + (v4_cell)V4_PORT_WAIT) return -1;
|
||||
return (int)(addr - n->port);
|
||||
}
|
||||
```
|
||||
In `v4_node_store`, before the "any and which port are not written to" fault:
|
||||
```c
|
||||
if (port >= (int)V4_PORT_OFFER && port < (int)V4_PORT_WAIT) {
|
||||
unsigned k = (unsigned)port - V4_PORT_OFFER;
|
||||
n->offer[k] = value;
|
||||
n->offers |= 1u << k;
|
||||
return;
|
||||
}
|
||||
```
|
||||
In `v4_node_fetch`, with the other port indexes (the wait is fetched as "any" is):
|
||||
```c
|
||||
if (port == (int)V4_PORT_WAIT) {
|
||||
if (!n->given) return 0;
|
||||
n->given = 0;
|
||||
n->last_from = n->given_port;
|
||||
return n->given_value;
|
||||
}
|
||||
if (port >= (int)V4_PORT_OFFER) return 0; /* an offer is not read back */
|
||||
```
|
||||
`v4_node_read_ready`:
|
||||
```c
|
||||
int v4_node_read_ready(v4_node *n, v4_cell addr)
|
||||
{
|
||||
int port = v4_node_port_index(n, addr);
|
||||
if (port < 0 || (port > (int)V4_PORT_ANY && port != (int)V4_PORT_WAIT)) return 1;
|
||||
if (n->given && (port >= (int)V4_PORT_ANY || (unsigned)port == n->given_port)) return 1;
|
||||
n->reading = 1;
|
||||
n->read_port = port == (int)V4_PORT_WAIT ? V4_PORT_ANY : (unsigned)port;
|
||||
n->waiting = port == (int)V4_PORT_WAIT;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
`v4_node_port_give` also sets `n->waiting = 0; n->offers = 0;` only when `n->waiting` was set (a plain read leaves offers alone). New:
|
||||
```c
|
||||
int v4_node_in_wait(const v4_node *n) { return n->waiting && n->reading && !n->given; }
|
||||
|
||||
int v4_node_offer_taken(v4_node *n, unsigned k)
|
||||
{
|
||||
if (!v4_node_in_wait(n) || k >= V4_PORTS || !(n->offers & (1u << k))) return 0;
|
||||
v4_node_port_give(n, V4_PORTS + k, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int v4_node_offer_as_write(v4_node *n)
|
||||
{
|
||||
unsigned k;
|
||||
if (!v4_node_in_wait(n) || n->offers == 0) return 0;
|
||||
for (k = 0; !(n->offers & (1u << k)); k++) { }
|
||||
n->request = n->offer[k]; n->ask_port = k; n->asking = 1; n->offer_asked = 1;
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
`v4_node_port_served`: if `n->offer_asked`, clear it and `asking`, then `v4_node_port_give(n, V4_PORTS + n->ask_port, 0)`; otherwise as now. `v4_node_port_gone`: a node in the wait with `offers` set has `reading`, `waiting`, `offers` cleared and the error raised. `v4_node_port_attach` and `v4_node_reset` zero the new fields.
|
||||
|
||||
`fabric.c`, in `v4_fabric_step` after the two hand-over loops and before the error loops (so a word for the node comes first):
|
||||
```c
|
||||
/* the wait (MESH.md 7c): an offer is taken by whoever is ready for it */
|
||||
for (i = 0; i < f->capacity; i++) {
|
||||
v4_node *n;
|
||||
unsigned k;
|
||||
if (!awake(f, i)) continue;
|
||||
n = &f->place[i].node->n;
|
||||
if (!v4_node_in_wait(n)) continue;
|
||||
for (k = 0; k < V4_PORTS; k++) {
|
||||
const v4_wire *w = &f->place[i].wire[k];
|
||||
if (!(n->offers & (1u << k))) continue;
|
||||
if (w->kind == V4_WIRE_DEVICE) {
|
||||
if (w->device->take && w->device->take(w->device->self, n->offer[k])) { (void)v4_node_offer_taken(n, k); done++; break; }
|
||||
} else if (w->kind == V4_WIRE_NODE && awake(f, w->node)) {
|
||||
v4_node *r = &f->place[w->node].node->n;
|
||||
if (r->reading && !r->given && (r->read_port == w->port || r->read_port == V4_PORT_ANY)) {
|
||||
v4_cell word = n->offer[k];
|
||||
(void)v4_node_offer_taken(n, k);
|
||||
v4_node_port_give(r, w->port, word);
|
||||
done++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
(`r` in the wait is reading "any", so it takes the word and its own offers are withdrawn: the lower place writes.) In the gone-error loop add: a node in the wait with an offer on a port wired to nothing.
|
||||
- [ ] **Step 4: Run** `make -C v4 run-64-test_fabric.c run-32-test_fabric.c`, then `make -C v4 -k test` and `make -C v4 -k sanitize`. Expected: 0 failures everywhere; nothing else uses the new addresses yet.
|
||||
- [ ] **Step 5: Mutation check.** Move the offer loop above the hand-over loops: the "withdrawn by an incoming word" or "same step" check fails. Put it back.
|
||||
- [ ] **Step 6: Commit** `feat(v4.0.0): the wait in the engine -- a node offers a word and sleeps` and push.
|
||||
|
||||
### Task 2: The nucleus begins every message through the wait
|
||||
|
||||
**Files:** Modify `v4/capsule/core.v4` (`(GATE)`, `(PAY)`, `(PAY1)`, `(FLUSH-OUT)`, the 7a comment block), `v4/capsule/quit.v4` (`(IDLE)`, `(PASS-ON)`, `(FINISH)`, `(SEND1)`, `GONE`, `(RAISED)`), `v4/tests/host_map.h` (`(GATE-WORD)`, the constants for the offer and wait addresses), `v4/tools/mkimage.c` if it lists constants, `v4/system/boot.c`, `v4/tests/test_host_quit.c`; Test `v4/tests/test_host_mesh.c`.
|
||||
|
||||
**Interfaces — Consumes:** Task 1's addresses and `v4_node_offer_as_write`. **Produces:**
|
||||
- `(OFFERS) ( -- flag )` reads the wait once. Zero: a word came instead, and that whole message has been taken in and kept (`(TAKE)`), its seven words still in `(MQ-HDR)`. Non-zero: an offer was taken; B is at the port it was taken on, and `(GATE-PORT)` holds that port's address.
|
||||
- `(GATE) ( w port -- )` the word is offered on the port, by its address, until it is taken; B is left at the port. Its callers no longer write the first word themselves.
|
||||
- `(PAY) ( -- flag )` offers every NACK owed, one to a port, reads the wait once, and if one was taken writes the rest of it and marks it paid. Zero if nothing is owed.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests** in `test_host_mesh.c`. Replace the last section ("THE LIMIT") and the two before it that are about a stuck node with these; the fabric keeps `v4_fabric_gone_error(&f, V4_ERROR_NO_ONE)` from the start of the section.
|
||||
- *Higher creditor stuck (acceptance 4):* node 12 sends 55 a message by node 10, which has no way, and is then stuck in `BEGIN 0 UNTIL`. Node 10 owes it a NACK. `tell(10, "1 2 + .")` gives "3 "; `tell(11, "20 22 + .")` gives "42 ": node 10 goes on, and so does what passes through it. Node 10 is not `asking`.
|
||||
- *Lower creditor stuck:* node 11 sends 55 by node 12, which has no way, and is stuck. Node 12 owes it. `tell(12, "7 8 * .")` gives "56 ", and node 12 between lines is `v4_node_in_wait` (it sleeps: its `es` clock does not move over 1000 steps).
|
||||
- *A third node writes to the one that owes:* with node 11 stuck and node 12 owing it, node 10 sends node 12 text and gets its answer.
|
||||
- *Two owed (Review Focus 1):* node 10 owes one to stuck node 12 and then one to node 11, which sent 66 a message and is in `66 AWAIT`. Node 11's line ends "Message refused".
|
||||
- *A message for the stuck node:* node 10 does `: TO12 S" 1 DROP" 12 SEND 65 EMIT ; TO12` with 12 stuck: still running, node 10 `v4_node_in_wait`. Text from the console for node 11 then passes through and is answered, and node 10's line is still waiting (Review Focus 4). Text from the console for node 10 ends that line "Interrupted", "A" is never printed, and the new line is done.
|
||||
- *Removed:* node 12, stuck, is removed while node 10 offers to it: node 10's line ends "No one on that port" (acceptance 6), and it owes nothing.
|
||||
- The existing flood ("two hundred from each to each") stays as it is: it must still come to rest (acceptance 7). Its counts of arrived and lost may change; the check that `arrived + lost` accounts for every message sent stays exact.
|
||||
- [ ] **Step 2: Run** `make -C v4 run-64-test_host_mesh.c`. Expected: "higher creditor stuck" fails with node 10 `asking`; "lower creditor stuck" fails on the clock moving; the Hera-line check fails with node 10 `asking` and no "Interrupted" (the mesh fabric has no interrupt error set).
|
||||
- [ ] **Step 3: Implement the nucleus.**
|
||||
- Constants `(OFFER)` = `(PORT) + PORTS + 4` and `(WAIT)` = `(PORT) + 2 * PORTS + 4`, where the port constants are defined.
|
||||
- `(OFFERS)`: `(WAIT) b! @b` gives the word; `(PORT)+9 b! @b` gives which. If which is below `PORTS`: the pair is `( to port )` as in `(IDLE)`'s read of "any": B to `(PORT) + which`, `(TAKE)`, leave 0. Otherwise store `which - PORTS` in `(GATE-PORT)` as an address `(PORT) + k`, put B there, drop the 0 that was fetched, leave -1.
|
||||
- `(GATE)`: store the port in `(GATE-PORT)` and the word in `(GATE-WORD)`, because it runs with the stack as full as `EMIT` may be. Loop: store `(GATE-WORD)` at `(OFFER) + port number`; `(OFFERS)`; if taken, return. If not: when `(QUIET)` is 0 and the message just kept is text (type 1) for this node from `(A-CONSOLE)`, store 21 in `NODE-ERROR`; otherwise loop. With nothing on the port the engine raises 18 itself.
|
||||
- The five callers drop their own first write: `(FINISH)` `(MSG)+1 a! @ (DONE-PORT) a! @ (GATE)`; `(PASS-ON)` `(MSG) a! @ port (GATE)` then words 1 to 6 with `(MSG)+1 a! 5 FOR @+ !b UNEXT`; `(SEND1)`, `GONE`, `(FLUSH-OUT)` likewise.
|
||||
- `(PAY)`: for k from 0 to `(OWED#) - 1`: `(PORT-FOR)` of its `to`; no way, or `(THERE)` false: count in `(LOST)` and `(PAID)` (as now). Otherwise, if no offer has been stored on that port in this pass (a bit kept on the stack or in a cell), store `to` at that port's offer address. Then if nothing was offered leave 0. Otherwise `(OFFERS)`: taken on port k: find the first debt whose port is k, write `4 (HDR) 4 !b about !b`, `(PAID)`; leave -1 either way.
|
||||
- `(IDLE)`: `L:` set `(QUIET)`; if `(MQ#)` is not 0, go to `HAVE` as now; if `(OWED#)` is not 0, `(PAY) drop jump L`; else the blocking read of "any" as now. The branch that went round taking writers is deleted.
|
||||
- Delete from `(GATE)`'s old body and comments: the writers loop, the `(NEAR)` comparison, `NOTYET`/`NOONE`. Delete `(PAY1)`'s `BUSY`/`BLIND`/`LEAVE`. `(NEAR)` and `NEIGHBOUR` stay for `(CENTRE)`.
|
||||
- `(RAISED)`'s quiet branch is unchanged: an error 18 raised while paying counts and returns to `(IDLE)`, where `(PAY)` then drops the debt by `(THERE)`.
|
||||
- [ ] **Step 4: The lone node.** In `v4/system/boot.c`'s line loop and nucleus-loading loop, and in `test_host_quit.c`'s `run_line` and `boot_with`, call `(void)v4_node_offer_as_write(n);` at the top of each pass, before `asking` is looked at. Nothing else changes there: a write to a port with no one on it is already error 18.
|
||||
- [ ] **Step 5: Run** `make -C v4 run-64-test_host_mesh.c run-32-test_host_mesh.c`; then `make -C v4 -k test`, `make -C v4 -k sanitize`, `make -C v4 hosted-check`. Expected: 0 failures; `hosted-check` shows POST 538 of 538, the three ISAs identical, and the typed `5 7 PORT!` and `5 AWAIT` lines as before. `test_host_unit.c`'s "Hera blocked writing" check still passes (it now passes by the nucleus's "Interrupted", not the engine's).
|
||||
- [ ] **Step 6: Mutation checks.** (a) In `(GATE)` make the console test always false: the "ends that line Interrupted" check fails. (b) In `(PAY)` offer only the first debt: "two owed" fails. Put both back.
|
||||
- [ ] **Step 7: Commit** `feat(v4.0.0): every message begins through the wait -- a stuck node holds up no one` and push.
|
||||
|
||||
### Task 3: What the wait replaced comes out; the documents; the boots
|
||||
|
||||
**Files:** Modify `v4/include/v4/fabric.h`, `v4/src/fabric.c` (`interrupt_error`, `v4_fabric_interrupt_error`, `pending` in `v4_device`), `v4/include/v4/node.h`, `v4/src/node.c` (`since_look`, `v4_node_words_since_look`), `v4/include/v4/image.h` (`V4_ERROR_INTERRUPTED` stays only if still used), `v4/tests/test_fabric.c`, `v4/tests/test_host_mesh.c`, `v4/tests/test_host_unit.c` (device initialisers, the `v4_fabric_interrupt_error` call), `docs/v4.0.0/MESH.md`, `v4/README.md`.
|
||||
|
||||
**Interfaces — Consumes:** Tasks 1 and 2. **Produces:** nothing new.
|
||||
|
||||
- [ ] **Step 1: Write the failing test** in `test_host_unit.c`, in place of "Hera blocked writing to a node that is stuck": node 11 stuck; Hera's line `: T11 S" 1 DROP" 11 SEND 67 EMIT ; T11` is still running and she is `v4_node_in_wait`, not `asking` (acceptance 5); `tell(10, "11 KILL 68 EMIT")` gives "Interrupted" before "D", never "C"; node 11 is gone; `1 2 + .` gives "3 ". And one more: with node 12 stuck and owed a NACK by Hera, `tell(13, "13 100 * .")` gives "1300 ": a stuck node of the unit holds up no other. Remove the `v4_fabric_interrupt_error` call from the test's set-up.
|
||||
- [ ] **Step 2: Run** `make -C v4 run-64-test_host_unit.c`. Expected: the `v4_node_in_wait` check passes already (Task 2); so take Task 2's console test out of `(GATE)` to see "Interrupted" fail, and put it back. Say so in the ledger.
|
||||
- [ ] **Step 3: Remove** `interrupt_error` and its loop, `v4_fabric_interrupt_error`, `pending` from `v4_device` and every initialiser, `since_look` and `v4_node_words_since_look`, and the section of `test_fabric.c` that tested them. `grep -rn 'interrupt_error\|\.pending\|console_pending\|since_look' v4 kernel/src/v4` is empty.
|
||||
- [ ] **Step 4: Run** `make -C v4 -k test`, `sanitize`, `hosted-check`, and `mkcapsule --lint capsules/` with the freshly built tool. 0 failures, lint clean.
|
||||
- [ ] **Step 5: Documents.** `MESH.md`: 7b's rule and 7b.7 lose the "stuck node can hold up its neighbours" limit and point to 7c; 7b.6's "A blocked write" paragraph says it was replaced by 7c; 4.1's table gains the offer and wait addresses; step 6d in section 10 is marked done with the tests, the logs and the hash. `v4/README.md`: the last sentence of "Refusals and waits" becomes what now holds.
|
||||
- [ ] **Step 6: The three boots**, in order amd64, aarch64, riscv64, each `clean qemu` with `STARFORTH_V4=1` and the typed session used for step 6c. Expected on each: `PARITY:V4_POST tests=538 pass=538 fail=0`, `PARITY:OK`, `POST: PASSED`, the same `word_count` and `dict_hash` as `hosted-check`, and the session's lines as in `logs/20261007-220949`.
|
||||
- [ ] **Step 7: Commit** `feat(v4.0.0): the wait replaces looking and the console release; step 6d written up`, with the logs, and push.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Step 6e: Messages on the Wire — 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:** A node that is stuck holds up no other node, by having the fabric move whole messages between neighbours and hold them on the wires.
|
||||
|
||||
**Architecture:** The fabric keeps a queue of whole messages each way on every wire. A node asks the fabric, through a few addresses after its ports, to look at, take, move, drop or put a whole message; each is one engine step, all or nothing. A node holds nothing in transit: the nucleus's ring of messages waiting, the words that turned it, the list of NACKs owed and `(GATE)` are removed. Only two things block: sleep, and sleep for room on a wire, used by printing alone.
|
||||
|
||||
**Tech Stack:** C99 (`v4/src`, `v4/include/v4`, `v4/system/boot.c`, `v4/tools/hosted.c`, tests), the v4 nucleus dialect (`v4/capsule/core.v4`, `quit.v4`), FORTH (`capsules/v4/hera.4th`), Python 3 (`v4/tools/depthsweep.py`), `make -C v4`, `make -f kernel/Makefile`.
|
||||
|
||||
**Spec:** `docs/v4.0.0/MESH.md` section 7d (7d.3 the interface, 7d.4 the nucleus, 7d.6 the limits, 7d.7 the acceptance). Sections 7c.6 and 7c.7 say what the first attempt got wrong; read them before Task 1.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Branch `StarForth-v4.0.0`, main checkout. No new branch, no stash, no worktree. End commit messages with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
- **Every commit is chained on its checks with `&&`**: `make -C v4 hosted-check` must pass in the same command that commits. A commit was once pushed with it failing.
|
||||
- No bad code is released: a defect found on the way is fixed, test first, and reported. No stubs or stand-ins. Do not change v3.
|
||||
- A statement about what v3 or v4 does today is checked in the code before it is relied on. A difference from the F18 is noted in a line and not held for ruling.
|
||||
- Both stacks stay at 32 (`V4_DATA_RING=30`, `V4_RET_RING=31`). A text may leave 28 values on the stack.
|
||||
- `V4_WIRE_CELLS` is 1,024, a build parameter set where `V4_PORTS` is.
|
||||
- A message is seven words and its text, four characters to a word (MESH.md section 6); on a wire it takes `7 + (length + 3) / 4` cells. A length below 0 or above 1,024 is "not a message".
|
||||
- Operation numbers and results are those of MESH.md 7d.3: 1 look, 2 take, 3 move, 4 drop, 5 put, 6 first, 7 next, 8 sleep, 9 sleep for room, 10 room; results 0 done, 1 no room, 2 nothing on that port, 3 no such message, 4 not a message.
|
||||
- Message types: 1 text, 2 output, 3 done, 4 NACK, 5 GONE. Errors: 18 "No one on that port", 19 "Message refused", 20 "Node gone", 21 "Interrupted".
|
||||
- **Every nucleus path that begins, takes or passes on a message is tested at every depth of both stacks, and with a node removed at every step of the exchange, before the task that adds it is called done.** A hang, a line not answered, or a node that does not come back is a failure; a clean error is not.
|
||||
- After each task the hosted product is compared with the commit before on the same typed session (the session of `depthsweep.py` at shallow depth, plus `WORDS`, a 400-number loop, an error inside a definition after output, `QUIT`, `ABORT"`): any difference is explained by MESH.md 7d.6 or it is a defect.
|
||||
- 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 and committed. Never delete a log.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **A put or take whose addresses run off the node's memory**, or lie in its ports. Expected: result 4, nothing copied, nothing on the wire changed. Test in Task 1.
|
||||
2. **A node that leaves a message for itself on a wire and sleeps.** Expected: it sleeps (the engine clock does not move) until another message comes; it does not go round. Tests in Task 1 (engine) and Task 3 (`AWAIT`, and printing that waits).
|
||||
3. **A message for another node behind one the node has left for itself.** Expected: it is passed on; the one left stays where it was. Test in Task 3.
|
||||
4. **A text that prints with the wire back holding exactly the room kept for its answer.** Expected: printing waits; how the text ended is never refused at the node that did it. Test in Task 3.
|
||||
5. **A node removed, or a wire cut, with messages waiting both ways.** Expected: they are let go; no other node is left blocked or with a mark at a message that is gone. Tests in Task 1 and Task 4.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: The wire in the engine
|
||||
|
||||
**Files:** Create `v4/include/v4/wire.h`, `v4/src/wire.c`, `v4/tests/test_wire.c`. Modify `v4/include/v4/node.h`, `v4/src/node.c`, `v4/include/v4/fabric.h`, `v4/src/fabric.c`, `v4/tests/test_fabric.c`.
|
||||
|
||||
**Interfaces — Produces:**
|
||||
|
||||
```c
|
||||
/* wire.h -- a queue of whole messages, one way along a wire */
|
||||
#ifndef V4_WIRE_CELLS
|
||||
#define V4_WIRE_CELLS 1024
|
||||
#endif
|
||||
typedef struct {
|
||||
v4_cell cell[V4_WIRE_CELLS]; /* the messages, one after another, going round */
|
||||
unsigned head; /* where the front message begins */
|
||||
unsigned used; /* how many cells they take */
|
||||
unsigned mark; /* how many cells from the front the reader's mark is; == used: at no message */
|
||||
unsigned arrived; /* not 0: a message has come since the reader last asked which wires have one */
|
||||
} v4_wire_queue;
|
||||
|
||||
enum { V4_WIRE_DONE = 0, V4_WIRE_NO_ROOM = 1, V4_WIRE_NO_ONE = 2, V4_WIRE_NO_MESSAGE = 3, V4_WIRE_NOT_A_MESSAGE = 4 };
|
||||
|
||||
void v4_wire_reset(v4_wire_queue *q);
|
||||
unsigned v4_wire_cells(v4_cell length); /* cells a message of that length takes; 0: not a message */
|
||||
int v4_wire_room(const v4_wire_queue *q, unsigned cells);
|
||||
int v4_wire_put(v4_wire_queue *q, const v4_cell *header7, const v4_cell *text); /* a V4_WIRE_ result */
|
||||
void v4_wire_first(v4_wire_queue *q);
|
||||
int v4_wire_next(v4_wire_queue *q); /* V4_WIRE_DONE, or V4_WIRE_NO_MESSAGE at the end */
|
||||
int v4_wire_look(const v4_wire_queue *q, v4_cell *header7);
|
||||
int v4_wire_take(v4_wire_queue *q, v4_cell *header7, v4_cell *text, unsigned text_cap);
|
||||
int v4_wire_drop(v4_wire_queue *q);
|
||||
int v4_wire_move(v4_wire_queue *from, v4_wire_queue *to); /* the marked message, its fourth word one fewer */
|
||||
```
|
||||
|
||||
- `node.h`: indexes `V4_PORT_WIRE (V4_PORTS + 4)`, `V4_PORT_A`, `V4_PORT_B`, `V4_PORT_DO`, `V4_PORT_HOW`, `V4_PORT_HAVE (V4_PORTS + 9)`. Node fields `unsigned wire; v4_cell wire_a, wire_b; int doing; v4_cell do_op; v4_cell how; unsigned have;`. A store to `V4_PORT_DO` records the operation and blocks the node (`doing = 1`) until whoever connects its ports has done it.
|
||||
- `int v4_node_doing(const v4_node *n);` and `void v4_node_done(v4_node *n, v4_cell how);` for that connector; `void v4_node_have(v4_node *n, unsigned mask);`.
|
||||
- `fabric.h`: `void v4_fabric_queues(v4_fabric *f, v4_wire_queue *pool, unsigned count);` — the owner gives the fabric the queues; wiring a port takes two, unwiring gives them back, and `v4_fabric_wire`/`v4_fabric_wire_device` fail when none are left. `int v4_fabric_device_put(v4_fabric *f, unsigned node, unsigned port, const v4_cell *header7, const v4_cell *text);` and `int v4_fabric_device_take(v4_fabric *f, unsigned node, unsigned port, v4_cell *header7, v4_cell *text, unsigned text_cap);` for what is on the other end of a device's wire.
|
||||
- The word-at-a-time ports, `v4_device`'s `take`/`give`, and the looking words stay as they are in this task.
|
||||
|
||||
- [ ] **Step 1: Write `v4/tests/test_wire.c`, failing.** The queue alone, no node:
|
||||
- an empty queue: room for 1,024, look/take/drop answer 3;
|
||||
- put of a message of 0, 1, 4, 5 and 1,024 characters: each takes `v4_wire_cells`; taken back, the seven words and every word of text are what was put, in order, across the end of the array (fill and empty it three times with lengths that do not divide 1,024);
|
||||
- a put that does not fit answers 1 and changes nothing (compare `head`, `used`, and every cell);
|
||||
- a length of -1 and of 1,025 answers 4 and changes nothing;
|
||||
- first/next walk three messages and then answer 3; take or drop of the middle one leaves the first and third in order and the mark at the third;
|
||||
- move takes the marked message from one queue to the back of another with its fourth word one fewer, and answers 1, changing neither queue, when the other has no room;
|
||||
- `take` with `text_cap` too small answers 4 and takes nothing;
|
||||
- `arrived` is set by a put and by a move into the queue.
|
||||
- [ ] **Step 2: Run** `make -C v4 run-64-test_wire.c`. Expected: it does not compile, naming `v4/wire.h`.
|
||||
- [ ] **Step 3: Write `wire.h` and `wire.c`.** No allocation; every function leaves the queue as it found it unless it answers `V4_WIRE_DONE`. `v4_wire_cells(length)` is `length < 0 || length > 1024 ? 0 : 7 + (length + 3) / 4`.
|
||||
- [ ] **Step 4: Run** `make -C v4 run-64-test_wire.c run-32-test_wire.c` and both under `sanitize`. Expected: 0 failures.
|
||||
- [ ] **Step 5: Write the fabric tests, failing**, in `test_fabric.c` (small assembled programmes, as there):
|
||||
- a node puts a message on its wire 0 and a neighbour takes it from its wire 3: the words are what was put; "how it went" is 0 on both; each took one engine step blocked;
|
||||
- a put with nothing on the port is 2; to a wire with no room is 1; neither changes the wire;
|
||||
- addresses off the end of memory, or inside the ports, for look, take and put: 4, and nothing copied (Review Focus 1);
|
||||
- "which wires have a message" has the bit of each wire with one, and fetching it clears `arrived`;
|
||||
- sleep: a node with nothing arrived is blocked and the engine clock does not move for 500 steps; a put to one of its wires wakes it; **a node that looks, leaves the message there, fetches "which wires" and sleeps again stays asleep** until a second message comes (Review Focus 2);
|
||||
- sleep for room: woken when the reader takes enough; woken by a message coming instead, and "how it went" says which;
|
||||
- room answers at once and changes nothing;
|
||||
- move from one of a node's wires to another; with the other full, 1 and nothing moved;
|
||||
- a node removed with messages waiting both ways on two wires: the neighbours' queues are empty, their marks at no message, and a neighbour asleep for room on that wire is woken with 2 (Review Focus 5);
|
||||
- a device's wire: `v4_fabric_device_put` and `_take` each way;
|
||||
- wiring a third wire when the pool has queues for two fails and changes nothing.
|
||||
- [ ] **Step 6: Run** `make -C v4 run-64-test_fabric.c`. Expected: it does not compile, naming `V4_PORT_WIRE`.
|
||||
- [ ] **Step 7: Implement** the node indexes and fields, and in `v4_fabric_step`, after the nodes have executed, a loop that does each `doing` node's operation whole and calls `v4_node_done`, but for 8 and 9, which stay until their condition holds. Before the nodes execute, each awake node's "which wires" mask is set.
|
||||
- [ ] **Step 8: Run** `make -C v4 -k test` and `sanitize`. Expected: 0 failures; the nucleus does not use any of it yet, and `hosted-check` is unchanged.
|
||||
- [ ] **Step 9: Commit** `feat(v4.0.0): the wire in the engine -- whole messages, put and taken in one step`, chained on `hosted-check`, and push.
|
||||
|
||||
### Task 2: The products' host and the test hosts speak whole messages
|
||||
|
||||
**Files:** Modify `v4/system/boot.c`, `v4/tools/hosted.c`, `kernel/src/v4/sk_v4.c`, `v4/tests/test_host_quit.c`, `v4/tests/test_host_mesh.c`, `v4/tests/test_host_unit.c`, and any other `v4/tests/test_host_*.c` whose loop serves a node's console (find them with `grep -ln 'ask_port == CONSOLE_PORT\|console_take' v4/tests v4/system v4/tools kernel/src/v4`).
|
||||
|
||||
**Interfaces — Consumes:** Task 1. **Produces:** in `boot.c`, for the lone node, the same operations done by the host: port 1 is the console's wire and port 0 the kernel's; `v4_boot` owns two `v4_wire_queue` for each. The test hosts' consoles use `v4_fabric_device_put`/`_take`.
|
||||
|
||||
This task changes no behaviour by itself: it is done together with Task 3 and committed with it, because the nucleus and its hosts change over at once. It is a separate task so that the hosts are written, and read, before the nucleus is.
|
||||
|
||||
- [ ] **Step 1:** In each host, replace the word-at-a-time console (a `v4_message` filled word by word from `asking` on the console port, and words given one at a time to a read) with whole messages: a typed line is put on the node's console wire; each step, every message on the wire from the node is taken and handled (output shown; done ends the line).
|
||||
- [ ] **Step 2:** Kernel requests stay words: `asking` on port 0 is served as now.
|
||||
- [ ] **Step 3:** In `boot.c` the lone node's operations are done by a function `boot_do(b)`, called each pass when `v4_node_doing(n)`; sleep ends when a line has been put on its wire; sleep for room cannot fail to find room once the host has taken the console's messages, which it does every pass.
|
||||
- [ ] **Step 4:** Do not run the suite yet: it passes only with Task 3.
|
||||
|
||||
### Task 3: The nucleus keeps no messages but the one it is doing
|
||||
|
||||
**Files:** Modify `v4/capsule/core.v4`, `v4/capsule/quit.v4`, `v4/tests/host_map.h`, `v4/tools/mkimage.c` if it lists the cells, `capsules/v4/hera.4th` only if a word it uses changes its stack effect, `v4/tools/depthsweep.py`. Tests: `v4/tests/test_host_mesh.c`, `v4/tests/test_host_unit.c`, `v4/tests/test_host_quit.c`; create `v4/tests/test_host_depth.c`.
|
||||
|
||||
**Interfaces — Consumes:** Tasks 1 and 2. **Produces (nucleus words):**
|
||||
- `(DO) ( op -- how )` stores the operation and fetches how it went. `(WIRE!) ( port -- )` by its number.
|
||||
- `(ROOM?) ( cells port -- flag )` operation 10.
|
||||
- `(PUT) ( text-addr port -- how )` puts the message whose seven words are in `(OUT-HDR)` and whose text is at the word address given.
|
||||
- `(WAY) ( node -- port | -1 )` the number of the port that leads to a node.
|
||||
- `(REFUSE)` — the marked message on the wire in `(WIRE)` is dropped, counted, and a NACK put toward its sender (an answer's NACK is about the node that answered and goes toward whom the answer was for).
|
||||
- `(SERVE) ( -- )` one pass over every wire that has a message, with first and next: what is for another node is moved or refused; what is for this node is left. `(IDLE)`, `AWAIT` and the wait for room each call it and then look for what is theirs.
|
||||
- `(ROOM)` as now, tried before text begins any of this.
|
||||
- Removed: `(MQ)` and its cells, `(MQ!)`, `(MQ@)`, `(TAKE-HDR)`, `(TAKE-KEEP)`, `(TAKE)`, `(GATE)`, `(OWE)`, `(PAY)`, `(PAY1)`, `(PAID)`, `(OWED)`, `(A-QUEUE)`, `(A-HDR)`, `(A-BACK)`, `(A-TEXT)`, `(LOW)`, and the use of `(WRITERS)`/`(READERS)`. `(NEAR)` and `NEIGHBOUR` stay, for `(CENTRE)`.
|
||||
- Kept, with the same meaning to their callers: `SEND`, `SEND-ON`, `AWAIT`, `GONE`, `NO-ROUTE`, `ROUTE`, `DEFAULT-ROUTE`, `(REFUSED)`, `(LOST)`, `EMIT`, `(FLUSH-OUT)`.
|
||||
|
||||
- [ ] **Step 1: Rewrite the tests that say what must hold, failing.** In `test_host_mesh.c` (three nodes; keep `row`-style fresh rows for each scene):
|
||||
- everything in the file up to the flood that does not depend on waiting to write stays as it is and must pass unchanged;
|
||||
- *refused at once (acceptance 2, 3):* node 11 stuck in `BEGIN 0 UNTIL`. Node 10 sends it short texts in a loop: `(REFUSED)` on node 10 is 0 while they fit on the wire and rises by one for each after; node 10's line ends; nothing waited. `tell(12, ...)` and `tell(10, ...)` answer throughout;
|
||||
- *a stuck node on the way:* text for node 12 beyond stuck node 11 in a row is refused at node 10 once the wire to 11 is full, and the console is sent the NACK;
|
||||
- *a refusal owed to a stuck node:* node 11 sends 55 a message by node 12, which has no way, and sticks: node 12's NACK goes on the wire (or is counted if it is full), and node 12 is asleep, not going round, and answers;
|
||||
- *printing is not lost (acceptance 5):* node 12 does `: P 3000 0 DO I . LOOP ; P` with node 10 made slow to take (a word that loops between lines): every number arrives, in order, and then how the text ended;
|
||||
- *printing held by a stuck node:* node 12's way to the console is by stuck node 11; `P` fills the wire and node 12 sleeps for room; meanwhile a message from node 10 for a fourth number by node 12 is passed on or refused, not kept; node 11 removed: node 12's text ends "No one on that port" and it goes on;
|
||||
- *the answer has room kept (Review Focus 4):* the wire from node 12 back to node 10 is filled to within 8 cells by messages node 12 passes on; node 12 then does text from node 10 that prints: its printing waits, and how it ended arrives;
|
||||
- *left for itself (Review Focus 2, 3):* node 10 in `12 AWAIT` is sent, on the same wire, text for itself from node 11, then a message for the console from node 11, then node 12's answer: the message for the console is passed on, the wait ends with the answer, the text is then done, in that order; and between arrivals node 10's engine clock does not move;
|
||||
- *order (acceptance 8):* the `TRIO` scene of the first attempt ("DEF");
|
||||
- *`AWAIT`'s endings:* refused (NACK about the node), gone (GONE from the centre, and not from another), interrupted (text from the console), as now;
|
||||
- *the flood:* 800 messages among three nodes comes to rest; every message sent is either done or counted once (`(LOST)` plus `(REFUSED)` accounts for it exactly — the check is exact again).
|
||||
- [ ] **Step 2: In `test_host_unit.c`** (Hera and four): the scenes of step 6c for `KILL`, with these changed: text for a stuck node is refused once its wire is full, not kept; `KILL` of one stuck node with another stuck ends, and every node not stuck is told; Hera is typed to throughout (acceptance 4).
|
||||
- [ ] **Step 3: Create `test_host_depth.c`** (acceptance 7 on the mesh): on a row of three, for the data stack of node 11 at each depth from 20 to 32 and from 20 to 32 calls deep, each of: a text that prints; `SEND` to node 12; `SEND` to a node there is no way to; `12 AWAIT` answered; `12 AWAIT` with node 12 stuck, ended from the console's node by a GONE; node 11 passing a message on while its own text waits for room. After each: every node answers `1 2 + .` and node 11's stack is as deep as it was or was emptied by an error it reported.
|
||||
- [ ] **Step 4: Extend `depthsweep.py`** with: a loop that prints 400 numbers; `SEND` to a node with a route and nothing on the port.
|
||||
- [ ] **Step 5: Run** the three tests. Expected: they do not build or fail throughout, as the nucleus still writes messages a word at a time.
|
||||
- [ ] **Step 6: Write the nucleus** as MESH.md 7d.4 says, in this order, running `make -C v4 run-64-test_host_quit.c` after each: (a) `(DO)`, `(WIRE!)`, `(ROOM?)`, `(PUT)`, `(WAY)`; (b) `(FLUSH-OUT)` and `(FINISH)` — output and then how the text ended, put on the wire, with room for the answer kept and the wait for room; (c) `(IDLE)`: sleep, `(SERVE)`, take the first message for this node on any wire and do it; (d) `(REFUSE)`; (e) `SEND-ON`, `(SEND1)`; (f) `AWAIT`; (g) delete what is listed as removed, and the cells of `host_map.h` that go with it. Cells freed are noted in `host_map.h` as free.
|
||||
- [ ] **Step 7: Stack depth.** `(SERVE)`, `(REFUSE)` and everything they call keep to four cells of the data stack and four entries of the return stack, with what they need between operations held in cells, not on the stack. `test_host_quit.c`'s "28 values may wait" must still say 28.
|
||||
- [ ] **Step 8: Run** `make -C v4 -k test`, `sanitize`, `hosted-check` (which runs `depthsweep.py`). Expected: 0 failures; `test_host_depth.c` reports no node that did not come back.
|
||||
- [ ] **Step 9: Compare the product with the commit before** on the session of Global Constraints (build the commit before into the scratchpad with `git archive`). Every difference is one MESH.md 7d.6 names.
|
||||
- [ ] **Step 10: Mutation checks.** Each must make a named test fail: (a) sleep woken by a message being there, not coming; (b) the 8 cells for the answer not kept; (c) `(SERVE)` stopping at a message for itself; (d) a refused `SEND` not raising 19.
|
||||
- [ ] **Step 11: Commit** Tasks 2 and 3 together, `feat(v4.0.0): messages are whole and wait on the wire -- a node keeps none but the one it is doing`, chained on `hosted-check`, and push.
|
||||
|
||||
### Task 4: A node removed at every step; what is no longer used; the documents; the boots
|
||||
|
||||
**Files:** Modify `v4/tests/test_host_mesh.c`, `v4/src/fabric.c`, `v4/include/v4/fabric.h`, `v4/src/node.c`, `v4/include/v4/node.h` (only what Step 2 finds unused), `docs/v4.0.0/MESH.md`, `v4/README.md`, `docs/v4.0.0/ENGINE.md` if it describes the ports.
|
||||
|
||||
- [ ] **Step 1: Write the removal test, failing if anything is wrong** (acceptance 6). For each exchange — text from the console to node 12 through 11, and its printing and answer back; a `SEND` from 11 to 12 and its answer; a NACK from 12 to 10 — record how many engine steps it takes with no removal, then for every step from 0 to that many run it again on a fresh row with node 11 (and, again, node 12) removed at that step. After each: every node left comes to rest, answers `1 2 + .`, and is not blocked in any operation.
|
||||
- [ ] **Step 2: Remove what nothing uses**, found with `grep`, not assumed: the `pending` hook of `v4_device` and anything of the first attempt if any is left; the word-at-a-time `give` of the console in hosts. The looking words stay in the engine if the engine's own tests use them.
|
||||
- [ ] **Step 3: Run** `make -C v4 -k test`, `sanitize`, `hosted-check`, lint with the freshly built `mkcapsule`.
|
||||
- [ ] **Step 4: Documents.** MESH.md: 4.1's table gains the six addresses; 7a and 7b say what of them still holds (the lower-number rule and the NACKs owed do not; the endings of `AWAIT` and Hera's `KILL` do); 7b.7's limit is closed with a pointer to 7d.6; step 6e is marked done with tests, logs and the hash. `v4/README.md` likewise.
|
||||
- [ ] **Step 5: The three boots**, amd64, aarch64, riscv64, each `clean qemu` with `STARFORTH_V4=1` and the typed session of step 6d's last boots (29 values on a line; `WORDS` with 26; `AWAIT` with 27 and a line), plus a loop printing 400 numbers. Expected: POST 538 of 538, `PARITY:OK`, one `dict_hash` on all six, and the session's answers the same on all three.
|
||||
- [ ] **Step 6: Commit** `feat(v4.0.0): step 6e -- a node removed at any step leaves the rest at rest; written up`, with the logs, chained on `hosted-check`, and push.
|
||||
- [ ] **Step 7: Final review** by a fresh reviewer on the most capable model, with MESH.md 7c.6, 7c.7 and 7d, this plan's Review Focus, and the instruction to break it: faults at every depth, removal at every step, and what the products do that the commit before did not.
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user