Files
LithosAnanake/v4/system/boot.c
T
rajamesandClaude Opus 5.5 18adf59091 fix(v4.0.0): POST leaves nothing, as v3 really does; and what the review of step 6b found
Captain Bob was told that v3 leaves the words POST's cases define in the
dictionary, and ruled that v4 should.  That was false: v3's run_test_suite
puts the dictionary back after each word's cases (test_common.c:333,
:365).  Shown that, he ruled that POST leaves nothing.  The boot now
seals the system, runs POST, and has the node do COLD, whose printing is
not shown; PARITY:V4_SYSTEM is the system as sealed.

A case the node does not come back from ends POST there, named, with how
many were not run: it would have stalled the boot for hours, where the
capsule had ended it.  The runner's test of it now uses a word that
really never ends.

hosted-check also requires that RS1 and T{ are unknown after boot, and
boots a program whose POST has failing cases (tests/post_cases_fail.c):
PARITY:FAIL, POST: FAILED, no prompt, none of a case's printing shown.

Comments and documents that still named the POST capsule or its two
hooks are brought up to date; NUCLEUS.md 6.3 says what v3 does, with the
lines, and how the wrong ruling came about.

make -C v4 test, sanitize and hosted-check pass; amd64, aarch64 and
riscv64 boot, POST 538 of 538, word_count=314, dict_hash
0x220ab283a504a3b3 on all six: logs/20261007-112638, -112901, -113220.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:40:20 -04:00

412 lines
16 KiB
C

/* boot.c -- the nucleus, 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,
* which only a whole system has.
*/
#include "v4/post.h"
#include "v4/blocks.h"
#include "v4/boot.h"
#include "v4/message.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_generated.h"
#include "starkernel/capsule_sig.h"
#include "starkernel/capsule_blocks.h"
/* The capsules, in the order they are loaded. */
static const char *const boot_capsules[] = {
"v4:forth79.4th",
};
/* POST is part of the boot, and is the kernel's (v4/include/v4/post.h;
* docs/v4.0.0/NUCLEUS.md 6.3): when the capsules are in and the system is
* sealed, the kernel feeds its cases to the node and judges them, puts the
* node back to the system as sealed, and the boot passes only if none
* failed. -DV4_POST_AT_BOOT=0 leaves it out, for work on a vocabulary
* that does not pass yet. */
#ifndef V4_POST_AT_BOOT
#define V4_POST_AT_BOOT 1
#endif
#define LINE_MAX 1025u /* a line is at most 1024 characters, a block */
#define STEP_LIMIT 4000000000ULL /* instruction words one line may take */
/* ---- printing ------------------------------------------------------------ */
static void say(const v4_boot *b, const char *s)
{
unsigned len = 0;
while (s[len]) len++;
b->out(s, len);
}
static void say_dec(const v4_boot *b, uint32_t v)
{
char buf[10];
unsigned i = sizeof buf;
do { buf[--i] = (char)('0' + v % 10u); v /= 10u; } while (v);
b->out(buf + i, (unsigned)sizeof buf - i);
}
static void say_hex(const v4_boot *b, uint64_t v)
{
char buf[18];
unsigned i;
buf[0] = '0'; buf[1] = 'x';
for (i = 0; i < 16; i++) buf[2 + i] = "0123456789abcdef"[(v >> (60 - 4 * i)) & 15u];
b->out(buf, sizeof buf);
}
/* ---- hashing: FNV-1a, 64 bits, as kernel/src/vm/parity.c ----------------- */
uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im)
{
return v4_image_dict_hash(n, im);
}
/* how many words FORTH holds: the list from LATEST, each entry's link in the
* cell before its code */
static uint32_t word_count(const v4_node *n, const v4_image *im)
{
v4_cell xt = n->mem[im->latest];
uint32_t count = 0;
while (xt > 0 && xt < (v4_cell)V4_NODE_WORDS && count < (uint32_t)V4_NODE_WORDS) {
count++;
xt = n->mem[xt - 1];
}
return count;
}
/* ---- handing the node a line ---------------------------------------------- */
/* The boot is the node's neighbour on two of its ports. On port 0 it is
* the kernel: the node writes the number of a request there (ENGINE.md
* 3.3) -- for a block (blocks.h), or for one of the host's words. On port
* 1 it is the console: it writes the node a message of text and reads back
* messages of what the node printed and of how the text ended (message.h). */
#define KERNEL_PORT 0u
#define CONSOLE_PORT 1u
#define CONSOLE_ID 1 /* the console's number as a sender; the node's is 0 until it is given one */
/* What the kernel keeps of the node's block window (blocks.h). */
static v4_blocks boot_blocks;
/* Hand the node a line and run it to its end. What it prints goes to the
* console; or, if `keep` is given, into `keep`, at most `cap` characters,
* and none of it is shown: *printed is then how many it printed in all. */
static int run_line(const v4_boot *b, const char *text, unsigned len, char *keep, unsigned cap, unsigned *printed)
{
static v4_message out, in; /* one line at a time: the boot is not re-entered */
v4_node *n = b->n;
uint64_t steps = 0;
unsigned sent = 0, i;
if (!v4_message_text(&out, 0, CONSOLE_ID, V4_MSG_TEXT, text, len)) return V4_BOOT_LINE_TOO_LONG;
in.count = 0;
for (;;) {
(void)v4_exec_step_word(n, b->es, b->h);
if (n->stopped) return V4_BOOT_LINE_STOPPED;
if (n->asking && n->ask_port == KERNEL_PORT) { /* a request: the kernel's turn */
if (v4_blocks_serve(&boot_blocks, n, n->request)) { /* a block, as for any node */
v4_node_port_served(n);
} else if (b->serve && n->request >= 1 && n->request <= (v4_cell)b->word_count) {
b->serve(n, (unsigned)n->request);
v4_node_port_served(n);
} else {
/* a request no one serves is an error, as any other (D-18):
* 12, Argument out of range */
v4_node_port_served(n);
v4_node_store(n, n->error_reg, 12); /* a store to NODE-ERROR raises it */
}
continue;
}
if (n->asking && n->ask_port == CONSOLE_PORT) { /* a word of a message from the node */
v4_cell word = n->request;
v4_node_port_served(n);
if (v4_message_word(&in, word)) {
unsigned chars = v4_message_length(&in);
if (v4_message_type(&in) == V4_MSG_OUTPUT) {
for (i = 0; i < chars; i++) {
char c = v4_message_char(&in, i);
if (!keep) b->out(&c, 1);
else { if (*printed < cap) keep[*printed] = c; (*printed)++; }
}
} else if (v4_message_type(&in) == V4_MSG_DONE) {
return (int)in.word[V4_MSG_HEADER];
}
in.count = 0;
}
continue;
}
if (n->asking) return V4_BOOT_LINE_STOPPED; /* a port with nothing on it */
if (n->reading && !n->given) {
if (sent < out.count && (n->read_port == CONSOLE_PORT || n->read_port == V4_PORT_ANY)) {
v4_node_port_give(n, CONSOLE_PORT, out.word[sent++]); /* a word of the message to the node */
continue;
}
return V4_BOOT_LINE_STOPPED; /* waiting for what will not come */
}
if (v4_image_waiting(n, b->im)) { /* the text is reading the keyboard */
int c = b->key ? b->key() : V4_BOOT_KEY_END;
unsigned char ch = (unsigned char)c;
if (c == V4_BOOT_KEY_END) return V4_BOOT_LINE_NO_INPUT;
if (c >= 0) (void)v4_node_console_feed(n, &ch, 1u);
continue;
}
if (++steps > STEP_LIMIT) return V4_BOOT_LINE_STOPPED;
}
}
int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
{
return run_line(b, text, len, 0, 0u, 0);
}
#if V4_POST_AT_BOOT
/* POST's host (post.h): a line is handed to the node as any line is, and
* what the node prints is kept for the runner and not shown. */
static int post_line(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len)
{
int how;
*len = 0;
how = run_line((const v4_boot *)self, text, text_len, out, cap, len);
return (how == V4_TEXT_QUIT || how == V4_TEXT_COMPLETED || how == V4_TEXT_ERROR) ? how : -1;
}
static void post_say(void *self, const char *text, unsigned len)
{
((const v4_boot *)self)->out(text, len);
}
#endif
/* ---- 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)
{
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;
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; }
vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) {
say(b, "V4: capsule "); say(b, name); say(b, ": "); say(b, capsule_validate_result_str(vr)); say(b, "\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 "); say(b, name); say(b, " 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) { say(b, "V4: capsule "); say(b, name); say(b, " has no payload\n"); return 0; }
end = p + cap->length;
while (p < end) {
const uint8_t *after, *nl;
uint32_t num;
unsigned len, i;
char text[LINE_MAX];
if (capsule_block_header(p, end, &num, &after)) {
block = num; line = 0; in_block = 1; p = after;
continue;
}
for (nl = p; nl < end && *nl != '\n'; nl++) { }
len = (unsigned)(nl - p);
if (len && p[len - 1] == '\r') len--;
if (in_block) {
line++;
if (len >= LINE_MAX) { say_where(b, name, block, line); say(b, " is too long\n"); return 0; }
if (len) {
for (i = 0; i < len; i++) text[i] = (char)p[i];
int how = v4_boot_line(b, text, len);
if (how != V4_TEXT_COMPLETED) {
say_where(b, name, block, line);
say(b, how == V4_TEXT_ERROR ? " ended in an error: "
: how == V4_TEXT_QUIT ? " ended with QUIT: "
: how == V4_BOOT_LINE_NO_INPUT ? " reads the keyboard: "
: " stopped the node: ");
b->out(text, len); say(b, "\n");
return 0;
}
}
}
p = (nl < end) ? nl + 1 : end;
}
say(b, "PARITY:V4_CAPSULE name="); say(b, name);
say(b, " capsule_id="); say_hex(b, cap->capsule_id);
say(b, " capsule_hash="); say_hex(b, cap->content_hash);
say(b, " dict_hash="); say_hex(b, v4_boot_dict_hash(b->n, b->im));
say(b, "\n");
return 1;
}
/* ---- the nucleus ----------------------------------------------------------- */
/* 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)
static int load_nucleus(const v4_boot *b)
{
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;
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");
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;
/* until it has taken every word and is waiting for a message: reading
* its ports again, but from its memory now and not from the port */
while (at < words || !(n->reading && !n->given && n->p != b->im->port + (v4_cell)V4_PORT_ANY)) {
(void)v4_exec_step_word(n, b->es, b->h);
if (n->stopped || n->asking || ++steps > STEP_LIMIT) {
say(b, "V4: the node did not take the nucleus in\n");
return 0;
}
if (n->reading && !n->given) {
v4_ucell word = 0;
unsigned i;
if (at >= words) {
if (n->p != b->im->port + (v4_cell)V4_PORT_ANY) break; /* it is waiting for a message */
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;
}
/* ---- the whole boot ------------------------------------------------------ */
int v4_boot_run(const v4_boot *b)
{
unsigned i;
/* a node is born with nothing in it, and is sent the nucleus */
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");
return 0;
}
/* what is on its two ports, the kernel and the console, takes a word
* when it is written: the node need never wait before writing to them
* (node.h, v4_node_port_status) */
v4_node_port_status(b->n, 0, 1u << KERNEL_PORT | 1u << CONSOLE_PORT);
v4_blocks_init(&boot_blocks, b->im->block_window);
if (!load_nucleus(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++) {
char text[64];
unsigned at = 0, k, num = i + 1, div = 1;
while (num / div >= 10) div *= 10;
for (; div; div /= 10) text[at++] = (char)('0' + num / div % 10);
for (k = 0; " KERNEL-WORD "[k]; k++) text[at++] = " KERNEL-WORD "[k];
for (k = 0; b->words[i][k] && at < sizeof text; k++) text[at++] = b->words[i][k];
if (b->words[i][k] || v4_boot_line(b, text, at) != V4_TEXT_COMPLETED) {
say(b, "V4: kernel word "); say(b, b->words[i]); say(b, " could not be made\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
}
for (i = 0; i < sizeof boot_capsules / sizeof boot_capsules[0]; i++)
if (!load_capsule(b, boot_capsules[i])) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
/* The kernel's words and the capsules are the system: COLD comes back
* to here, and FORGET will not go below it. */
v4_image_seal(b->n, b->im);
#if V4_POST_AT_BOOT
{
static const char cold[] = "COLD";
v4_post_host host;
v4_post_tally tally;
unsigned printed = 0;
int how;
host.self = (void *)b; host.n = b->n; host.line = post_line; host.say = post_say;
if (!v4_post_run(&host, v4_post_cases, v4_post_case_count, &tally)) {
say(b, "V4: POST reported failures\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
/* POST leaves nothing: the node is put back to the system as it was
* sealed, as v3 puts the dictionary back after each of its suites
* (v3/src/test_runner/test_common.c, run_test_suite). What COLD
* prints is not shown. */
v4_dstack_reset(&b->n->ds);
how = run_line(b, cold, (unsigned)sizeof cold - 1u, (char *)cold, 0u, &printed);
if (how != V4_TEXT_COMPLETED && how != V4_TEXT_QUIT) {
say(b, "V4: the node did not come back to the system after POST\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
}
#endif
say(b, "PARITY:V4_SYSTEM word_count="); say_dec(b, word_count(b->n, b->im));
say(b, " dict_hash="); say_hex(b, v4_boot_dict_hash(b->n, b->im));
say(b, "\n");
#if V4_POST_AT_BOOT
say(b, "PARITY:OK\nPOST: PASSED\n");
#else
say(b, "PARITY:OK\n");
#endif
return 1;
}