docs(v4.0.0): nucleus, FORTH-79 capsule and POST design

The assembled vocabulary shrinks to a nucleus; the FORTH-79 Required Word
Set is loaded at boot from a capsule as colon definitions, POST (v3's cases,
ported, comparing results) runs on it, and the node reaches ok> -- hosted
and bare metal, on amd64, aarch64 and riscv64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-05 15:16:00 -04:00
co-authored by Claude Opus 5.5
parent c1bbcaaba4
commit d4bd6e9601
+273
View File
@@ -0,0 +1,273 @@
# 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` | `v4:post79.4th` | The POST harness and its cases |
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
`post79.4th` defines a small harness in FORTH — a case is written
`T{ 1 2 3 ROT -> 2 3 1 }T` — and a tally. Cases that check printed output
or an expected error use harness words for those. The harness needs one
thing from the nucleus that FORTH-79 does not provide: a way to run a case
and regain control if it raises an error. That is one nucleus word.
After the last case POST prints the number of cases, passes and failures,
and names each failing case.
### 6.4 Generating the expected values
A script reads the v3 test modules, keeps the cases for Required Word Set
words, pipes each line through the hosted v3 binary, and writes the
`T{ ... }T` lines. It is a development tool, run when the cases change; its
output, `post79.4th`, is committed and reviewed like any source.
## 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_CAPSULE name=v4:post79.4th capsule_id=0x... capsule_hash=0x... dict_hash=0x...
PARITY:V4_POST tests=N pass=N fail=N
PARITY:OK
POST: PASSED
ok>
```
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 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.