A block is a kernel request, as ENGINE.md 3.3 has it: the node puts the
block's number and the address of 256 cells on its stack and writes the
request to port 0, and the kernel leaves the status there. The requests
are -1, read, and -2, write, the same for every node. v4/system/blocks.c
serves them from the kernel's block subsystem, which is v3's. The four
storage registers are gone from the engine.
The device that spoke block messages (4a505a15) is withdrawn with its
test and its message types: Captain Bob ruled on 2026-10-07 that it, a
node's own drive, and nodes with no storage had left the OS as designed
(docs/v4.0.0/MESH.md 8.5).
Hera no longer sends POST to the nodes she births: POST is the kernel's,
once. Every node has its kernel on port 0; it serves a node's blocks and,
for Hera alone, her requests for nodes and capsules.
Bare metal: the node boots and is POSTed against POST's own block RAM,
and the kernel's chain -- fast RAM, the ramdrive, the virtio disk -- is
set up after POST and before the prompt, as on the v3 path. The disk is
read and not written: nothing in v4 yet gives the owner's word that it
may be formatted. A hosted program has the chain's fast RAM, as hosted
v3 has with no disk. Error 17 is Storage refused.
make -C v4 test and sanitize pass at both widths; hosted-check passes on
three ISAs; amd64, aarch64 and riscv64 boot, POST 538 of 538, with the
typed session: logs/20261007-081603, -081839, -082226. The hashes are
the same on all six.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
392 lines
15 KiB
C
392 lines
15 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/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. */
|
|
/* POST is part of the boot: v4:post79.4th is loaded after the vocabulary it
|
|
* tests and must end with a clean tally. -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
|
|
static const char *const boot_capsules[] = {
|
|
"v4:forth79.4th",
|
|
#if V4_POST_AT_BOOT
|
|
"v4:post79.4th",
|
|
#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 ---------------------------------------------- */
|
|
|
|
/* POST's verdict. The POST capsule ends by printing one line,
|
|
* PARITY:V4_POST tests=N pass=N fail=N
|
|
* and the boot passes only if it has seen that line with fail=0. That no
|
|
* line was refused is not enough: a POST that was broken half way would
|
|
* refuse nothing. */
|
|
static char post_line[96];
|
|
static unsigned post_len;
|
|
static int post_seen, post_clean;
|
|
|
|
static int text_at(const char *line, unsigned len, unsigned at, const char *want)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; want[i]; i++)
|
|
if (at + i >= len || line[at + i] != want[i]) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static void post_watch(char c)
|
|
{
|
|
static const char clean[] = " fail=0";
|
|
if (c != '\n') {
|
|
if (post_len < sizeof post_line) post_line[post_len++] = c;
|
|
return;
|
|
}
|
|
if (text_at(post_line, post_len, 0, "PARITY:V4_POST ")) {
|
|
post_seen = 1;
|
|
post_clean = post_len >= sizeof clean - 1 && text_at(post_line, post_len, post_len - (unsigned)(sizeof clean - 1), clean);
|
|
}
|
|
post_len = 0;
|
|
}
|
|
|
|
/* 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 */
|
|
|
|
int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
|
|
{
|
|
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(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);
|
|
post_watch(c);
|
|
b->out(&c, 1);
|
|
}
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
/* ---- 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;
|
|
|
|
post_len = 0; post_seen = 0; post_clean = 0;
|
|
/* 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);
|
|
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; }
|
|
#if V4_POST_AT_BOOT
|
|
if (!post_seen || !post_clean) {
|
|
say(b, post_seen ? "V4: POST reported failures\n" : "V4: POST did not report\n");
|
|
say(b, "PARITY:FAIL\nPOST: FAILED\n");
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
/* 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
|
|
say(b, "PARITY:OK\nPOST: PASSED\n");
|
|
#else
|
|
say(b, "PARITY:OK\n");
|
|
#endif
|
|
return 1;
|
|
}
|