From 952df0f44a0a43ed927d11ca3ec6194572a7bcb0 Mon Sep 17 00:00:00 2001 From: rajames Date: Thu, 15 Oct 2026 06:01:54 -0400 Subject: [PATCH] docs(v4.0.0): plan for MACHINE.md steps 1-2 -- the ROM, and the .f18 trailer Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-15-machine-step1-rom.md | 420 ++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md diff --git a/docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md b/docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md new file mode 100644 index 00000000..2d4e19c9 --- /dev/null +++ b/docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md @@ -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= 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-.c` (the binary, as `const v4_cell v4_rom_[]`) and `v4/build/rom-.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 + +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-.c` and `.sym` from `capsules/v4/rom/*.f18`, + per width; the ROM tests link `rom-.c`) + +**Interfaces:** +- Produces: `extern const v4_cell v4_rom_64[]; extern const unsigned v4_rom_64_words;` (and + `_32`). In `rom-.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-.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-.f18` (constants), the 14 `sys/*.f18` files in order, then the rest of + `map-.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-.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_, v4_rom__words)`, P = `V4_ROM_BASE`. + 2. Feed `v4:sys/map-.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.