feat(v4): the machine boots from its ROM; the prebuilt nucleus is gone

The host node starts in its ROM (entry 12), and the boot feeds it the
system's source through the console: capsules/v4/sys/map-64.f18, the
fourteen .f18 blobs and end.f18, each found, hash-checked and
signature-checked as any capsule, then EOT.  The ROM assembles the system
and starts it at (IDLE); the host reads the image record
(v4_image_from_node) for what only the assembled system knows.  A ROM
error is reported with its blob, line and meaning, then PARITY:FAIL.

Deleted: v4/tools/mkimage.c, capsules/v4/nucleus-64.f18, v4/capsule/*.v4,
v4/tools/v4tof18.py.  The .f18 files are the source now.

- text.c speaks the rest of the ROM's language (set, here, store, start;
  v4_text_value, v4_text_start), so the oracle reads the same source:
  test_host_rom_nucleus compares the ROM against text.c on the .f18 files,
  word for word, image record included.  Host tests load the .f18 files.
- mkmap image writes v4_map_image (the map's description of the system);
  hosted and the kernel link the ROM and the map instead of v4_image_64.c.
- Hera (Captain Bob's ruling, option 1): births stream the system the ROM
  assembled, as assembled code; hera.4th is unchanged.
  test_host_unit's host builds the system with the ROM.
- New parity lines: PARITY:V4_F18 per blob, PARITY:V4_ROM words=2049
  rom_hash=0xa6c792b5821010b7 instructions=60725216.

Acceptance: amd64, aarch64, riscv64 boot to ok> with PARITY:V4_POST
tests=538 pass=538 fail=0, POST: PASSED, and PARITY:V4_SYSTEM
word_count=317 dict_hash=0xe5a57f3eb72ff94a on all three -- the same hash
as the prebuilt nucleus gave.  make -C v4 test test-32 hosted-check: all
pass.  Logs: logs/20261015-141648, -141938, -142350.

Startup cost under QEMU TCG, the ROM's assembly (last V4_F18 line to
V4_ROM): amd64 26.7 s of 132 s to ok>; aarch64 41.0 s of 195 s; riscv64
20.5 s of 101 s.  Hosted native: +0.85 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-15 14:26:02 -04:00
co-authored by Claude Opus 5.5
parent 1bf19d6224
commit a73580e949
69 changed files with 1164 additions and 4025 deletions
+167 -72
View File
@@ -1,4 +1,5 @@
/* boot.c -- the nucleus, then its capsules, then the prompt. See boot.h.
/* boot.c -- the machine's ROM assembles the system, then its capsules, then
* the prompt. See boot.h.
*
* Nothing here uses the C library: the bare-metal kernel links this file.
* It is not part of the engine (v4/src): it needs the capsule directory,
@@ -311,25 +312,19 @@ static void post_say(void *self, const char *text, unsigned len)
/* ---- one capsule --------------------------------------------------------- */
static void say_where(const v4_boot *b, const char *name, uint32_t block, uint32_t line)
{
say(b, "\nV4: capsule "); say(b, name);
say(b, " block "); say_dec(b, block);
say(b, " line "); say_dec(b, line);
}
static int load_capsule(const v4_boot *b, const char *name)
/* The capsule `name`: found, its hash checked, its signature verified -- one
* that does not verify is refused, a missing one is only reported. Its
* payload, or NULL with what was wrong said. */
static const uint8_t *open_capsule(const v4_boot *b, const char *name, const CapsuleDesc **found)
{
const CapsuleDirHeader *dir = capsule_get_directory();
const CapsuleDesc *descs = capsule_get_descriptors();
const CapsuleNameEntry *names = capsule_get_names();
const uint8_t *arena = capsule_get_arena();
const CapsuleDesc *cap;
const uint8_t *p, *end;
const uint8_t *p;
CapsuleValidateResult vr;
CapsuleSigResult sr;
uint32_t block = 0, line = 0;
int in_block = 0;
cap = capsule_find_by_name(dir, descs, names, name);
if (!cap) { say(b, "V4: capsule "); say(b, name); say(b, " is not in this binary\n"); return 0; }
@@ -348,6 +343,27 @@ static int load_capsule(const v4_boot *b, const char *name)
p = capsule_get_payload(cap, arena);
if (!p) { say(b, "V4: capsule "); say(b, name); say(b, " has no payload\n"); return 0; }
*found = cap;
return p;
}
static void say_where(const v4_boot *b, const char *name, uint32_t block, uint32_t line)
{
say(b, "\nV4: capsule "); say(b, name);
say(b, " block "); say_dec(b, block);
say(b, " line "); say_dec(b, line);
}
static int load_capsule(const v4_boot *b, const char *name)
{
const CapsuleDesc *cap;
const uint8_t *p, *end;
uint32_t block = 0, line = 0;
int in_block = 0;
p = open_capsule(b, name, &cap);
if (!p) return 0;
end = p + cap->length;
while (p < end) {
@@ -391,78 +407,157 @@ static int load_capsule(const v4_boot *b, const char *name)
return 1;
}
/* ---- the nucleus ----------------------------------------------------------- */
/* ---- the system: the machine's ROM assembles it ----------------------------- */
/* The nucleus is a capsule of F18 code (v4/include/v4/capsule.h): the words
* themselves, to be written to the port of a node that has nothing in it.
* The boot is that node's neighbour. It finds the capsule, checks its hash
* and its signature as it does any capsule's, and gives the node a word
* each time it reads its port, until the node has taken them all and is
* where the nucleus waits to be handed a line. */
#define NUCLEUS_NAME_(bits) "v4:nucleus-" #bits ".f18"
#define NUCLEUS_NAME__(bits) NUCLEUS_NAME_(bits)
#define NUCLEUS_NAME NUCLEUS_NAME__(V4_CELL_BITS)
#define CELL_BYTES (V4_CELL_BITS / 8)
/* The ROM (docs/v4.0.0/MACHINE.md section 2): the machine's one binary,
* built by v4/tools/mkrom.c for this cell width. The machine starts at its
* entry 12, (ROM) (capsules/v4/rom/main.f18), which reads the system's
* source from the console, assembles it into RAM and, at the source's
* `start`, runs it. */
#if V4_CELL_BITS == 64
extern const v4_cell v4_rom_64[];
extern const unsigned v4_rom_64_words;
#define BOOT_ROM v4_rom_64
#define BOOT_ROM_WORDS v4_rom_64_words
#else
extern const v4_cell v4_rom_32[];
extern const unsigned v4_rom_32_words;
#define BOOT_ROM v4_rom_32
#define BOOT_ROM_WORDS v4_rom_32_words
#endif
#define ROM_START 12u /* the entry where the machine starts */
#define ROM_STEP_LIMIT 400000000ULL /* instruction words the ROM may take: it takes about 16 million */
#define EOT 4 /* the end of the source: the ROM's (KEY) sees no more */
static int load_nucleus(const v4_boot *b)
/* The system's source, in the order the ROM reads it: the memory map, then
* the system, then its end -- the state it is switched on in, and `start`.
* capsules/v4/sys; each blob is a capsule like any other. */
#define MAP_NAME_(bits) "v4:sys:map-" #bits ".f18"
#define MAP_NAME__(bits) MAP_NAME_(bits)
static const char *const system_blobs[] = {
MAP_NAME__(V4_CELL_BITS),
"v4:sys:core.f18", "v4:sys:input.f18", "v4:sys:dict.f18", "v4:sys:codegen.f18", "v4:sys:compile.f18",
"v4:sys:quit.f18", "v4:sys:forth.f18", "v4:sys:numout.f18", "v4:sys:words.f18", "v4:sys:system.f18",
"v4:sys:qmath.f18", "v4:sys:blocks.f18", "v4:sys:log.f18", "v4:sys:acl.f18",
"v4:sys:end.f18",
};
#define SYSTEM_BLOBS (sizeof system_blobs / sizeof system_blobs[0])
/* What the ROM's error codes mean (v4/tests/host_map.h, ROM_ERR_*): the ROM
* stops with the code, and the loader, which owns the console, says it. */
static const char *const rom_errors[] = {
"", "name too long", "comment not closed", "cannot be used as a name", "already defined",
"too many names", "does not fit: memory is full, or a branch cannot reach", "not an opcode the packer takes there",
"not allowed in a macro", "macro without endmacro, or endmacro without macro",
"label already defined in this word", "too many labels in one word",
"FOR nested too deep, NEXT or UNEXT without FOR, or FOR without NEXT",
"FOR ... UNEXT must fit one instruction word and hold no literal", "not something to branch to",
"never defined", "number does not fit a cell", "& needs a word that is already defined",
"write the number, not @p", "a name, a target, a value or a string is missing", "macros nested too deep",
"too many unresolved branches", "not a value", "the source ended with no start", "start of something not a word",
};
#define ROM_ERRORS (sizeof rom_errors / sizeof rom_errors[0])
/* FNV-1a over the ROM, a cell at a time, low byte first, as the dictionary
* hash is (image.c). */
static uint64_t rom_hash(void)
{
const CapsuleDirHeader *dir = capsule_get_directory();
const CapsuleDesc *descs = capsule_get_descriptors();
const CapsuleNameEntry *names = capsule_get_names();
const uint8_t *arena = capsule_get_arena();
const CapsuleDesc *cap;
const uint8_t *p;
CapsuleValidateResult vr;
CapsuleSigResult sr;
v4_node *n = b->n;
uint64_t words, at = 0, steps = 0;
uint64_t h = 0xcbf29ce484222325ULL;
unsigned w, i;
for (w = 0; w < BOOT_ROM_WORDS; w++)
for (i = 0; i < V4_CELL_BITS / 8; i++) {
h ^= (uint64_t)(((v4_ucell)BOOT_ROM[w] >> (8 * i)) & 0xFFu);
h *= 0x00000100000001b3ULL;
}
return h;
}
cap = capsule_find_by_name(dir, descs, names, NUCLEUS_NAME);
if (!cap) { say(b, "V4: capsule " NUCLEUS_NAME " is not in this binary\n"); return 0; }
vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) {
say(b, "V4: capsule " NUCLEUS_NAME ": "); say(b, capsule_validate_result_str(vr)); say(b, "\n");
/* The machine starts in its ROM, and the boot is its console: it feeds the
* system's source to the console a blob at a time, each found and checked
* as any capsule is, with a new line after each, and then EOT, until the
* ROM has assembled the system and started it. Then the node is where the
* system waits for a message, and its host reads from the image record
* what only the system knows (v4_image_from_node). If the ROM stops on an
* error, the blob and line are said, and what the error is. */
static int load_system(const v4_boot *b)
{
static const uint8_t *payload[SYSTEM_BLOBS];
static uint32_t length[SYSTEM_BLOBS], first_line[SYSTEM_BLOBS];
static const char newline = '\n', eot = EOT;
v4_node *n = b->n;
uint64_t steps = 0;
uint32_t lines = 0, at = 0;
unsigned i, blob = 0;
int fed_all = 0;
for (i = 0; i < SYSTEM_BLOBS; i++) {
const CapsuleDesc *cap;
uint32_t k;
payload[i] = open_capsule(b, system_blobs[i], &cap);
if (!payload[i]) return 0;
length[i] = (uint32_t)cap->length;
first_line[i] = lines;
for (k = 0; k < length[i]; k++) if (payload[i][k] == '\n') lines++;
lines++; /* the new line after it */
say(b, "PARITY:V4_F18 name="); say(b, system_blobs[i]);
say(b, " capsule_hash="); say_hex(b, cap->content_hash);
say(b, "\n");
}
v4_node_rom_attach(n, BOOT_ROM, BOOT_ROM_WORDS);
n->p = V4_ROM_BASE + (v4_cell)ROM_START;
while (n->p >= V4_ROM_BASE) {
/* the console is given more of the source whenever it has taken
* all it had */
if (!fed_all && n->input_pos == n->input_len) {
if (blob < SYSTEM_BLOBS && at < length[blob]) {
at += v4_node_console_feed(n, payload[blob] + at, length[blob] - at);
} else if (blob < SYSTEM_BLOBS) {
if (v4_node_console_feed(n, &newline, 1)) { blob++; at = 0; }
} else if (v4_node_console_feed(n, &eot, 1)) {
fed_all = 1;
}
}
(void)v4_exec_step_word(n, b->es, b->h);
if (n->stopped || ++steps > ROM_STEP_LIMIT) {
say(b, "V4: the machine's ROM did not assemble the system\n");
return 0;
}
if ((steps & 1023u) == 0 && n->mem[b->im->rom_failed] != 0) {
v4_cell code = n->mem[b->im->rom_failed];
uint32_t line = (uint32_t)n->mem[b->im->rom_errline];
for (i = SYSTEM_BLOBS; i-- > 1 && first_line[i] > line; ) { }
say(b, "V4: "); say(b, system_blobs[i]); say(b, " line "); say_dec(b, line - first_line[i] + 1u); say(b, ": ");
say(b, code > 0 && (unsigned)code < ROM_ERRORS ? rom_errors[code] : "an error the ROM has no words for");
say(b, "\n");
return 0;
}
}
say(b, "PARITY:V4_ROM words="); say_dec(b, BOOT_ROM_WORDS);
say(b, " rom_hash="); say_hex(b, rom_hash());
say(b, " instructions="); say_dec(b, (uint32_t)b->es->anticlock);
say(b, "\n");
if (!v4_image_from_node(n, b->im) || n->p != b->im->idle) {
say(b, "V4: the system the ROM assembled has no image record, or did not start where it says it waits\n");
return 0;
}
sr = capsule_verify_signature(descs, names, capsule_get_signatures(), arena, dir->desc_count, (int)(cap - descs));
if (sr != CAPSULE_SIG_OK) {
say(b, "V4: capsule " NUCLEUS_NAME " signature: "); say(b, capsule_sig_result_str(sr)); say(b, "\n");
if (sr == CAPSULE_SIG_INVALID) return 0;
}
p = capsule_get_payload(cap, arena);
if (!p || cap->length % CELL_BYTES != 0) { say(b, "V4: capsule " NUCLEUS_NAME " is not whole words\n"); return 0; }
words = cap->length / CELL_BYTES;
v4_node_fault_attach(n, b->im->fault_table);
/* until it has taken every word and is where the nucleus waits for a
* message: asleep, no message having come */
/* until it is where the system waits for a message: asleep, no message
* having come */
for (;;) {
if (!boot_step(b)) {
if (at >= words && n->do_op == 8) break;
say(b, "V4: the node did not take the nucleus in\n");
if (n->do_op == 8) break;
say(b, "V4: the system did not come to where it waits\n");
return 0;
}
if (n->stopped || n->asking || ++steps > STEP_LIMIT) {
say(b, "V4: the node did not take the nucleus in\n");
say(b, "V4: the system did not come to where it waits\n");
return 0;
}
if (n->reading && !n->given) {
v4_ucell word = 0;
unsigned i;
if (at >= words) {
say(b, "V4: the node wants more than the nucleus holds\n");
return 0;
}
for (i = 0; i < CELL_BYTES; i++) word |= (v4_ucell)p[at * CELL_BYTES + i] << (8 * i);
v4_node_port_give(n, 0, (v4_cell)word);
at++;
}
}
say(b, "PARITY:V4_NUCLEUS name=" NUCLEUS_NAME);
say(b, " capsule_id="); say_hex(b, cap->capsule_id);
say(b, " capsule_hash="); say_hex(b, cap->content_hash);
say(b, " words="); say_dec(b, word_count(n, b->im));
say(b, "\n");
return 1;
}
@@ -472,15 +567,15 @@ int v4_boot_run(const v4_boot *b)
{
unsigned i;
/* a node is born with nothing in it, and is sent the nucleus */
/* a node is born with nothing in it, and its ROM assembles the system */
if (!v4_image_born(b->n, b->es, b->h, b->im)) {
say(b, "V4: the nucleus is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
say(b, "V4: the memory map is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
/* its console's wire, with nothing on it */
v4_wire_reset(TO_NODE); v4_wire_reset(FROM_NODE);
v4_blocks_init(&boot_blocks, b->im->block_window);
if (!load_nucleus(b)) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
if (!load_system(b)) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
/* the kernel's words, before any capsule that might use one */
for (i = 0; i < b->word_count; i++) {