A v4 node no longer reads its own command line or prints a prompt. Its host puts a line of text in the node's input buffer and starts it at (LINE); the node interprets it and stops at (IDLE), leaving in (LINE-STATUS) how it ended: completed, an error, or QUIT. The host says " ok" or " ERROR" and prompts, as the kernel's REPL does for a v3 VM. A line may be 1024 characters, a block, as v3's. Ruled 2026-10-05 (V3-PARITY.md 1b); design ENGINE.md 3.1. - quit.v4: (REPL), the node's prompt loop, is gone; (LINE) (IDLE) (DONE) - image.h/.c: v4_line_begin, v4_line_done, v4_line_status; the node is idle at switch-on - boot.c: v4_boot_line, the one loop the hosted binary, the kernel and the capsule loader hand a line with; the code that took " ok" and the prompt back out of the node's output is gone - hosted.c, sk_v4.c: the prompt and the line editing are the host's - test_host_quit.c: the tests are the node's host; two tests of the old 80-character prompt line now test a whole line, 1024 and 1025 characters Verified: make -C v4 test passes at both widths; hosted-check passes on three ISAs; clean qemu with STARFORTH_V4=1 on amd64, aarch64 and riscv64 passes POST (550 of 550) with the same hashes as hosted, and three lines typed at each bare-metal prompt through the serial port are answered correctly (logs/20261005-180922, -181152, -181541). Still the lone node: kernel_main.c starts it before the fleet tables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
281 lines
9.3 KiB
C
281 lines
9.3 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/boot.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 ----------------- */
|
|
|
|
#define FNV_OFFSET 0xcbf29ce484222325ULL
|
|
#define FNV_PRIME 0x00000100000001b3ULL
|
|
|
|
static uint64_t hash_cell(uint64_t h, v4_cell c)
|
|
{
|
|
v4_ucell u = (v4_ucell)c;
|
|
unsigned i;
|
|
for (i = 0; i < V4_CELL_BITS / 8; i++) {
|
|
h ^= (uint64_t)((u >> (8 * i)) & 0xffu);
|
|
h *= FNV_PRIME;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im)
|
|
{
|
|
uint64_t h = FNV_OFFSET;
|
|
v4_cell here = (n->mem[im->dp] + 3) / 4, k;
|
|
|
|
if (here < 0) here = 0;
|
|
if (here > (v4_cell)V4_NODE_WORDS) here = (v4_cell)V4_NODE_WORDS;
|
|
for (k = 0; k < here; k++) h = hash_cell(h, n->mem[k]);
|
|
return hash_cell(h, n->mem[im->latest]);
|
|
}
|
|
|
|
static uint64_t image_hash(const v4_image *im)
|
|
{
|
|
uint64_t h = FNV_OFFSET;
|
|
unsigned i;
|
|
for (i = 0; i < im->count; i++) {
|
|
h = hash_cell(h, im->cells[i].addr);
|
|
h = hash_cell(h, im->cells[i].value);
|
|
}
|
|
return h;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
|
|
{
|
|
v4_node *n = b->n;
|
|
uint64_t steps = 0;
|
|
unsigned i;
|
|
|
|
if (!v4_line_begin(n, b->im, text, len)) return V4_BOOT_LINE_TOO_LONG;
|
|
for (;;) {
|
|
(void)v4_exec_step_word(n, b->es, b->h);
|
|
if (n->console_len) {
|
|
for (i = 0; i < n->console_len; i++) post_watch((char)n->console[i]);
|
|
b->out((const char *)n->console, n->console_len);
|
|
n->console_len = 0;
|
|
}
|
|
if (n->stopped) return V4_BOOT_LINE_STOPPED;
|
|
if (v4_line_done(n, b->im)) return v4_line_status(n, b->im);
|
|
if (v4_image_waiting(n, b->im)) { /* the line 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_LINE_COMPLETED) {
|
|
say_where(b, name, block, line);
|
|
say(b, how == V4_LINE_ERROR ? " ended in an error: "
|
|
: how == V4_LINE_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 whole boot ------------------------------------------------------ */
|
|
|
|
int v4_boot_run(const v4_boot *b, unsigned char *disk, unsigned blocks)
|
|
{
|
|
unsigned i;
|
|
|
|
post_len = 0; post_seen = 0; post_clean = 0;
|
|
if (!v4_image_boot(b->n, b->es, b->h, b->im, disk, blocks)) {
|
|
say(b, "V4: the nucleus image is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
|
|
return 0;
|
|
}
|
|
say(b, "PARITY:V4_NUCLEUS words="); say_dec(b, word_count(b->n, b->im));
|
|
say(b, " image_hash="); say_hex(b, image_hash(b->im));
|
|
say(b, "\n");
|
|
|
|
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
|
|
|
|
#if V4_POST_AT_BOOT
|
|
say(b, "PARITY:OK\nPOST: PASSED\n");
|
|
#else
|
|
say(b, "PARITY:OK\n");
|
|
#endif
|
|
return 1;
|
|
}
|