Files
LithosAnanake/docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md
T

25 KiB
Raw Blame History

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 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-<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

/* 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).
  • 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-<w>.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-<w>.f18 holds:

  • a constant for each host_load constant;

  • stores 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, 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-<w>.f18 stores. 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.