docs(v4.0.0): plan for MACHINE.md steps 1-2 -- the ROM, and the .f18 trailer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-15 06:01:54 -04:00
co-authored by Claude Opus 5.5
parent d0b0eee54b
commit 952df0f44a
@@ -0,0 +1,420 @@
# MACHINE.md Steps 1–2: The ROM and the `.f18` Trailer — 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 machine boots from its ROM, a prebuilt F18 binary. The ROM assembles the system's
`.f18` source at boot, and the result is the same node memory the prebuilt nucleus gives today:
the same `dict_hash`, POST 538/538.
**Architecture:** The engine gains a read-only ROM address range above RAM. The ROM is an F18
assembler written in F18, assembled once off the node by `text.c`, as a bitstream is built. It
reads `.f18` text from the console input register and lays down code and data in RAM. When the
loader's last `.f18` blob ends, it jumps to the entry the source names. Today's
`v4/capsule/*.v4` is converted mechanically into `.f18` blobs. Everything `mkimage.c` did in C
to make the switch-on state (headers, variables, the image record) becomes `.f18` source,
written with generic assembler directives. After this plan, the prebuilt nucleus is gone.
**Tech stack:** C99 (engine, tests, the off-node ROM build), F18 assembler in the `text.h`
notation, Python 3 (the one-time `.v4` → `.f18` converter), GNU make.
**Spec:** `docs/v4.0.0/MACHINE.md`, sections 1–3, 5, 6 (steps 1 and 2), 7.
## Global Constraints
- The VM is a machine: registers, a control unit, the opcodes. The ROM knows no FORTH: no
dictionary layout, no `:` as FORTH means it, no `INTERPRET`. (MACHINE.md 7.1)
- The ROM is the only binary. Size: whatever it takes. (MACHINE.md 2)
- `.f18` blobs are plain text, laid out as pretty-printed F18 code, with no `Block N` format.
(MACHINE.md 7.2)
- One ROM per cell width, 32 and 64. Same behaviour on the C engine and, later, the FPGA.
- No stubs, stand-ins or "for now" code. Every task delivers working, as-intended code.
- Strict C99, `-Wall -Wextra -Werror`. The engine (`v4/src`) and `v4/system` use no C library
(the kernel links them).
- Acceptance: three ISAs, `make -f kernel/Makefile ARCH=<arch> clean qemu`, one QEMU at a time,
in the foreground, to `ok>` with `PARITY:V4_POST tests=538 pass=538 fail=0`, `POST: PASSED`
and the same `PARITY:V4_SYSTEM ... dict_hash=` on all three; then `make -C v4 test-32`.
Logs committed under `logs/`.
- Work on `StarForth-v4.0.0`. Commit and push per task. No branches. No stash.
## Review Focus
1. **A blob that ends mid-token or mid-definition**, such as a file with no trailing newline,
or one whose last word has an unresolved forward branch. Expected: the ROM finishes the
token, and resolution happens only at the final `start`. Test in Task 7.
2. **A forward branch that cannot reach its target** from its slot at 16384 words plus the ROM
range. Expected: the ROM reports the same error `text.c` gives and stops. It never emits a
branch that goes somewhere else. Test in Task 5.
3. **A symbol defined twice, or an opcode or reserved word used as a name.** Expected: the same
errors as `text.c`'s `define`. Test in Task 4.
4. **The ROM's scratch (symbol table, token buffer) left in RAM after assembly.** Expected: it is
zeroed before `start` jumps, so nothing of the building is left behind (as `mkimage.c` today).
Test in Task 8, in the whole-memory comparison.
5. **The console input running dry between blobs.** Expected: the ROM waits on
CONSOLE-STATUS and does not take the absence of a character as the end. Test in Task 3.
## How the ROM is checked: the oracle
`text.c` stays as the oracle. It is the ROM's specification at the bit level, and it is what
builds the ROM. Every ROM component is tested by feeding the same text to `text.c` and to the
ROM running on the golden model, then comparing node memory word for word. A test that names a
ROM routine calls it directly: the ROM build writes its symbol table (Task 2), so a test sets P
to a routine's address, runs it until it returns, and reads the stacks.
The F18 bodies of the ROM routines are written during each task against these tests. The plan
fixes each routine's name, stack effect, memory and the exact behaviour required, which is
`text.c`'s, cited by function. It does not pre-write unrun F18 code. The test is what decides
whether a routine is done.
## File structure
| File | Responsibility |
|---|---|
| `v4/include/v4/node.h`, `v4/src/node.c` (modify) | The ROM range: `V4_ROM_BASE`, `V4_ROM_WORDS`, `v4_node_rom_attach`. Fetch and execute from ROM; a store there is an address fault. |
| `capsules/v4/rom/*.f18` (create) | The ROM's own source, one file per part: `in.f18` (input and tokens), `num.f18`, `sym.f18`, `pack.f18`, `word.f18`, `dir.f18`, `main.f18` |
| `v4/tools/mkrom.c` (create, replaces `mkimage.c`) | Off the node: assemble `capsules/v4/rom/*.f18` with `text.c` at `V4_ROM_BASE`. Write `v4/build/rom-<w>.c` (the binary, as `const v4_cell v4_rom_<w>[]`) and `v4/build/rom-<w>.sym` (name, address) for the tests. |
| `v4/tests/test_rom_*.c` (create) | One per ROM part, against the oracle |
| `v4/tools/v4tof18.py` (create; run once, then deleted in Task 9) | Convert `v4/capsule/*.v4` to `capsules/v4/sys/*.f18` |
| `capsules/v4/sys/*.f18` (create, generated once, then source) | The system's primitives: today's nucleus, as `.f18` blobs |
| `capsules/v4/sys/map-32.f18`, `map-64.f18` (create) | The memory map's constants (today `host_load`'s `v4_text_constant` calls), and the switch-on stores and image record (today `mkimage.c`'s `main`) |
| `v4/system/boot.c` (modify) | Attach the ROM, feed the `.f18` blobs to the console input, wait for the ROM's `start`, then go on as today with `.4th` |
| `v4/include/v4/image.h`, `v4/src/image.c` (modify) | `v4_image` read from the image record the `.f18` source stores in RAM, not from a generated C file |
| `v4/Makefile`, `kernel/Makefile` (modify) | Build the ROM, drop the nucleus image |
| Deleted at the end | `v4/tools/mkimage.c`, `capsules/v4/nucleus-64.f18`, `v4/capsule/*.v4`, `v4/tools/v4tof18.py` |
## The ROM's language
These are the generic directives that let the system's source say what `mkimage.c` and
`text.c`'s `header` say today. Each is a machine notion: an address, a cell, a value. None is
FORTH.
| Directive | Effect |
|---|---|
| everything in `text.h`'s notation | as `text.c`, bit for bit: opcodes, numbers, constants, `CONST+N`, `&NAME`, `: NAME`, labels, branches, `macro … endmacro`, `FOR`/`NEXT`/`UNEXT`, `\` and `(` comments |
| `constant NAME VALUE` | as `v4_text_constant` |
| `set NAME VALUE` | an assembler variable: like a constant, but may be set again |
| `here` | as a VALUE: the next word address (`v4_text_here`) |
| `#, VALUE` | one cell of data at `here` (`text.c` `put_cell`) |
| `", TOKEN` | TOKEN as a counted string: count byte, then the characters, four to a cell, low byte first, zero-padded to a whole cell (as `header` lays a name today) |
| `store ADDR VALUE` | `ADDR` := `VALUE`, anywhere in RAM (the switch-on state) |
| `start NAME` | the end of the system's `.f18`: as `v4_text_finish`, then zero the ROM's scratch, then jump to NAME |
VALUE is a number, a constant, a set variable, `CONST+N`, `here`, or `&NAME`.
Today's `header NAME f1 f2 : CODE` becomes, mechanically (Task 9):
```
set (NFA) here ", NAME #, F #, (NFA) #, (LINK) set (LINK) here
: CODE
```
Here F is the flags OR'd together: immediate 1, data 4, inline 8, compile-only 16 (`text.c`
`flag_of`). At the end, `store` puts `(LINK)` into `(LATEST)`. That is exactly the memory
`header` lays today.
---
### Task 1: The ROM range in the engine
**Files:**
- Modify: `v4/include/v4/node.h`, `v4/src/node.c`, `v4/src/exec.c` (instruction fetch)
- Test: `v4/tests/test_rom_range.c`
**Interfaces:**
- Produces: `V4_ROM_BASE` (= `V4_NODE_WORDS`), `V4_ROM_WORDS` (build parameter, default 4096);
`void v4_node_rom_attach(v4_node *n, const v4_cell *rom, unsigned words);`. A load or
instruction fetch at `V4_ROM_BASE + i` gives `rom[i]`. A store there calls
`v4_node_fault(n, V4_FAULT_ADDR, addr)`. `v4_node_addr_ok` accepts the ROM range for loads
only. `v4_node_reset` keeps the ROM attached: it is the machine, not state.
- [ ] **Step 1: Write the failing test**
```c
/* test_rom_range.c -- the ROM is the machine's: it can be read and run, never written. */
#include "v4/asm.h"
#include "v4/exec.h"
#include "v4/testcode.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
static v4_node n; static v4_exec_state es; static v4_heat h;
static v4_cell rom[8];
int main(void)
{
v4_asm as;
/* ROM word 0: 7 ; */
v4_node_reset(&n);
rom[0] = 0; rom[1] = 0;
{ v4_node scratch; v4_asm ra;
v4_node_reset(&scratch);
v4_asm_begin(&ra, &scratch, 0);
v4_asm_lit(&ra, 7); v4_asm_op(&ra, V4_OP_SEMI);
CHECK(v4_asm_ok(&ra), "ROM code assembles");
rom[0] = scratch.mem[0]; rom[1] = scratch.mem[1];
}
v4_node_rom_attach(&n, rom, 8);
CHECK(v4_node_load(&n, V4_ROM_BASE) == rom[0], "ROM reads");
/* RAM 16: call ROM, then ; */
v4_asm_begin(&as, &n, 16);
v4_asm_branch(&as, V4_OP_CALL, V4_ROM_BASE);
v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "a call into the ROM assembles");
CHECK(testcode_run(&n, &es, &h, 16) == 7, "code in the ROM runs: left %ld", (long)n.t);
v4_node_store(&n, V4_ROM_BASE + 1, 99);
CHECK(v4_node_load(&n, V4_ROM_BASE + 1) == rom[1], "a store into the ROM changes nothing");
CHECK(n.fault_kind == V4_FAULT_ADDR, "and is an address fault");
v4_node_reset(&n);
CHECK(v4_node_load(&n, V4_ROM_BASE) == rom[0], "reset keeps the ROM");
printf("test_rom_range: %d checks, %d failures\n", checks, failures);
return failures != 0;
}
```
(`testcode_run`, `n.t` and `n.fault_kind` are the names `v4/include/v4/testcode.h` and
`node.h` use. Before writing, read both and use their exact names. If the fault is observed
through the fault table rather than a field, check it the way `v4/tests/test_guard.c` does.)
- [ ] **Step 2: Run it and see it fail.** `make -C v4 test` should fail to compile with
`v4_node_rom_attach` undeclared.
- [ ] **Step 3: Implement.** In `node.h`, declare the API above and add `const v4_cell *rom;`
and `unsigned rom_words;` to `v4_node`. In `node.c`, `v4_node_load`, `v4_node_fetch` and the
instruction fetch in `exec.c` map `V4_ROM_BASE <= a < V4_ROM_BASE + rom_words` to `rom[a -
V4_ROM_BASE]`. `v4_node_store` faults there. `v4_node_reset` saves and restores
`rom`/`rom_words` around its clear. Also check that `v4_iword_slot_mask` with `text.c`'s
placement rule still places branches correctly when the address space is `V4_NODE_WORDS +
V4_ROM_WORDS`. If `place_branch` uses `V4_NODE_WORDS - 1` as "the whole node", change it to
the top of the ROM range.
- [ ] **Step 4: Run it and see it pass.** `make -C v4 test test-32`: every test passes at both
widths.
- [ ] **Step 5: Commit.** `feat(v4): the machine has a ROM -- read and run, never written`
### Task 2: `mkrom`, the off-node ROM build, and its first part: input
**Files:**
- Create: `v4/tools/mkrom.c`, `capsules/v4/rom/in.f18`, `v4/tests/test_rom_in.c`
- Modify: `v4/Makefile` (rule: `$(BINDIR)/rom-<w>.c` and `.sym` from `capsules/v4/rom/*.f18`,
per width; the ROM tests link `rom-<w>.c`)
**Interfaces:**
- Produces: `extern const v4_cell v4_rom_64[]; extern const unsigned v4_rom_64_words;` (and
`_32`). In `rom-<w>.sym`, one `NAME ADDRESS` per line. A test helper in
`v4/tests/rom_harness.h`:
`v4_cell rom_addr(const char *name);`
`void rom_boot(v4_node *n);` (attach the ROM and the console input),
`int rom_call(v4_node *n, const char *name, unsigned max_steps);` (push a return to an idle
word, set P, run until it returns; 1 if it returned).
- ROM routines from `in.f18` (the console input is `CONSOLE-RX`/`CONSOLE-STATUS`, as
`host_map.h`):
- `(KEY) ( -- c )`: wait while CONSOLE-STATUS is 0, then fetch CONSOLE-RX.
- `(TOKEN) ( -- len )`: skip blanks (space, tab, CR, LF), counting LFs into `(LINE)`. Read up
to the next blank into `(TOK)`, at most 63 characters, then NUL. `\` skips to end of line
and `(` skips to `)`, as `text.c` `next_token`. Length 0 means the end of input was marked
by EOT (4), which the loader sends after the last blob. A token longer than 63 is the
error "name too long".
- [ ] **Step 1: Write the failing test.** In `test_rom_in.c`, feed
`" dup \\ c\n ( x ) over\t; \x04"` with `v4_node_console_feed`. Call `(TOKEN)` four times
and expect `dup`, `over`, `;`, then length 0. Expect `(LINE)` to be 1 after the first. Also
feed nothing, run `(TOKEN)` for 10000 steps and expect it to be still waiting with P inside
`(KEY)` (Review Focus 5). Then feed `"x\x04"` and expect it to finish with `x`.
- [ ] **Step 2: Run it and see it fail** (no `mkrom`, no `rom-64.c`).
- [ ] **Step 3: Implement.** Write `mkrom.c` from `mkimage.c`'s `main`: `v4_text_begin(&tx, &n,
V4_ROM_BASE)`, the ROM's constants (`CONSOLE-RX`, `CONSOLE-STATUS`, and the scratch addresses
below), assemble each `capsules/v4/rom/*.f18` in a fixed order, `v4_text_finish`, then write
the words from `V4_ROM_BASE` to `here` as the C array and every symbol as `.sym`. Write
`in.f18`. The ROM's scratch RAM is in dictionary space, which is free during assembly:
`(TOK)` 16 cells, `(LINE)`, and the tables of Task 4. Put their addresses as constants in
`mkrom.c`, next to `host_map.h`'s `DICT_W`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): mkrom -- the ROM built off the node; it reads tokens`
### Task 3: Numbers
**Files:** Create `capsules/v4/rom/num.f18`, `v4/tests/test_rom_num.c`
**Interfaces:** `(NUMBER) ( -- n flag )` on `(TOK)`: flag 1 if the whole token is a number, 0
if not, -1 if it is a number that does not fit a cell. Same rules as `text.c` `number_of`:
optional `$` for hex (either case of a–f) or a leading `-`; it must fit as signed or as unsigned.
- [ ] **Step 1: Write the failing test.** A table of tokens and expected `(n, flag)`. Run each
through `text.c`'s rule as well, so the test cannot drift from it: `0 1 -1 $ff $FF $7FFF…
(max) $1…0 (max+1, flag -1) -$1 (flag 0) 1a (0) - (0) $ (0)`, at the width under test. Feed
each token with EOT, call `(TOKEN)` then `(NUMBER)`, and compare.
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Implement** `(NUMBER)` in `num.f18`. Multiply by 10 or 16 with `+*`, or with
shifts and adds. Detect overflow per `number_of`: unsigned beyond `~0`, or a negative beyond
`V4_MSB`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM reads numbers as text.c does`
### Task 4: The symbol table
**Files:** Create `capsules/v4/rom/sym.f18`, `v4/tests/test_rom_sym.c`
**Interfaces:**
- Memory: `(SYMS)`, records of `name-offset kind value`; `(POOL)`, packed name characters;
`(NSYM)`, `(POOLN)`. Capacity matches `text.h`: 2048 symbols, a 65536-character pool. Kinds:
1 word, 2 constant, 3 macro, 4 set variable.
- `(FIND) ( -- rec | 0 )` finds `(TOK)`, newest first, as `text.c` `find`.
- `(DEFINE) ( value kind -- )` defines `(TOK)` with `text.c` `define`'s refusals: an opcode,
a reserved word (`text.c` `is_reserved`, plus `constant set here #, ", store start`), a label
form, a number, a name starting `&`, "already defined" (except kind 4 redefining kind 4),
"too many names". On error it stores the message and line in `(ERR)`/`(ERRLINE)` and sets
`(FAILED)`.
- `(OPCODE) ( -- op | -1 )` matches the 32 mnemonics (`text.c` `mnemonic[]`).
- [ ] **Step 1: Write the failing test.** Define `A` const 5, `B` word 100, `C` macro, and find
each. Then `A` again (error "already defined"), `dup` (error "cannot be used as a name"),
`x:`, `12`, `&y`, and `set V 1` then `set V 2` (both allowed, find gives 2). For each case,
run the same input through `text.c` and require the same failed/ok outcome (Review Focus 3).
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Implement** `sym.f18`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM's symbol table`
### Task 5: The slot packer
**Files:** Create `capsules/v4/rom/pack.f18`, `v4/tests/test_rom_pack.c`
**Interfaces:** `(OP) ( op -- )`, `(LIT) ( v -- )`, `(BRANCH) ( op target -- )`,
`(BRANCH>) ( op -- ref )`, `(RESOLVE) ( ref target -- )`, `(LABEL) ( -- addr )`. The state is
`(HERE) (CUR) (SLOT) (LITS)×6 (NLIT)` in scratch. The behaviour is `asm.c` exactly: `@p` plus
the literal after the word, `;` and `ex` close the word, a branch closes it and goes to a new
word from slot 4 or 5, the reach check, plus `text.c` `place_branch` (a branch only in a slot
that reaches the whole address space).
- [ ] **Step 1: Write the failing test.** Generate 2000 random sequences of op/lit/branch/label
with a fixed seed. Apply each with `asm.c` + `place_branch` into node A, and with the ROM
routines into node B. Compare RAM word for word. Add a fixed case where a forward branch
from slot 3 cannot reach: both must fail (Review Focus 2).
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Implement** `pack.f18`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM's slot packer, word for word with asm.c`
### Task 6: Words, labels, branches by name, macros, `FOR`
**Files:** Create `capsules/v4/rom/word.f18`, `v4/tests/test_rom_word.c`
**Interfaces:** `(ONE) ( -- )` assembles the token in `(TOK)`, as `text.c` `one_token` does
for everything except the directives: `:`, `LABEL:`, branch mnemonics with their target,
labels local to a word and settled at `end_word`, a bare name as a call (now or forward),
`&NAME`, a number, a constant, `CONST+N`, a macro (expanded through a reader stack 8 deep),
`FOR`/`NEXT`/`UNEXT`. The forward-reference table is `text.h`'s `V4_TEXT_MAX_REFS`.
- [ ] **Step 1: Write the failing test.** For each of `v4/tests/test_text.c`'s source texts
(reuse them: same inputs, same expected outcomes), assemble with `text.c` into A, and with
the ROM fed the text plus `start` into B. Compare RAM, and the failed/ok outcome and the
message.
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Implement** `word.f18`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM assembles words, labels, macros and FOR`
### Task 7: The directives and `start`
**Files:** Create `capsules/v4/rom/dir.f18`, `capsules/v4/rom/main.f18`,
`v4/tests/test_rom_dir.c`
**Interfaces:** `constant set here #, ", store start`, as in "The ROM's language" above.
`main.f18` holds the ROM's entry at `V4_ROM_BASE`: clear its scratch, then loop `(TOKEN)` /
directive or `(ONE)`, stopping at an error or at `start`. On error it writes `(ERR)` to
CONSOLE-TX and halts at a known address `(ROM-FAILED)`. `start NAME` does `text.c`
`v4_text_finish` (resolve every reference, "never defined"), zeroes the scratch (Review
Focus 4), and jumps to NAME.
- [ ] **Step 1: Write the failing test.** Five cases:
- `#, 5 ", DUP store 9000 7 set L here #, L` with `start` to a word that does `;`:
check each cell.
- A blob split over two feeds, with an EOT only after the second, and a forward call
resolved by a word in the second (Review Focus 1).
- A reference never defined: error, and P at `(ROM-FAILED)`.
- After `start`, every scratch cell is zero.
- A header written in the directive form gives the same memory as `text.c`'s `header` for
the same name and flags.
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Implement** `dir.f18` and `main.f18`.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM's directives and start`
### Task 8: The proof — the ROM rebuilds today's nucleus exactly
**Files:** Create `v4/tools/v4tof18.py`, `capsules/v4/sys/*.f18` (generated by the script),
`capsules/v4/sys/map-32.f18`, `map-64.f18`, `v4/tests/test_rom_nucleus.c`
**Interfaces:** `v4tof18.py` reads `v4/capsule/*.v4` in `mkimage.c`'s order (`core input dict
codegen compile quit forth numout words system qmath blocks log acl`) and writes one `.f18` per
file. It rewrites `header NAME flags :` into the directive form and leaves everything else
token for token. `map-<w>.f18` holds:
- a `constant` for each `host_load` constant;
- `store`s for every switch-on value `mkimage.c` `main` sets (`DP LATEST STATE CFP BASE FENCE
LOG_LEVEL CONTEXT CURRENT SRC BOOT_CELLS LINE_STATUS`, `HOST_MESSAGES_EMPTY`'s cells);
- the image record (Task 9);
- last, `start (IDLE)`.
- [ ] **Step 1: Write the failing test.** `test_rom_nucleus.c` builds node A the way
`mkimage.c` does (`host_load`, `v4_text_finish`, `main`'s stores) and node B by feeding the
ROM `map-<w>.f18` (constants), the 14 `sys/*.f18` files in order, then the rest of
`map-<w>.f18`, then EOT. Run B until P reaches `(IDLE)`. Require every RAM word equal, A to
B. On a difference, print the first address and the symbol at or below it.
- [ ] **Step 2: Run it and see it fail.**
- [ ] **Step 3: Write `v4tof18.py`, generate the files, and fix the ROM** until the memory is
equal at both widths. Fix the ROM, not the test. The test is `text.c` and `mkimage.c` as they
are.
- [ ] **Step 4: Run it and see it pass**, at both widths.
- [ ] **Step 5: Commit.** `feat(v4): the ROM assembles the system's .f18 to the nucleus, word
for word`
### Task 9: Boot from the ROM; the nucleus binary goes
**Files:**
- Modify: `v4/system/boot.c` (replace `load_nucleus`), `v4/include/v4/image.h`,
`v4/src/image.c`, `v4/tools/hosted.c`, `kernel/src/v4/sk_v4.c`, `v4/Makefile`,
`kernel/Makefile`, `capsules/v4/hera.4th` (`(NUCLEUS)`: see the note below)
- Delete: `v4/tools/mkimage.c`, `capsules/v4/nucleus-64.f18`, `v4/capsule/*.v4`,
`v4/tools/v4tof18.py`, `v4/tests/test_rom_nucleus.c`'s node-A half (it built from `.v4`;
from here the `.f18` are the source, and the test becomes "boot reaches `(IDLE)` with POST
passing")
**Interfaces:**
- `v4_image` is filled from the image record: a fixed RAM block whose address is a constant of
the map, which `map-<w>.f18` `store`s. It holds `idle`, `fault_table`, `key_start`,
`key_end` and the register addresses `v4_image` holds today. `v4_image_from_node(const
v4_node *n, v4_image *im)` reads it.
- `boot.c`:
1. `v4_node_rom_attach(n, v4_rom_<w>, v4_rom_<w>_words)`, P = `V4_ROM_BASE`.
2. Feed `v4:sys/map-<w>.f18` (its constants part), then the system `.f18` blobs, then the
rest of the map, then EOT, each found, hash-checked and signature-checked as
`NUCLEUS.md` 5.3 items 1–3.
3. Run until P = idle. On `(ROM-FAILED)`, print the ROM's message, then `PARITY:FAIL` and
`POST: FAILED`.
4. `v4_image_from_node`, then the `.4th` capsules, POST and `COLD` exactly as today.
- `PARITY:V4_NUCLEUS` becomes `PARITY:V4_ROM words=N rom_hash=0x…` plus one
`PARITY:V4_F18 name=… capsule_hash=…` per blob.
- **Note on Hera:** a node Hera births is a v4 machine too, so it has its ROM. `(NUCLEUS)`
streams the system `.f18` blobs to it as text, not the nucleus binary. If `hera.4th`'s tests
(`test_host_unit.c`) need more than that change, stop and report before going further.
- [ ] **Step 1: Write the failing test.** `make -C v4 hosted-check` with `nucleus-64.f18`
removed from the capsule directory must still boot to `PARITY:OK` on all three hosted ISAs,
with `PARITY:V4_SYSTEM dict_hash=` equal to the value before this task. Run it first to
record that value.
- [ ] **Step 2: Run it and see it fail** (no nucleus).
- [ ] **Step 3: Implement** the boot change and the deletions.
- [ ] **Step 4: Run it and see it pass.** `make -C v4 test test-32 hosted-check`.
- [ ] **Step 5: Acceptance.** Read `.claude/CLAUDE.md` "Running / Acceptance" first. Then:
- `git status --short disk` must be clean.
- `make -f kernel/Makefile ARCH=amd64 clean qemu`, then `aarch64`, then `riscv64`: one QEMU
at a time, in the foreground, each to `ok>`. Every log must show POST 538/538,
`POST: PASSED`, and the same `dict_hash` as before this task.
- `make -C v4 test-32`: 0 failures.
- Commit the logs plain (memory `git-lfs-logs-plain`).
- [ ] **Step 6: Commit.** `feat(v4): the machine boots from its ROM; the prebuilt nucleus is
gone`
---
## What this plan does not do (later plans, MACHINE.md section 6)
- Step 3, `init.4th` after POST.
- Step 5, FORTH-79 moved out of `.f18` into `forth79.4th` (all 130 names), plus `starforth.4th`.
- Step 6, v3 out of the build.
- Step 4 beyond the note in Task 9.