Files
LithosAnanake/docs/v4.0.0/plans/2026-10-15-mesh-rom.md
T
2026-10-15 19:23:31 -04:00

47 KiB
Raw Blame History

The Mesh ROM 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: A small ROM for mesh nodes holding shared math (UM*, UM/MOD, D+, DNEGATE, Q.*, Q./), the same six routines as entries 0–5 of the host ROM, with the system's words of those names calling the ROM.

Architecture: One F18 source, capsules/v4/rom/math.f18, is assembled into both ROMs by v4/tools/mkrom.c. A C X-macro, v4/include/v4/rom_entries.h, is the one list of the six entries. v4/tools/mkmap.c turns it into the (MATH-ENTRIES) macro that both ROMs' entry tables start with, and into the ROM-UM* … constants the system's source uses. The system's six words become ROM-xxx push ;. In the test host, a node Hera births carries the mesh ROM.

Tech Stack: C99 (-Wall -Wextra -Wpedantic -Werror), the v4 text assembler notation (v4/include/v4/text.h), GNU make, QEMU for acceptance.

Spec: docs/v4.0.0/MACHINE.md section 2.2 (commit a6da45df), with sections 2 and 2.1 above it.

Global Constraints

  • Work on branch StarForth-v4.0.0 in the main checkout. Never create a branch, never stash, never --no-verify.
  • Commit with G="git -c filter.lfs.process= -c filter.lfs.clean=cat -c filter.lfs.smudge=cat -c filter.lfs.required=false"; push with PATH="$PATH:/usr/share/gitkraken/resources/app.asar.unpacked/git/libexec/git-core" $G push -q origin StarForth-v4.0.0. Commit messages end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
  • Never apply a fix that is not in this plan; report it.
  • Nothing language-specific in the ROM: no names, no headers, no FORTH (MACHINE.md 7.1).
  • The entry order is fixed: 0 UM*, 1 UM/MOD, 2 D+, 3 DNEGATE, 4 Q.*, 5 Q./; the host ROM's assembler routines are 6–17 and (ROM) is 18.
  • The six routines behave exactly as the system's words did before this plan (the tables in Task 1 are those results).
  • Acceptance (.claude/CLAUDE.md): make -f kernel/Makefile ARCH=<arch> clean qemu for amd64, aarch64 and riscv64, one QEMU at a time in the foreground, each to ok> with PARITY:V4_POST tests=538 pass=538 fail=0 and POST: PASSED, PARITY:V4_SYSTEM dict_hash= equal across the three; and make -C v4 test-32 with 0 failures. Logs are committed plain. git status --short disk must be clean before boots.
  • The suite for every task: make -C v4 test test-32 hosted-check.

Rulings made while planning

  • The system's words call the ROM with ROM-xxx push ;, not jump ROM-xxx. The spec says "a single jump". The text assembler refuses a branch to a constant ("not something to branch to", v4/src/text.c branch), and the ROM's assembler must accept the same language word for word. push ; puts the entry's address on the return stack and returns into it: the same effect, the same peak return-stack depth, one cell more. Cost if wrong: a language extension in text.c and in the ROM's assembler, and six one-line edits.
  • host_load (v4/tests/host_map.h) attaches the host ROM, so every test_host_* test links the ROM. Without it, a system that text.c builds for a test faults at its first UM*. mkmap includes host_map.h but cannot link the ROM (the ROM is built from mkmap's output), so the attach is compiled only where V4_ROM_LINKED is defined. Cost if wrong: the define and one Makefile line.
  • The mesh ROM is checked to be under 256 words (the spec says about 150 to 200). Cost if wrong: one number in a test.
  • The products do not link the mesh ROM. They have no births yet (spec, "Births"), so it would be dead code there. make -C v4 rom builds it at both widths. Cost if wrong: one object in kernel/Makefile.

Review Focus

  1. An error raised inside a ROM routine on a node with a prompt. Q./ by zero stores 11 into NODE-ERROR, which traps; the prompt must print "Division by zero" and come back, as it did when the code was in RAM. Pinned by a hosted-check line in Task 2.
  2. A node that runs the system without its ROM attached. It must fault at the first call into the ROM range, never compute a wrong answer silently. Pinned in Task 2 (a detached ROM makes UM* not return).
  3. 32-bit cells. Every table in Task 1 has its 32-bit values; make -C v4 test-32 runs them.
  4. The two entry lists cannot drift. The F18 macro is generated from the C X-macro, and Task 1 checks that each entry word of both ROMs jumps to the routine of that name.
  5. Return-stack depth through the trampoline at the deepest Q48 chains (Q.LOG → Q./ → (UQ/) → D2*C). The trampoline's push is popped by its ; before the routine runs, so the peak is unchanged; POST's Q48 cases exercise it on the host.

Task 1: The math in both ROMs, through one entry list

Files:

  • Create: v4/include/v4/rom_entries.h
  • Create: capsules/v4/rom/math.f18
  • Create: capsules/v4/rom/mesh.f18
  • Create: v4/tests/test_host_rom_math.c
  • Modify: capsules/v4/rom/main.f18 (the entry table)
  • Modify: v4/tools/mkmap.c (the (MATH-ENTRIES) macro and the math's constants in the ROM map)
  • Modify: v4/tools/mkrom.c (an array-name argument)
  • Modify: v4/Makefile (the mesh ROM, math.f18 in the host ROM)
  • Modify: v4/tests/rom_harness.h (the entry list, the start, the mesh ROM's externs)
  • Modify: v4/tests/test_host_rom_asm.c:72 (the start)
  • Modify: v4/system/boot.c:415-430 (the start)

Interfaces:

  • Produces: V4_ROM_MATH_ENTRIES(X) with X(index, routine, forth, effect); V4_ROM_MATH_COUNT (6); V4_ROM_START (18).

  • Produces: arrays v4_rom_<w>[]/v4_rom_<w>_words (unchanged names) and v4_mesh_rom_<w>[]/v4_mesh_rom_<w>_words; files $(BINDIR)/rom-<w>.{c,sym} and $(BINDIR)/mesh-rom-<w>.{c,sym}.

  • Produces: in rom_harness.h, MESH_ROM, MESH_ROM_WORDS; the test define V4_MESH_ROM_SYM.

  • Step 1: Write the failing test

Create v4/tests/test_host_rom_math.c:

/* test_host_rom_math.c -- the ROMs' shared math (docs/v4.0.0/MACHINE.md 2.2).
 *
 * Both ROMs begin with the same six entries (v4/include/v4/rom_entries.h):
 * UM*, UM/MOD, D+, DNEGATE, Q.* and Q./.  Each is called through its entry,
 * on the host ROM and on the mesh ROM, with the values below, and must give
 * what the system's words of those names gave before they moved into the
 * ROM.  The tables are those words' results as text.c assembled the system
 * on 2026-10-15, before the move, at each width.
 */
#include "v4/rom_entries.h"
#include "v4/iword.h"
#include "rom_harness.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)

#define M      ((v4_cell)V4_MSB)                 /* the most negative cell */
#define X      ((v4_cell)~(v4_ucell)V4_MSB)      /* the largest */
#define ONES   (~(v4_ucell)0)
#define CANARY ((v4_cell)0x0C0FFEE5)

/* entry, arguments (deepest first), results (deepest first), NODE-ERROR after */
typedef struct { unsigned entry, argc; v4_cell in[4]; unsigned outc; v4_cell out[2]; v4_cell error; } math_case;

static const math_case cases[] = {
    { 0, 2, {0, 0},           2, {0, 0}, 0 },
    { 0, 2, {1, -1},          2, {-1, 0}, 0 },
    { 0, 2, {-1, -1},         2, {1, -2}, 0 },
    { 0, 2, {M, 2},           2, {0, 1}, 0 },
    { 0, 2, {12345, 67890},   2, {838102050, 0}, 0 },
    { 0, 2, {-1, M},          2, {M, X}, 0 },
    { 0, 2, {X, X},           2, {1, X / 2}, 0 },
    { 1, 3, {0, 0, 1},        2, {0, 0}, 0 },
    { 1, 3, {100, 0, 7},      2, {2, 14}, 0 },
    { 1, 3, {-1, -2, -1},     2, {-2, -1}, 0 },
    { 1, 3, {0, 1, 3},        2, {1, (v4_cell)(ONES / 3u)}, 0 },
    { 1, 3, {-1, 0, 10},      2, {5, (v4_cell)(ONES / 10u)}, 0 },
#if V4_CELL_BITS == 64
    { 1, 3, {12345, 6, 7},    2, {2, -2635249153387077039LL}, 0 },
#else
    { 1, 3, {12345, 6, 7},    2, {0, -613564993L}, 0 },
#endif
    { 1, 3, {M, X - 1, X},    2, {X - 1, -2}, 0 },
    { 1, 3, {-1, X, M},       2, {X, -1}, 0 },
    { 2, 4, {-1, 0, 1, 0},    2, {0, 1}, 0 },
    { 2, 4, {-1, -1, 1, 0},   2, {0, 0}, 0 },
    { 2, 4, {M, 0, M, 0},     2, {0, 1}, 0 },
    { 2, 4, {5, -1, -6, 0},   2, {-1, -1}, 0 },
    { 2, 4, {0, X, 0, 1},     2, {0, M}, 0 },
    { 2, 4, {123, 4, -200, -5}, 2, {-77, -1}, 0 },
    { 3, 2, {0, 0},           2, {0, 0}, 0 },
    { 3, 2, {1, 0},           2, {-1, -1}, 0 },
    { 3, 2, {0, 1},           2, {0, -1}, 0 },
    { 3, 2, {-1, -1},         2, {1, 0}, 0 },
    { 3, 2, {0, M},           2, {0, M}, 0 },
    { 3, 2, {5, 0},           2, {-5, -1}, 0 },
    { 4, 4, {65536, 0, 65536, 0},       2, {65536, 0}, 0 },
    { 4, 4, {163840, 0, -196608, -1},   2, {-491520, -1}, 0 },
    { 4, 4, {-65536, -1, -65536, -1},   2, {65536, 0}, 0 },
    { 4, 4, {32768, 0, 32768, 0},       2, {16384, 0}, 0 },
    { 4, 4, {0, 0, 65536, 0},           2, {0, 0}, 0 },
    { 4, 4, {0, 1, 0, 1},               2, {0, (v4_cell)1 << (V4_CELL_BITS - 16)}, 0 },
    { 4, 4, {-1, X, 131072, 0},         2, {-2, -1}, 0 },
    { 4, 4, {1, 0, 1, 0},               2, {0, 0}, 0 },
    { 5, 4, {65536, 0, 196608, 0},      2, {21845, 0}, 0 },
    { 5, 4, {-491520, -1, 131072, 0},   2, {-245760, -1}, 0 },
    { 5, 4, {65536, 0, 0, 0},           2, {-1, X}, 11 },
    { 5, 4, {-65536, -1, 0, 0},         2, {0, M}, 11 },
    { 5, 4, {0, 0, 0, 0},               2, {0, 0}, 11 },
    { 5, 4, {0, 1, 1, 0},               2, {0, 65536}, 0 },
    { 5, 4, {-1, X, 32768, 0},          2, {-1, X}, 0 },
    { 5, 4, {M, -1, -65536, -1},        2, {M, 0}, 0 },
    { 5, 4, {1, 0, 65536, 0},           2, {1, 0}, 0 },
};
#define CASES (sizeof cases / sizeof cases[0])

static v4_node n;

/* Call entry c->entry of `rom`.  1 if it returned with the canary under its
 * results; got[] the results, deepest first, *err NODE-ERROR. */
static int run(const v4_cell *rom, unsigned words, const math_case *c, v4_cell *got, v4_cell *err)
{
    unsigned i;
    rom_boot(&n);
    v4_node_rom_attach(&n, rom, words);
    v4_dstack_push(&n.ds, CANARY);
    for (i = 0; i < c->argc; i++) v4_dstack_push(&n.ds, c->in[i]);
    if (v4_test_call(&n, &rom_es, &rom_h, V4_ROM_BASE + (v4_cell)c->entry, 10000000L) <= 0) return 0;
    for (i = c->outc; i-- > 0; ) got[i] = v4_dstack_pop(&n.ds);
    *err = n.mem[NODE_ERROR];
    return v4_dstack_pop(&n.ds) == CANARY;
}

static void cases_on(const char *which, const v4_cell *rom, unsigned words)
{
    unsigned k;
    for (k = 0; k < CASES; k++) {
        const math_case *c = &cases[k];
        v4_cell got[2] = { 0, 0 }, err = -99;
        int ok = run(rom, words, c, got, &err);
        CHECK(ok && got[0] == c->out[0] && got[1] == c->out[1] && err == c->error,
              "%s ROM, entry %u, case %u: got %lld %lld error %lld, want %lld %lld error %lld",
              which, c->entry, k, (long long)got[0], (long long)got[1], (long long)err,
              (long long)c->out[0], (long long)c->out[1], (long long)c->error);
    }
}

/* The address of `name` in a ROM's symbol file, or -1. */
static long sym_addr(const char *path, const char *name)
{
    FILE *f = fopen(path, "r");
    char sym[80];
    long addr, found = -1;
    if (!f) { printf("    cannot read %s\n", path); return -1; }
    while (fscanf(f, "%79s %ld", sym, &addr) == 2) if (strcmp(sym, name) == 0) found = addr;
    fclose(f);
    return found;
}

/* Entry i of `rom` is a jump, in slot 0, to the routine `routine`. */
static int entry_jumps_to(const v4_cell *rom, unsigned i, const char *sym, const char *routine)
{
    v4_iword w = (v4_iword)((v4_ucell)rom[i] & 0xFFFFFFFFu);
    long target = sym_addr(sym, routine);
    return target >= 0 && v4_iword_op(w, 0) == V4_OP_JUMP
        && v4_iword_branch(V4_ROM_BASE + (v4_cell)i, w, 0) == (v4_cell)target;
}

int main(void)
{
    printf("v4 ROM math tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);

    /* ---- one entry list, in both ROMs ---- */
    {
#define CHECK_ENTRY(i, routine, forth, effect) \
        CHECK(entry_jumps_to(ROM_IMAGE, i, V4_ROM_SYM, routine), "host ROM entry %d jumps to %s", i, routine); \
        CHECK(entry_jumps_to(MESH_ROM, i, V4_MESH_ROM_SYM, routine), "mesh ROM entry %d jumps to %s", i, routine);
        V4_ROM_MATH_ENTRIES(CHECK_ENTRY)
#undef CHECK_ENTRY
    }
    CHECK(entry_jumps_to(ROM_IMAGE, V4_ROM_START, V4_ROM_SYM, "(ROM)"), "the host ROM starts at entry %u, (ROM)", (unsigned)V4_ROM_START);
    CHECK(entry_jumps_to(ROM_IMAGE, V4_ROM_MATH_COUNT, V4_ROM_SYM, "(KEY)"), "the assembler's entries follow the math's");

    /* ---- the mesh ROM is small and holds nothing of the assembler ---- */
    CHECK(MESH_ROM_WORDS < 256u, "the mesh ROM is small: %u words", MESH_ROM_WORDS);
    CHECK(sym_addr(V4_MESH_ROM_SYM, "(TOKEN)") < 0 && sym_addr(V4_MESH_ROM_SYM, "(KEY)") < 0 && sym_addr(V4_MESH_ROM_SYM, "(ROM)") < 0,
          "the mesh ROM has none of the assembler's routines");
    printf("  the host ROM: %u words; the mesh ROM: %u words\n", ROM_IMAGE_WORDS, MESH_ROM_WORDS);

    /* ---- each entry, on each ROM, gives what the system's words gave ---- */
    cases_on("host", ROM_IMAGE, ROM_IMAGE_WORDS);
    cases_on("mesh", MESH_ROM, MESH_ROM_WORDS);

    printf("  %d checks, %d failures\n", checks, failures);
    return failures != 0;
}
  • Step 2: Run it to verify it fails

Run: make -C v4 build/64-test_host_rom_math.c 2>&1 | tail -5 Expected: FAIL to build: v4/rom_entries.h: No such file or directory.

  • Step 3: The one entry list

Create v4/include/v4/rom_entries.h:

/* rom_entries.h -- the ROMs' shared entries (docs/v4.0.0/MACHINE.md 2.2).
 *
 * Both ROMs, the host node's and a mesh node's, begin with these entries, in
 * this order: entry i is at V4_ROM_BASE + i, a jump to the routine.  A
 * payload calls an entry, never an address inside the ROM.  This is the one
 * list: tools/mkmap.c makes of it the (MATH-ENTRIES) macro both ROMs' entry
 * tables begin with (capsules/v4/rom/main.f18, mesh.f18) and the constants
 * ROM-UM* ... the system's source calls them by (tests/host_map.h).
 *
 * X(INDEX, ROUTINE, FORTH, EFFECT): ROUTINE is the ROM's name for the code
 * (capsules/v4/rom/math.f18), FORTH the system's word that calls it, EFFECT
 * its stack effect.  An entry is added only at the end.
 */
#ifndef V4_ROM_ENTRIES_H
#define V4_ROM_ENTRIES_H

#define V4_ROM_MATH_ENTRIES(X) \
    X(0, "(UM*)",     "UM*",     "( u1 u2 -- ulo uhi )") \
    X(1, "(UM/MOD)",  "UM/MOD",  "( ulo uhi ud -- urem uquot )") \
    X(2, "(D+)",      "D+",      "( d1 d2 -- d3 )") \
    X(3, "(DNEGATE)", "DNEGATE", "( d -- -d )") \
    X(4, "(Q.*)",     "Q.*",     "( a b -- c )") \
    X(5, "(Q./)",     "Q./",     "( a b -- q )")

#define V4_ROM_MATH_COUNT 6u

/* The host ROM's own entries follow: the assembler's twelve routines, then
 * (ROM), where the host machine starts (capsules/v4/rom/main.f18). */
#define V4_ROM_START (V4_ROM_MATH_COUNT + 12u)

#endif
  • Step 4: The math, in ROM source

Create capsules/v4/rom/math.f18. The routine bodies are the system's, copied without change from capsules/v4/sys/core.f18 (UM*, D+, DNEGATE), numout.f18 (UM/MOD) and qmath.f18 (Q.TO-INT, Q.*, D2*C, (UQ/), Q./, (QERR11)), with the names the ROM gives them and calls between them renamed to match:

\ math.f18 -- the ROM's shared math (docs/v4.0.0/MACHINE.md 2.2).
\
\ Six routines a payload calls through the ROM's entry table, as a GA144
\ application calls its node ROM's multiply and divide steps, and their
\ helpers.  Both ROMs carry them: the host node's and a mesh node's.  They
\ were the system's (capsules/v4/sys: core.f18, numout.f18, qmath.f18) and
\ do exactly what they did there; the system's words of those names now call
\ these.  No names, no headers, no FORTH.
\
\ Constants from the map (tools/mkmap.c): N-1 NODE-ERROR MSB MAXHI 2N+15
\ N-18 N-17, and (Q/), Q./'s five scratch cells, fixed in the memory map
\ (MACHINE.md 2.1).
\
\ ERRORS.  Q./ by zero leaves the result D-11 gives and stores 11, "Division
\ by zero", into NODE-ERROR (D-18).

macro SWAP   over push push drop pop pop   endmacro
macro -      push inv pop + inv            endmacro      \ x y -- x-y
macro NIP    push drop pop                 endmacro
macro ROT    push SWAP pop SWAP            endmacro
macro OR     over inv and xor              endmacro

\ ( u1 u2 -- ulo uhi )   entry 0
: (UM*)
  over 1 and inv 1 +  over 2/ and      \ u1 u2 t0       t0 = u2 2/ if u1 odd
  over a!  N-1 push                    \ A: u2          R: loop count
  push over 2/ pop                     \ u1 u2 s t0     s = u1 2/
  L: +* unext                          \ u1 u2 s hi     A: lo
  push drop pop                        \ u1 u2 hi
  2* a -if L0  drop 1 + jump L1   L0: drop   L1:  push
  over -if L2  drop dup jump L3   L2: drop 0  L3:  push
  dup -if L4  drop over 1 and jump L5   L4: drop 0  L5:
  pop + pop +  push
  and 1 and  a 2* +  pop ;

\ ( ulo uhi ud -- urem uquot )   entry 1.  One step a bit, the divisor in
\ A.  Exact for uhi < ud.
: (UM/MOD)
  a!  N-1 FOR
    -if L0
      2* over -if L1  drop 1 + jump L2
      L1: drop  L2: push 2* pop
      jump SUB
    L0:
      2* over -if L3  drop 1 + jump L4
      L3: drop  L4: push 2* pop
      dup a xor -if L5
        drop -if NOSUB  jump SUB
      L5: drop dup inv a + inv -if L6
        drop jump NOSUB
      L6: push drop pop jump SETBIT
    SUB:    inv a + inv
    SETBIT: push 1 + pop
    NOSUB:
  NEXT
  over push push drop pop pop ;

\ ( d1 d2 -- d3 )   entry 2
: (D+)
  push  over push  push drop pop       \ al bl      R: bh ah
  over over xor -if L1
    drop + -if C1  jump C0
  L1: drop over -if L2
    drop + jump C1
  L2: drop +
  C0: pop pop + ;
  C1: pop pop + 1 + ;

\ ( d -- -d )   entry 3
: (DNEGATE)
  inv over if L1  drop push inv 1 + pop ;
  L1: drop 1 + ;

\ ( q -- n )   the system's Q.TO-INT, which Q.* needs
: (Q>INT)   push a! 0 pop  15 FOR +* UNEXT  drop drop a ;

\ ( a b -- c )   entry 4
: (Q.*)
  SWAP push  over over (UM*) drop
  push  push over pop
  -if L1  SWAP jump L2   L1: SWAP drop 0   L2:
  pop SWAP -  push
  push over pop  (UM*)  pop +
  ROT  pop dup push
  over -if L3  drop dup jump L4   L3: drop 0   L4:
  push (UM*) pop -  (D+)
  ROT pop (UM*)
  SWAP push 0 (D+) pop
  push over pop SWAP (Q>INT)  push (Q>INT) pop SWAP ;

: (QERR11)   NODE-ERROR b! 11 !b ;

: (D2*C) ( lo hi cin -- lo' hi' cout )
  a!  dup -if L1  drop 1 jump L2   L1: drop 0   L2:  push
  over -if L3  drop 1 jump L4   L3: drop 0   L4:
  over + +
  push 2* a + pop  pop ;

Then append (UQ/) and (Q./): copy qmath.f18's : (UQ/) through the end of : Q./ (the line Z0: drop jump (QERR11)) verbatim, with exactly these renames and nothing else changed:

  • : Q./ ( a b -- q ) becomes : (Q./) ( a b -- q ) \ entry 5;
  • every call D2*C becomes (D2*C);
  • every call DNEGATE becomes (DNEGATE);
  • the call (UQ/) in DIV: stays (UQ/), and : (UQ/) stays its name.

The set HDR-NFA … set HDR-LINK here header line before : Q./ is not copied: the ROM has no headers.

  • Step 5: The entry tables

Create capsules/v4/rom/mesh.f18:

\ mesh.f18 -- a mesh node's ROM: its entry table (docs/v4.0.0/MACHINE.md 2.2).
\
\ A mesh node's ROM is the shared math (math.f18) and nothing else.  It has
\ no start: a mesh node at reset blocks on its ports and executes what
\ arrives (docs/v4.0.0/MESH.md 4.2 and 5).  Its entries are the first of
\ the host ROM's, at the same numbers: (MATH-ENTRIES), which tools/mkmap.c
\ writes from v4/include/v4/rom_entries.h.

: (ENTRIES)
  (MATH-ENTRIES)

In capsules/v4/rom/main.f18, replace the header comment's last sentence and the table. The table becomes:

: (ENTRIES)
  (MATH-ENTRIES)                \ 0-5  the shared math (math.f18; v4/rom_entries.h)
  jump (KEY)                    \ 6   ( -- c )
  jump (TOKEN)                  \ 7   ( -- len )
  jump (NUMBER)                 \ 8   ( -- n flag )
  jump (FIND)                   \ 9   ( -- rec | 0 )
  jump (DEFINE)                 \ 10  ( value kind -- )
  jump (OPCODE)                 \ 11  ( -- op | -1 )
  jump (OP)                     \ 12  ( op -- )
  jump (LIT)                    \ 13  ( v -- )
  jump (BRANCH)                 \ 14  ( op target -- )
  jump (BRANCH>)                \ 15  ( op -- ref )
  jump (RESOLVE)                \ 16  ( ref target -- )
  jump (LABEL)                  \ 17  ( -- addr )
  jump (ROM)                    \ 18  the ROM itself: where the machine starts (V4_ROM_START)

and the header comment's "DATASHEET-F18.md lists each routine's stack effect." becomes "Entries 0-5 are the shared math, the same in a mesh node's ROM (mesh.f18, MACHINE.md 2.2). DATASHEET-F18.md lists each routine's stack effect."

  • Step 6: The ROM map gives the macro and the math's constants

In v4/tools/mkmap.c, add #include "../include/v4/rom_entries.h" after the host_map.h include, and in main's default branch, after the HEX-HIGH constant and before return 0;:

    /* the math's constants (capsules/v4/rom/math.f18) */
    constant("N-1", (long)(V4_CELL_BITS - 1));
    constant("NODE-ERROR", (long)NODE_ERROR);
    constant("(Q/)", (long)QBASE);
    constant("MAXHI", (long)(v4_cell)(V4_MSB - 1u));
    constant("2N+15", (long)(2 * V4_CELL_BITS + 15));
    constant("N-18", (long)(V4_CELL_BITS - 18));
    constant("N-17", (long)(V4_CELL_BITS - 17));
    /* the shared entries, the start of both ROMs' tables (v4/rom_entries.h) */
    printf("macro (MATH-ENTRIES)");
#define MATH_ENTRY_JUMP(i, routine, forth, effect) printf("  jump %s", routine);
    V4_ROM_MATH_ENTRIES(MATH_ENTRY_JUMP)
#undef MATH_ENTRY_JUMP
    printf("  endmacro\n");

Also update the file's header comment: "the console's receive registers and the ROM's state area (MACHINE.md 2.1)" becomes "the console's receive registers, the ROM's state area (MACHINE.md 2.1), the math's constants and its shared entries (MACHINE.md 2.2)".

  • Step 7: mkrom names the array it writes

In v4/tools/mkrom.c: the usage becomes mkrom NAME OUT.c OUT.sym MAP.f18 SOURCE.f18..., writing const v4_cell v4_<NAME>_<width>[] and v4_<NAME>_<width>_words. Concretely:

  • header comment line mkrom OUT.c OUT.sym MAP.f18 SOURCE.f18... → mkrom NAME OUT.c OUT.sym MAP.f18 SOURCE.f18..., and OUT.c the ROM: const v4_cell v4_rom_<width>[] and its length → OUT.c the ROM: const v4_cell v4_<NAME>_<width>[] and its length (NAME rom: the host node's ROM; mesh_rom: a mesh node's, MACHINE.md 2.2);

  • if (argc < 5) → if (argc < 6), and its usage text to match;

  • every argv[1] → argv[2], every argv[2] (the symbol file) → argv[3], and the loop for (i = 3; ...) → for (i = 4; ...);

  • the two fprintf lines that name the array: "const v4_cell v4_rom_%d[] = {\n", V4_CELL_BITS → "const v4_cell v4_%s_%d[] = {\n", argv[1], V4_CELL_BITS, and "};\nconst unsigned v4_rom_%d_words = %ldu;\n", V4_CELL_BITS, ... → "};\nconst unsigned v4_%s_%d_words = %ldu;\n", argv[1], V4_CELL_BITS, ...;

  • the final message "mkrom: a ROM of %ld words, ..." gains the name: "mkrom: %s, %ld words, %d-bit cells -> %s\n", argv[1], ... with argv[2] for the output file.

  • Step 8: The Makefile builds both ROMs

In v4/Makefile:

  • ROM_FILES gains math.f18 after dir.f18: $(wildcard $(addprefix $(ROM_DIR)/,main.f18 in.f18 num.f18 sym.f18 pack.f18 word.f18 dir.f18 math.f18)).
  • After it: MESH_ROM_FILES := $(wildcard $(addprefix $(ROM_DIR)/,mesh.f18 math.f18)) with the comment # A mesh node's ROM (MACHINE.md 2.2): its entry table, then the shared math.
  • rom_src = $(if $(call uses_rom,$(1)),$(BINDIR)/rom-$(2).c $(BINDIR)/mesh-rom-$(2).c)
  • rom_sym = $(if $(call uses_rom,$(1)),-DV4_ROM_SYM='"$(BINDIR)/rom-$(2).sym"' -DV4_MESH_ROM_SYM='"$(BINDIR)/mesh-rom-$(2).sym"')
  • In ROM_RULE, the mkmap and mkrom prerequisites gain $(HERE)/include/v4/rom_entries.h only through the existing $$(wildcard $(HERE)/include/v4/*.h) (already there; nothing to add). The host ROM's recipe becomes $(BINDIR)/mkrom-$(1) rom $(BINDIR)/rom-$(1).c ..., and a new rule follows it:
$(BINDIR)/mesh-rom-$(1).c $(BINDIR)/mesh-rom-$(1).sym: $(BINDIR)/mkrom-$(1) $(BINDIR)/rom-map-$(1).f18 $$(MESH_ROM_FILES)
	$(BINDIR)/mkrom-$(1) mesh_rom $(BINDIR)/mesh-rom-$(1).c $(BINDIR)/mesh-rom-$(1).sym $(BINDIR)/rom-map-$(1).f18 $$(MESH_ROM_FILES)
  • rom: becomes $(foreach w,$(WIDTHS),$(BINDIR)/rom-$(w).c $(BINDIR)/mesh-rom-$(w).c).

  • The ROM comment block gains: math.f18, the shared math, is in both: tools/mkrom.c builds mesh-rom-<w>.c from mesh.f18 and math.f18 (MACHINE.md 2.2).

  • Step 9: The start moves to entry 18, and the harness knows the mesh ROM

In v4/tests/rom_harness.h:

  • add #include "v4/rom_entries.h" after #include "v4/sys_files.h";
  • after the ROM_IMAGE block, add:
/* A mesh node's ROM (MACHINE.md 2.2): the shared math only. */
#if V4_CELL_BITS == 64
extern const v4_cell v4_mesh_rom_64[];
extern const unsigned v4_mesh_rom_64_words;
#define MESH_ROM v4_mesh_rom_64
#define MESH_ROM_WORDS v4_mesh_rom_64_words
#else
extern const v4_cell v4_mesh_rom_32[];
extern const unsigned v4_mesh_rom_32_words;
#define MESH_ROM v4_mesh_rom_32
#define MESH_ROM_WORDS v4_mesh_rom_32_words
#endif
  • rom_entries[] becomes:
#define ROM_MATH_NAME(i, routine, forth, effect) routine,
static const char *const rom_entries[] = {
    V4_ROM_MATH_ENTRIES(ROM_MATH_NAME)
    "(KEY)", "(TOKEN)", "(NUMBER)", "(FIND)", "(DEFINE)", "(OPCODE)",
    "(OP)", "(LIT)", "(BRANCH)", "(BRANCH>)", "(RESOLVE)", "(LABEL)", "(ROM)"
};
#undef ROM_MATH_NAME
  • in rom_build_system, n->p = V4_ROM_BASE + 12; /* entry 12, (ROM): ... */ → n->p = V4_ROM_BASE + (v4_cell)V4_ROM_START; /* (ROM): where the machine starts */.

In v4/tests/test_host_rom_asm.c:72, b.p = V4_ROM_BASE + 12; → b.p = V4_ROM_BASE + (v4_cell)V4_ROM_START; (it includes rom_harness.h, which now includes rom_entries.h).

In v4/system/boot.c: add #include "v4/rom_entries.h" with the other v4/ includes; delete #define ROM_START 12u /* the entry where the machine starts */; n->p = V4_ROM_BASE + (v4_cell)ROM_START; → n->p = V4_ROM_BASE + (v4_cell)V4_ROM_START;; and the comment "The machine starts at its entry 12, (ROM)" → "The machine starts at its entry V4_ROM_START (v4/rom_entries.h), (ROM)".

  • Step 10: Run the test to verify it passes

Run: make -C v4 build/64-test_host_rom_math.c build/32-test_host_rom_math.c && v4/build/64-test_host_rom_math.c && v4/build/32-test_host_rom_math.c Expected: both print the host ROM: N words; the mesh ROM: M words with M under 256, and ... checks, 0 failures. (If the Makefile's test binary names differ from build/<w>-test_host_rom_math.c, use the names make -C v4 test builds; rule and continue.)

  • Step 11: Run the suite

Run: make -C v4 test test-32 hosted-check > /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt 2>&1; echo EXIT $?; grep -E 'FAIL|failures|hosted-check' /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt | grep -v ' 0 failures' | head Expected: EXIT 0, no failures. The system still has its own math in RAM, so dict_hash is unchanged; the ROM's hash changes.

  • Step 12: Commit
$G add v4/include/v4/rom_entries.h capsules/v4/rom/math.f18 capsules/v4/rom/mesh.f18 capsules/v4/rom/main.f18 \
       v4/tools/mkmap.c v4/tools/mkrom.c v4/Makefile v4/tests/rom_harness.h v4/tests/test_host_rom_asm.c \
       v4/tests/test_host_rom_math.c v4/system/boot.c
$G commit -m "feat(v4): the shared math in both ROMs, and a mesh node's ROM

MACHINE.md 2.2.  UM*, UM/MOD, D+, DNEGATE, Q.* and Q./ are entries 0-5
of the host ROM and of a mesh node's ROM, from one source (rom/math.f18)
and one entry list (v4/rom_entries.h).  The host ROM starts at entry 18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 2: The system calls the ROM

Files:

  • Modify: v4/tests/host_map.h (ROM-entry constants; host_load attaches the host ROM)
  • Modify: v4/tools/mkmap.c (mkmap sys writes the ROM-entry constants)
  • Modify: v4/Makefile (every test_host_* links the ROM and defines V4_ROM_LINKED; a hosted-check line)
  • Modify: v4/tests/rom_harness.h (the ROM externs move to host_map.h)
  • Modify: capsules/v4/sys/core.f18 (UM*, D+, DNEGATE), numout.f18 (UM/MOD), qmath.f18 (Q.*, Q./; D2*C, (UQ/), (QERR11) removed)
  • Modify (generated): capsules/v4/sys/map-64.f18, map-32.f18 (make -C v4 sysmap)
  • Modify: v4/tests/test_host_unit.c (host_born attaches the host ROM)
  • Test: v4/tests/test_host_rom_math.c

Interfaces:

  • Consumes: V4_ROM_MATH_ENTRIES, MESH_ROM, ROM_IMAGE from Task 1.

  • Produces: constants ROM-UM*, ROM-UM/MOD, ROM-D+, ROM-DNEGATE, ROM-Q.*, ROM-Q./ (= V4_ROM_BASE + index) in the system's map and in host_load; host_load leaves the host ROM attached.

  • Step 1: Write the failing test

Append to v4/tests/test_host_rom_math.c, before int main:

/* THE SYSTEM CALLS THE ROM.  The system as text.c builds it (host_load, the
 * ROM attached): each of the six words is a literal of its entry's address,
 * then push, then ; -- it goes to the entry and the entry's ; returns to the
 * word's caller -- and it gives the table's results.  On a node with no ROM
 * the call faults rather than computing anything. */
static v4_text tx;
static int system_cases(void)
{
    static const char *const files[] = {
#define SYS_FILE_NAME(file) #file ".f18",
        V4_SYS_FILES(SYS_FILE_NAME)
#undef SYS_FILE_NAME
    };
    unsigned k;
    if (!host_load(&tx, &n, files, (unsigned)(sizeof files / sizeof files[0])) || !v4_text_finish(&tx)) {
        printf("    the system does not assemble: %s\n", v4_text_error(&tx));
        return 0;
    }
#define CHECK_CALLS(i, routine, forth, effect) { \
        v4_cell w = v4_text_word(&tx, forth); \
        v4_iword iw = w >= 0 ? (v4_iword)((v4_ucell)n.mem[w] & 0xFFFFFFFFu) : 0; \
        CHECK(w >= 0 && v4_iword_op(iw, 0) == V4_OP_FETCH_P && v4_iword_op(iw, 1) == V4_OP_PUSH && v4_iword_op(iw, 2) == V4_OP_SEMI \
              && n.mem[w + 1] == V4_ROM_BASE + (v4_cell)(i), "the system's %s is the ROM's entry %d, then push and ;", forth, i); }
    V4_ROM_MATH_ENTRIES(CHECK_CALLS)
#undef CHECK_CALLS
    for (k = 0; k < CASES; k++) {
        static const char *const forth_of[] = {
#define FORTH_NAME(i, routine, forth, effect) forth,
            V4_ROM_MATH_ENTRIES(FORTH_NAME)
#undef FORTH_NAME
        };
        const math_case *c = &cases[k];
        v4_cell got[2] = { 0, 0 };
        unsigned i;
        int ok;
        v4_dstack_reset(&n.ds); v4_rstack_reset(&n.rs); v4_exec_reset(&rom_es); v4_heat_reset(&rom_h);
        n.mem[NODE_ERROR] = 0;
        v4_dstack_push(&n.ds, CANARY);
        for (i = 0; i < c->argc; i++) v4_dstack_push(&n.ds, c->in[i]);
        ok = v4_test_call(&n, &rom_es, &rom_h, v4_text_word(&tx, forth_of[c->entry]), 10000000L) > 0;
        for (i = c->outc; i-- > 0; ) got[i] = v4_dstack_pop(&n.ds);
        CHECK(ok && v4_dstack_pop(&n.ds) == CANARY && got[0] == c->out[0] && got[1] == c->out[1] && n.mem[NODE_ERROR] == c->error,
              "the system's %s, case %u: got %lld %lld error %lld", forth_of[c->entry], k,
              (long long)got[0], (long long)got[1], (long long)n.mem[NODE_ERROR]);
    }
    /* the same system with its ROM taken away: UM* does not return */
    v4_node_rom_attach(&n, ROM_IMAGE, 0u);
    v4_dstack_reset(&n.ds); v4_rstack_reset(&n.rs); v4_exec_reset(&rom_es); v4_heat_reset(&rom_h);
    v4_dstack_push(&n.ds, 12345); v4_dstack_push(&n.ds, 67890);
    CHECK(v4_test_call(&n, &rom_es, &rom_h, v4_text_word(&tx, "UM*"), 100000L) <= 0 || n.fault_kind == V4_FAULT_ADDRESS,
          "with no ROM, the system's UM* faults at the ROM's address and computes nothing");
    return 1;
}

and in main, before the final printf: CHECK(system_cases(), "the system the tests build with text.c");

v4_node_rom_attach(&n, ROM_IMAGE, 0u) attaches a ROM of no words, which leaves the range empty (node.c, v4_node_rom_attach).

  • Step 2: Run it to verify it fails

Run: make -C v4 build/64-test_host_rom_math.c && v4/build/64-test_host_rom_math.c | grep -c FAIL Expected: FAIL. Six the system's ... is the ROM's entry failures (the words are still the RAM code), and with no ROM, ... UM* faults fails (it computes the product). If instead the build fails because host_load does not know ROM-UM*, that is not yet reached: the system text does not use it yet, so the build succeeds and the checks fail as described.

  • Step 3: The map gives the entries' constants, and host_load attaches the ROM

In v4/tests/host_map.h:

  • add #include "v4/rom_entries.h" with its other includes;
  • after HOST_CONSTANTS, add:
/* The shared ROM entries (MACHINE.md 2.2): ROM-UM* ... , each the address
 * of its entry.  The system's six words of those names call these. */
#define HOST_ROM_ENTRY_CONSTANTS(X) \
    X("ROM-UM*", V4_ROM_BASE + 0) X("ROM-UM/MOD", V4_ROM_BASE + 1) X("ROM-D+", V4_ROM_BASE + 2) \
    X("ROM-DNEGATE", V4_ROM_BASE + 3) X("ROM-Q.*", V4_ROM_BASE + 4) X("ROM-Q./", V4_ROM_BASE + 5)

and directly under it, a compile-time check that this list matches rom_entries.h:

#define HOST_ROM_ENTRY_INDEX_OK(i, routine, forth, effect) typedef char host_rom_entry_##i[(i) < 6 ? 1 : -1];
V4_ROM_MATH_ENTRIES(HOST_ROM_ENTRY_INDEX_OK)
#undef HOST_ROM_ENTRY_INDEX_OK
typedef char host_rom_entry_count[V4_ROM_MATH_COUNT == 6u ? 1 : -1];
  • move the #if V4_CELL_BITS == 64 extern const v4_cell v4_rom_64[] ... #define ROM_IMAGE_WORDS ... #endif block from rom_harness.h into host_map.h, unchanged, above host_load;
  • in host_load, after HOST_CONSTANTS(HOST_CONSTANT), add HOST_ROM_ENTRY_CONSTANTS(HOST_CONSTANT); and after v4_node_stack_regs_attach(n, DSTACK_REG, RSTACK_REG); add:
#ifdef V4_ROM_LINKED
    v4_node_rom_attach(n, ROM_IMAGE, ROM_IMAGE_WORDS);   /* the system calls its math in the ROM (MACHINE.md 2.2) */
#endif

In v4/tools/mkmap.c's system_map, after HOST_CONSTANTS(SYS_CONSTANT), add HOST_ROM_ENTRY_CONSTANTS(SYS_CONSTANT).

In v4/Makefile: uses_rom = $(findstring /test_host_,$(1)) (every host test), and rom_sym gains -DV4_ROM_LINKED: rom_sym = $(if $(call uses_rom,$(1)),-DV4_ROM_LINKED -DV4_ROM_SYM='"$(BINDIR)/rom-$(2).sym"' -DV4_MESH_ROM_SYM='"$(BINDIR)/mesh-rom-$(2).sym"'). Update the comment above uses_rom: "Every test named test_host_*.c links the ROMs of its width: the system calls its math in the ROM (MACHINE.md 2.2)".

rom_harness.h keeps #include "host_map.h" and now gets ROM_IMAGE from it.

  • Step 4: The system's six words call the ROM

In capsules/v4/sys/core.f18, keep each set HDR-NFA here ... header line exactly, and replace the definitions:

  • : UM* ( u1 u2 -- ulo uhi ) and its body (through and 1 and a 2* + pop ;) → : UM* ROM-UM* push ; \ ( u1 u2 -- ulo uhi ) the ROM's entry 0 (MACHINE.md 2.2)
  • : D+ ( d1 d2 -- d3 ) and its body (through C1: pop pop + 1 + ;) → : D+ ROM-D+ push ; \ ( d1 d2 -- d3 ) the ROM's entry 2
  • : DNEGATE ( d -- -d ) and its body (through L1: drop 1 + ;) → : DNEGATE ROM-DNEGATE push ; \ ( d -- -d ) the ROM's entry 3

In capsules/v4/sys/numout.f18: the comment \ One step a bit, the divisor in A. Exact for uhi < ud. and : UM/MOD ... through its over push push drop pop pop ; →

\ Exact for uhi < ud.  The ROM's entry 1 (MACHINE.md 2.2).
: UM/MOD  ROM-UM/MOD push ;      \ ( ulo uhi ud -- urem uquot )

In capsules/v4/sys/qmath.f18:

  • : Q.* ( a b -- c ) through push over pop SWAP Q.TO-INT push Q.TO-INT pop SWAP ; → : Q.* ROM-Q.* push ; \ ( a b -- c ) the ROM's entry 4 (MACHINE.md 2.2)
  • delete : D2*C ... and : (UQ/) ... whole (they are the ROM's now, capsules/v4/rom/math.f18);
  • : Q./ ( a b -- q ) through Z0: drop jump (QERR11) → : Q./ ROM-Q./ push ; \ ( a b -- q ) the ROM's entry 5;
  • delete : (QERR11) NODE-ERROR b! 11 !b ; (only Q./ used it; (QERR12) stays);
  • in the header comment, "Constants the loader supplies" gains the line \ ROM-Q.* ROM-Q./ the ROM's entries (MACHINE.md 2.2), and the (Q/) line becomes \ (Q/) (QE) (QR) (QL) (QT) (QP) word addresses of the words' scratch cells: 5, 8, 5, 7, 6 and 4 of them; (Q/) is the ROM's Q./'s.

If any other system word called D2*C, (UQ/) or (QERR11), the build fails with "never defined"; grep -n 'D2\*C\|(UQ/)\|(QERR11)' capsules/v4/sys/*.f18 must show none after the edit.

  • Step 5: Regenerate the system's map, and have the test host's nodes carry the ROM

Run: make -C v4 sysmap Expected: capsules/v4/sys/map-64.f18 and map-32.f18 gain six constant ROM-... lines (ROM-UM* is 16384 at both widths).

In v4/tests/test_host_unit.c's host_born, after v4_node_fault_attach(&pool[k].n, w_fault);, add:

    v4_node_rom_attach(&pool[k].n, ROM_IMAGE, ROM_IMAGE_WORDS);   /* the nucleus calls its math in the ROM (MACHINE.md 2.2) */
  • Step 6: hosted-check: an error inside the ROM returns to the prompt

In v4/Makefile's hosted-check recipe, after the noone lines, add the same shape for a division by zero in the ROM (use the variable and file-naming of the neighbouring lines, $(BINDIR)/qdiv-amd64.txt):

	@printf '65536 0 0 0 Q./\n3 .\n' | $(BINDIR)/starforth4-amd64 > $(BINDIR)/qdiv-amd64.txt 2>&1; true
	@grep -q '^ok> Division by zero$$' $(BINDIR)/qdiv-amd64.txt && grep -q '^ok> 3  ok$$' $(BINDIR)/qdiv-amd64.txt || { echo "hosted-check: a division by zero inside the ROM's Q./ was not an error the node came back from"; tail -4 $(BINDIR)/qdiv-amd64.txt; exit 1; }

If the error is printed in another form, take the form the noone line's error shows (^ok> <message>$) and rule.

  • Step 7: Run the test to verify it passes

Run: make -C v4 build/64-test_host_rom_math.c build/32-test_host_rom_math.c && v4/build/64-test_host_rom_math.c && v4/build/32-test_host_rom_math.c Expected: 0 failures at both widths.

  • Step 8: Run the suite

Run: make -C v4 test test-32 hosted-check > /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt 2>&1; echo EXIT $?; grep -E 'FAIL|failures|hosted-check|dict_hash' /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt | grep -v ' 0 failures' | head -20 Expected: EXIT 0; test_host_unit passes POST 538/538 on Hera; hosted-check reports the three ISAs boot identically. dict_hash differs from 0xe5a57f3eb72ff94a, the same on all three ISAs. Note the new value for the final report.

  • Step 9: Commit
$G add v4/tests/host_map.h v4/tools/mkmap.c v4/Makefile v4/tests/rom_harness.h v4/tests/test_host_unit.c \
       v4/tests/test_host_rom_math.c capsules/v4/sys/core.f18 capsules/v4/sys/numout.f18 capsules/v4/sys/qmath.f18 \
       capsules/v4/sys/map-64.f18 capsules/v4/sys/map-32.f18
$G commit -m "feat(v4): the system's UM*, UM/MOD, D+, DNEGATE, Q.* and Q./ call the ROM

MACHINE.md 2.2.  Each is its entry's address, push and ;.  The tests'
systems carry the host ROM, and a division by zero inside the ROM
returns to the prompt (hosted-check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 3: A node Hera births carries the mesh ROM

Files:

  • Modify: v4/tests/test_host_unit.c (host_born; checks after the unit is born)

Interfaces:

  • Consumes: MESH_ROM, MESH_ROM_WORDS, ROM_IMAGE (rom_harness.h, host_map.h).

  • Step 1: Write the failing test

In v4/tests/test_host_unit.c, after the check "all five are waiting at their ports" and its block, add:

    /* ---- the nodes she births carry a mesh node's ROM (MACHINE.md 2.2) ---- */
    CHECK(node_numbered(10)->rom == ROM_IMAGE && node_numbered(11)->rom == MESH_ROM && node_numbered(12)->rom == MESH_ROM
          && node_numbered(13)->rom == MESH_ROM && node_numbered(14)->rom == MESH_ROM,
          "Hera carries the host's ROM, and the four she birthed a mesh node's");
    CHECK(strcmp(tell(11, "12345 67890 UM* . ."), "0 838102050 ") == 0 && ended_from == 11, "11's UM* is its ROM's: \"%s\"", printed);
    CHECK(strcmp(tell(12, "100 0 7 UM/MOD . ."), "14 2 ") == 0 && ended_from == 12, "12's UM/MOD: \"%s\"", printed);
    CHECK(strcmp(tell(13, "65536 0 196608 0 Q./ . ."), "0 21845 ") == 0 && ended_from == 13, "13's Q./: \"%s\"", printed);
  • Step 2: Run it to verify it fails

Run: make -C v4 build/64-test_host_unit.c && v4/build/64-test_host_unit.c | grep FAIL Expected: FAIL on Hera carries the host's ROM, and the four she birthed a mesh node's only. The three arithmetic checks pass already, since every node carries the host ROM from Task 2, whose first six entries are the same.

  • Step 3: host_born attaches the mesh ROM to every node but Hera

In host_born, the line Task 2 added becomes:

    /* Hera, who is made first, is the host's node and carries its ROM; a
     * node she births carries a mesh node's (MACHINE.md 2.2).  The nucleus
     * calls its math at the same entries in either. */
    if (k == 0) v4_node_rom_attach(&pool[k].n, ROM_IMAGE, ROM_IMAGE_WORDS);
    else        v4_node_rom_attach(&pool[k].n, MESH_ROM, MESH_ROM_WORDS);

Update the file's header comment: after "Hera is born empty and is given it through the port her requests go to." add "She carries the host's ROM; a node she births carries a mesh node's, the shared math only (MACHINE.md 2.2), at the same entries."

  • Step 4: Run it to verify it passes

Run: make -C v4 build/64-test_host_unit.c && v4/build/64-test_host_unit.c | tail -3 Expected: 0 failures.

  • Step 5: Run the suite

Run: make -C v4 test test-32 hosted-check > /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt 2>&1; echo EXIT $?; grep -E 'FAIL|failures' /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/suite.txt | grep -v ' 0 failures' | head Expected: EXIT 0, no failures.

  • Step 6: Commit
$G add v4/tests/test_host_unit.c
$G commit -m "feat(v4): a node Hera births carries a mesh node's ROM

MACHINE.md 2.2.  Hera carries the host's ROM; the four outer nodes the
shared math only, and compute UM*, UM/MOD and Q./ with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 4: The documents say what is built

Files:

  • Modify: docs/v4.0.0/DATASHEET-F18.md (§5's ROM paragraph; a new entry list; §13 item 3)

  • Modify: docs/v4.0.0/MACHINE.md (top status; §2.2 status and the push ; ruling)

  • Step 1: The datasheet's entry list

In docs/v4.0.0/DATASHEET-F18.md, the ROM paragraph (it begins "ROM. Built (2026-10-15; MACHINE.md §2): 2049 words") becomes accurate: the host ROM's word count from Task 2's PARITY:V4_ROM words= line or make -C v4 rom's mkrom: line, "The machine starts at its entry 18", and that a mesh node's ROM of N words holds entries 0–5 only (N from mkrom: mesh_rom, N words). Its sentence "This sheet will list each entry's stack effect and the registers it uses once the ROM is built." is replaced by a table, placed directly after that paragraph:

| Entry | Routine | Stack | Uses | In |
|---|---|---|---|---|
| 0 | `UM*` | ( u1 u2 -- ulo uhi ) | A | both ROMs |
| 1 | `UM/MOD` | ( ulo uhi ud -- urem uquot ), exact for uhi < ud | A | both ROMs |
| 2 | `D+` | ( d1 d2 -- d3 ) | — | both ROMs |
| 3 | `DNEGATE` | ( d -- -d ) | — | both ROMs |
| 4 | `Q.*` | ( a b -- c ), Q48.16 | A | both ROMs |
| 5 | `Q./` | ( a b -- q ), Q48.16; by zero: D-11's result and error 11 | A, B; `(Q/)`'s 5 cells | both ROMs |
| 6–17 | the assembler's routines (`capsules/v4/rom/main.f18`) | | | host ROM |
| 18 | `(ROM)`: where the host machine starts | | | host ROM |

Before writing the "Uses" column, check each routine's source in capsules/v4/rom/math.f18 for a!, a, b!, @b, !b (including the helpers it calls) and correct the column to what the code uses.

  • Step 2: §13 item 3

Replace item 3's text with: "The ROM is built and the host node boots from it (2026-10-15). A mesh node's ROM is built too (MACHINE.md §2.2): entries 0–5, the shared math, and nothing else. In the test host a node Hera births carries it. The products have no births yet, so they do not link it. A node that is born is still sent the host's assembled system through its port."

  • Step 3: MACHINE.md

  • The status line at the top: replace "The mesh ROM (section 2.2) is designed and not built." with "The mesh ROM (section 2.2) is built."

  • §2.2's **Status: designed, for review. Not built.** → **Status: built** (the commits of plan docs/v4.0.0/plans/2026-10-15-mesh-rom.md).

  • In §2.2's "The system calls the ROM" paragraph, "Each one's code becomes a single jump to its ROM entry." → "Each one's code is its entry's address, push and ;: the address goes onto the return stack and ; goes there, which is a jump. A branch to a constant is not in the assembler's language (v4/src/text.c), and the ROM's assembler must accept the same language."

  • Step 4: Check and commit

Run: grep -n 'entry 12\|single jump\|Not built\|not built yet' docs/v4.0.0/MACHINE.md docs/v4.0.0/DATASHEET-F18.md Expected: no line about the ROM's start at entry 12, the jump, or the mesh ROM being unbuilt. (Other "not built" lines about other work stay.)

$G add docs/v4.0.0/DATASHEET-F18.md docs/v4.0.0/MACHINE.md
$G commit -m "docs(v4): the ROMs' shared entries, and the mesh ROM as built

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 5: Acceptance

Files:

  • Create: logs/<timestamp>/{amd64,aarch64,riscv64}/... (by the boots)

  • Step 1: Clean tree for the boots

Run: $G status --short disk Expected: nothing.

  • Step 2: Boot each ISA, one at a time, in the foreground

Run, in this order, each to completion before the next, with the session's helper (bash /tmp/claude-1000/-home-rajames-CLionProjects-LithosAnanake/7f40fa84-d337-4c0c-bdd7-61dc32496f02/scratchpad/accept.sh <arch> "65536 0 196608 0 Q./ . ."), which runs make -f kernel/Makefile ARCH=<arch> clean qemu and types the line at ok>:

  • amd64
  • aarch64
  • riscv64

Expected for each: PARITY:V4_ROM words=<host ROM words> rom_hash=<same on all three>, PARITY:V4_POST tests=538 pass=538 fail=0, POST: PASSED, ok>, the typed line answering 0 21845 ok, and PARITY:V4_SYSTEM ... dict_hash= equal on all three (and equal to Task 2's value).

  • Step 3: The 32-bit build

Run: make -C v4 test-32 2>&1 | grep -E 'failures' | grep -v ' 0 failures'; echo done Expected: only done.

  • Step 4: Commit the logs
$G add logs/<amd64 dir> logs/<aarch64 dir> logs/<riscv64 dir>
$G commit -m "logs: the three acceptance boots of the mesh ROM

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
PATH="$PATH:/usr/share/gitkraken/resources/app.asar.unpacked/git/libexec/git-core" $G push -q origin StarForth-v4.0.0

Push after each task's commit too (Tasks 1–4), with the same push command.