# 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 assembly-only tables (symbol table, references) left in RAM after assembly.** Expected: they are zeroed before `start` jumps, so nothing of the building is left behind (as `mkimage.c` today). The routines' permanent state area (MACHINE.md 2.1) is not cleared: it belongs to the ROM. Test in Task 8, in the whole-memory comparison, which leaves out exactly that area. 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). - **The entry table (MACHINE.md 2.1).** `main.f18` starts at `V4_ROM_BASE` with one `jump` per public routine, in a fixed order: `(KEY) (TOKEN) (NUMBER) (FIND) (DEFINE) (OP) (LIT) (BRANCH) (BRANCH>) (RESOLVE) (LABEL)`, then the ROM's entry. Each task adds its routines' entries. Tests call routines only through the entry table, as a payload would. `rom-.sym` still names every internal routine, for debugging. - **State area.** The routines' state (`(TOK)`, `(LINE)`, the packer's `(HERE) (CUR) (SLOT) (LITS) (NLIT)`) lives in a reserved RAM area, a constant of the memory map next to `host_map.h`'s others, outside dictionary space. The assembly-only tables (Task 4's symbol table and pool, Task 6's references) live in dictionary space. - 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 assembly-only tables (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 assembly-only table cell is zero, and the state area is as the last routine left it. - 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, outside the ROM's state area. 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.