Files
LithosAnanke/kernel/src/v4/sk_v4.c
T
Claude e8e7ec8b9f feat(v4): word accounts at call -- the word ID in the header, the kernel's records, the Stadium charged per tick (#189)
ENGINE step 4 Phase 2 (plan docs/v4.0.0/plans/2026-11-13-engine-step4-phase2-word-accounts.md). Rulings,
Captain Bob 2026-11-13: "ID field in the header"; "Per heartbeat tick"; the ACL "All of it in Phase 4".

- The word ID, at xt-4 before the flags (ENGINE.md 3b). The system's words are numbered as they are
  assembled, by a new directive, #id, (and #ids, the count), in the C assembler (text.c) and in the
  machine ROM's (capsules/v4/rom), which counts in the system's own (NEXT-ID). (HEADER) numbers later
  words; FORGET gives back the IDs it frees, always the newest; COLD resets to (BOOT-ID); (SEAL) records
  it. All 313 system headers carry the cell.
- The kernel's records (v4/mesh.h): (TELL-DEFINED) and (TELL-FORGOTTEN), requests -19/-20 on port 0,
  which the kernel puts in (WORD-DEFINED)/(WORD-FORGOTTEN) when it seals a node, and reads the system's
  own words once. A bit per address marks a word's xt (calls into the ROM or headerless code are not
  words); at call, the bit test and a count. v4_mesh_charge hands the owner the calls since.
- The Stadium: stadium_words_charge -- N calls as N of stadium_word_dispatch's dispatches, shared by calls
  when the reservoir is short; sk_v4.c charges every node's words at the tick beside its opcodes, and
  stadium_word_forget before an ID is used again.
- Tests first: test_text.c, test_host_dict.c, test_host_quit.c (numbering, FORGET, COLD),
  test_mesh_boot.c (44 checks), kernel/tests/test_stadium_words.c; qemu_session.py: Hera's words
  admitted after POST.
- amd64: Stadium words: promotions=141 after POST, CONSERVED; POST 539/539; word_count=377;
  dict_hash 0xa854ba15b437d288 (the new cell); the boot 142 s against Phase 1's 153 s.
- v4 test, test-32: all pass; v4-checks: 80 programs under the sanitizers, 0 failures; the hosted
  builds alike. docs/reference/v4-dictionary.tsv: the two new words; FABRIC-3 XLVIII.24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm
2026-11-13 19:12:44 +00:00

396 lines
18 KiB
C

/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* sk_v4.c -- StarForth v4 on bare metal: one host node at a prompt.
*
* The node is the golden model of the F18-derived engine (v4/src). It comes
* up in the kernel's fabric (v4/include/v4/mesh.h; #189 -- until 2026-11-13 as the hosted
* system still does, by v4_boot_run, v4/include/v4/boot.h):
* the machine's ROM assembles the system from its source, then the capsules
* from the directory baked into this kernel are loaded, each checked and its
* parity line printed, then the prompt.
* docs/v4.0.0/NUCLEUS.md. This file is all the kernel adds:
*
* - what the node prints goes to the kernel's console;
* - the prompt, and the line: it is read and edited here and handed to the
* node whole (docs/v4.0.0/ENGINE.md 3.1), and how it ended is said here;
* - blocks are kept in memory, and are gone at power-off;
* - the identity gate: with no identity drive the system is headless,
* and a console exists only while Zuse's drive is attached and checked
* (sk_v4_identity.c; ENGINE.md 2, ruled 2026-10-20).
*
* The kernel brings it up in its own boot, after the Stadium (#189, ENGINE.md step 4,
* Phase 1): sk_v4_boot(), the kernel's checks after POST, then sk_v4_prompt().
*
* Nothing of the v3 VM is started. docs/v4.0.0/DECOMPOSITION.md.
*/
#include "starkernel/v4/sk_v4_mint.h"
#include "starkernel/v4/sk_v4.h"
#include "starkernel/v4/sk_v4_identity.h"
#include "starkernel/v4/sk_v4_stop.h"
#include "starkernel/kbd_input.h"
#include "starkernel/console.h"
#include "starkernel/arch.h"
#include "starkernel/timer.h"
#include "v4/boot.h"
#include "v4/fabric.h"
#include "v4/manage.h"
#include "v4/mesh.h"
#include "starkernel/kmalloc.h"
#include "block_subsystem.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_generated.h"
#include "starkernel/capsule_sig.h"
#include "starkernel/capsule_blocks.h"
#ifdef STARFORTH_ENABLE_VM
#include "starkernel/vm/stadium.h"
#include "starkernel/vm/stadium_ops.h"
#include "starkernel/vm/stadium_words.h"
#endif
#define SK_V4_LINE 1024u /* what the line editor here takes: a line, as the boot takes one (v4/system/boot.c) */
/* v3's prompt (repl.c's SK_PROMPT_TEXT): "ok> " in bright cyan, after the
* console's own "[zuse@Hera] " line prefix. */
#define SK_V4_PROMPT "\x1b[38;2;85;255;255mok> \x1b[39m"
/* THE KERNEL'S FABRIC (#189, ENGINE step 4 Phase 1, Task 5c; Captain Bob, 2026-11-13: "Build it to
* proper spec"). The kernel boots the fabric by v4/mesh.h: the nucleus assembled by the machine's
* ROM, Hera born empty at place 0 and given it, her system and POST -- and every node steps
* together; there is no scheduler. A node Hera births has its room from the kernel's heap, a Stadium
* quota under its node key (stadium_node_key) granted from hers, and its opcodes charged as hers
* are; killed, its room and its quota are given back. */
#define SK_V4_PLACES 16u
static v4_mesh sk_v4_mesh;
#define SK_V4_IDLE_STEPS 4096u /* fabric steps between looks at the keyboard, while a node runs */
static v4_place sk_v4_places[SK_V4_PLACES];
static v4_wire_queue sk_v4_queues[4u * SK_V4_PLACES];
static v4_manage_entry sk_v4_reg[SK_V4_PLACES];
static v4_blocks sk_v4_blocks[SK_V4_PLACES];
static v4_device sk_v4_kernel[SK_V4_PLACES];
static v4_fabric_node sk_v4_nucleus, sk_v4_hera;
static v4_image sk_v4_image;
static v4_cell sk_v4_keyed[SK_V4_PLACES]; /* by place: the number its Stadium quota is keyed by; 0: none */
/* The words' accounts (#189, Phase 2; v4/mesh.h): each node's records, kept by word ID, and the bit for each
* address that is a word's xt -- from the kernel's heap at the boot -- and a tick's charge gathered. */
static uint32_t sk_v4_charge_ids[V4_MESH_WORD_IDS];
static uint64_t sk_v4_charge_calls[V4_MESH_WORD_IDS];
static uint32_t sk_v4_charge_n;
static void sk_v4_charge_one(void *ctx, v4_cell id, v4_uheat_t calls)
{
(void)ctx;
if (sk_v4_charge_n < V4_MESH_WORD_IDS) {
sk_v4_charge_ids[sk_v4_charge_n] = (uint32_t)id;
sk_v4_charge_calls[sk_v4_charge_n] = (uint64_t)calls;
sk_v4_charge_n++;
}
}
/* a word has gone: its ID will be used again, so the Stadium forgets its patron first (v3's coherence) */
static void sk_v4_forgot(void *self, unsigned place, v4_cell id)
{
(void)self;
#ifdef STARFORTH_ENABLE_VM
if (place < SK_V4_PLACES && sk_v4_keyed[place] != 0)
stadium_word_forget(stadium_node_key((uint32_t)sk_v4_keyed[place]), (uint32_t)id);
#else
(void)place; (void)id;
#endif
}
static v4_fabric_node *sk_v4_room(void *self)
{
v4_fabric_node *x = (v4_fabric_node *)kmalloc(sizeof *x);
unsigned char *b = (unsigned char *)x;
size_t i;
(void)self;
if (x) for (i = 0; i < sizeof *x; i++) b[i] = 0;
return x;
}
static void sk_v4_unroom(void *self, v4_fabric_node *x) { (void)self; kfree(x); }
static void sk_v4_gone(void *self, unsigned place)
{
(void)self;
#ifdef STARFORTH_ENABLE_VM
if (place < SK_V4_PLACES && sk_v4_keyed[place] != 0) {
VMUuid key = stadium_node_key((uint32_t)sk_v4_keyed[place]);
stadium_ops_forget(key);
(void)stadium_return_quota(key, stadium_node_key(SK_V4_HERA_NODE));
stadium_words_print_boot_diagnostics(stadium_node_key(SK_V4_HERA_NODE)); /* conservation, after */
}
#endif
if (place < SK_V4_PLACES) sk_v4_keyed[place] = 0;
}
/* A node the asker birthed has booted: its parity line, as the boot prints a capsule's. */
static void sk_v4_parity(void *self, unsigned place, v4_cell number, const v4_node *n)
{
static const char hex[] = "0123456789abcdef";
uint64_t h = v4_image_dict_hash(n, &sk_v4_image);
char line[64];
unsigned at = 0, k;
const char *s;
(void)self; (void)place;
for (s = "PARITY:V4_BIRTH node="; *s; s++) line[at++] = *s;
{ char d[20]; unsigned nd = 0; v4_ucell v = (v4_ucell)number; do { d[nd++] = (char)('0' + v % 10u); v /= 10u; } while (v && nd < 20);
while (nd) line[at++] = d[--nd]; }
for (s = " dict_hash=0x"; *s; s++) line[at++] = *s;
for (k = 16; k-- > 0; ) line[at++] = hex[(h >> (4 * k)) & 0xFu];
line[at] = 0;
console_println(line);
}
static const char *const sk_v4_capsules[] = { "v4:centre.4th", "v4:hera.4th" };
static char sk_v4_text[SK_V4_LINE + 1u];
/* POST's block RAM, as the v3 path has (sk_vm_bootstrap.c, sk_post_blk_ram):
* the node boots and is POSTed with these blocks, 1 to 2047 and nothing
* above them, and the kernel's real chain is set up afterwards. */
static uint8_t sk_v4_post_blk_ram[BLK_RAM_BLOCKS * BLK_FORTH_SIZE];
/* HEAT-OP into the Stadium (#189; Captain Bob: "Per heartbeat tick"): the node's opcode counts are
* charged to its opcode patrons at the heartbeat's service points -- where its host waits for a key,
* a tick at a time, and after each line. */
void sk_v4_charge_opcodes(void)
{
#ifdef STARFORTH_ENABLE_VM
unsigned k;
uint64_t now = heartbeat_ticks();
for (k = 0; k < SK_V4_PLACES; k++) {
v4_fabric_node *x = v4_fabric_node_at(&sk_v4_mesh.f, k);
v4_cell number;
if (!x) continue;
number = x->n.mem[sk_v4_image.me];
if (number <= 0) continue; /* not told its number yet: charged when it is */
if (sk_v4_keyed[k] != number) { /* a node first seen: its quota, granted from Hera's */
VMUuid key = stadium_node_key((uint32_t)number);
if (stadium_quota_slot_for_vm(key) < 0
&& stadium_grant_quota(key, stadium_node_key(SK_V4_HERA_NODE)) != 0) continue;
sk_v4_keyed[k] = number;
}
stadium_ops_charge(stadium_node_key((uint32_t)number), x->h.op, now);
/* and its words, by their calls since (#189, Phase 2: "Per heartbeat tick") */
sk_v4_charge_n = 0;
v4_mesh_charge(&sk_v4_mesh, k, sk_v4_charge_one, 0);
if (sk_v4_charge_n)
stadium_words_charge(stadium_node_key((uint32_t)number), sk_v4_charge_ids, sk_v4_charge_calls, sk_v4_charge_n, now);
}
#endif
}
static void sk_v4_out(const char *text, unsigned len)
{
unsigned i;
for (i = 0; i < len; i++) console_putc(text[i]);
}
/* a character for a line that reads the keyboard, when there is one: the
* serial line or any keyboard (docs/v4.0.0/STICK.md 5.4) */
static int sk_v4_key(void)
{
int c = sk_input_getc();
return c < 0 ? V4_BOOT_KEY_NONE : c;
}
/* THE KERNEL'S WORDS for the lone node. BYE is v3's (mama_word_bye): a cold
* restart, and MINT-ZUSE mints the real Zuse onto a second stick
* (sk_v4_mint.c). v3's BYE reaps Hera's children first; the lone node is started
* before there is a fleet, so there are none. The rest of the kernel's
* words come when the node is brought up in the VM's place (ENGINE.md
* step 4), where v3's own functions serve them. */
static const char *const sk_v4_words[] = { "BYE", "MINT-ZUSE" };
static void sk_v4_serve(v4_node *node, unsigned request)
{
(void)node;
if (request == 1) {
console_println("BYE: cold restart");
arch_cold_reset();
}
/* MINT-ZUSE: the real Zuse onto a second stick (docs/v4.0.0/STICK.md 6a;
* plan 2026-10-29-stick-5, Task 4). No POST case: it waits for a stick
* and a typed YES, as BYE has none. */
if (request == 2) (void)sk_v4_mint_zuse();
}
/* Read a line from the keyboard: characters are echoed, backspace rubs one
* out, Enter ends it. Returns its length; or -1 when, with nothing typed,
* Zuse's session has ended (her drive was pulled): the caller goes headless
* (not in a build with EMERGENCY_CONSOLE_ENABLED, whose console needs no
* login: v3's sk_console_readline() the same). While nothing is typed the
* idle beat runs, as v3's. A line typed past SK_V4_LINE characters is not
* cut short and run: what is typed after that is not kept, and
* SK_V4_LINE + 1 is returned, which the boot refuses as too long
* (V4_BOOT_LINE_TOO_LONG), running nothing. */
#define SK_V4_CURSOR_BLINK 50u /* ticks between blinks, as v3's (500 ms at 100 Hz) */
static int sk_v4_readline(void)
{
int len = 0, over = 0;
int cursor_on = 1;
uint64_t blink = heartbeat_ticks();
console_fb_draw_cursor();
for (;;) {
int c = sk_input_getc();
if (c < 0) {
uint64_t now = heartbeat_ticks();
if (now - blink >= SK_V4_CURSOR_BLINK) {
blink = now;
cursor_on = !cursor_on;
if (cursor_on) console_fb_draw_cursor();
else console_fb_erase_cursor();
}
if (len == 0) {
sk_v4_identity_beat();
#if !EMERGENCY_CONSOLE_ENABLED
if (!sk_v4_identity_present()) return -1;
#endif
} else {
heartbeat_service();
}
sk_v4_charge_opcodes();
/* Nothing typed, and a node can run: the fabric steps on between lines, as during them
* -- every node steps together (JUSTIFICATION.md 4; #189, "Check for the edge cases").
* A batch, then the keyboard is looked at again. */
if (v4_mesh_busy(&sk_v4_mesh)) {
unsigned k;
for (k = 0; k < SK_V4_IDLE_STEPS; k++) (void)v4_mesh_step(&sk_v4_mesh);
continue;
}
/* Nothing typed: wait for the next interrupt, not in a spin (#151 -- the spin kept a
* CPU core 100% busy at the prompt, and the demo's host with it). The heartbeat's
* timer wakes it at 100 Hz, so a key is seen within a tick, as the blink and the
* identity beat are. */
arch_halt();
continue;
}
if (c == '\r' || c == '\n') {
console_fb_erase_cursor(); console_putc('\n');
return over ? (int)SK_V4_LINE + 1 : len;
}
if (c == 8 || c == 127) {
if (over > 0) { over--; console_putc(8); console_putc(' '); console_putc(8); }
else if (len > 0) { len--; console_putc(8); console_putc(' '); console_putc(8); }
} else if (c >= 32 && c < 127) {
if (len < (int)SK_V4_LINE) sk_v4_text[len++] = (char)c;
else over++;
console_putc((char)c);
}
cursor_on = 1; /* typing shows a solid cursor, as v3's */
blink = heartbeat_ticks();
console_fb_draw_cursor();
}
}
/* a line to Hera, as a message from the console, and all that follows from it (v4/mesh.h) */
static int sk_v4_tell(const char *text, unsigned len) { return v4_mesh_tell(&sk_v4_mesh, V4_MESH_HERA, text, len); }
void sk_v4_boot(void (*chain)(void), const BootDisk *boot_disk)
{
console_println("StarForth v4: Hera, born in the kernel's fabric");
sk_v4_image = v4_map_image;
sk_v4_mesh.places = sk_v4_places; sk_v4_mesh.capacity = SK_V4_PLACES;
sk_v4_mesh.queues = sk_v4_queues; sk_v4_mesh.queue_count = sizeof sk_v4_queues / sizeof sk_v4_queues[0];
sk_v4_mesh.reg = sk_v4_reg; sk_v4_mesh.blocks = sk_v4_blocks; sk_v4_mesh.kernel = sk_v4_kernel;
sk_v4_mesh.nucleus = &sk_v4_nucleus; sk_v4_mesh.hera = &sk_v4_hera;
sk_v4_mesh.room = sk_v4_room; sk_v4_mesh.unroom = sk_v4_unroom;
sk_v4_mesh.born = 0; sk_v4_mesh.gone = sk_v4_gone; sk_v4_mesh.parity = sk_v4_parity;
sk_v4_mesh.accounts = (v4_mesh_account *)kmalloc((size_t)SK_V4_PLACES * V4_MESH_WORD_IDS * sizeof(v4_mesh_account));
sk_v4_mesh.is_word = (unsigned char *)kmalloc((size_t)SK_V4_PLACES * (V4_NODE_WORDS / 8u));
if (!sk_v4_mesh.accounts || !sk_v4_mesh.is_word) { /* no records kept: the words are not charged */
if (sk_v4_mesh.accounts) kfree(sk_v4_mesh.accounts);
if (sk_v4_mesh.is_word) kfree(sk_v4_mesh.is_word);
sk_v4_mesh.accounts = 0; sk_v4_mesh.is_word = 0;
console_println("StarForth v4: no room for the words' accounts; words are not charged");
} else { /* none kept yet: all zero */
unsigned char *z = (unsigned char *)sk_v4_mesh.accounts;
size_t i, n = (size_t)SK_V4_PLACES * V4_MESH_WORD_IDS * sizeof(v4_mesh_account);
for (i = 0; i < n; i++) z[i] = 0;
for (i = 0; i < (size_t)SK_V4_PLACES * (V4_NODE_WORDS / 8u); i++) sk_v4_mesh.is_word[i] = 0;
}
sk_v4_mesh.forgot = sk_v4_forgot;
sk_v4_mesh.out = sk_v4_out; sk_v4_mesh.key = sk_v4_key;
sk_v4_mesh.words = sk_v4_words; sk_v4_mesh.word_count = sizeof sk_v4_words / sizeof sk_v4_words[0];
sk_v4_mesh.serve = sk_v4_serve;
sk_v4_mesh.capsules = sk_v4_capsules; sk_v4_mesh.capsule_count = sizeof sk_v4_capsules / sizeof sk_v4_capsules[0];
sk_v4_mesh.im = &sk_v4_image; sk_v4_mesh.self = 0;
blk_subsys_init(sk_v4_post_blk_ram, sizeof(sk_v4_post_blk_ram));
if (!v4_mesh_boot(&sk_v4_mesh)) {
console_println("StarForth v4: not started");
sk_v4_stop(); /* #177: halts, not a 100% spin */
}
if (chain) {
/* the blocks are other blocks now: what the node has in its two
* buffers is POST's, and is let go */
chain();
if (sk_v4_tell("EMPTY-BUFFERS", 13) != V4_TEXT_COMPLETED) {
console_println("StarForth v4: the node did not let go of POST's blocks");
sk_v4_stop(); /* #177: halts, not a 100% spin */
}
}
sk_v4_identity_boot(boot_disk);
#if EMERGENCY_CONSOLE_ENABLED
/* a console that needs no login has the screen from the start */
console_fb_mirror(1);
console_fb_enable_ttf();
#endif
}
void sk_v4_prompt(void)
{
/* The prompt is the host's: read a line, hand it to the node, say how it
* ended. */
for (;;) {
int len;
int how;
int logged_out = 0;
sk_v4_charge_opcodes(); /* what the last line retired (#189) */
#if !EMERGENCY_CONSOLE_ENABLED
/* no identity, no console: headless until Zuse's drive is checked.
* The drives are looked at first, now: one pulled while the last
* line ran is seen before a prompt is shown (the identity gate's
* final review, item 1). Not in a build with
* EMERGENCY_CONSOLE_ENABLED, whose console needs no login (v3's
* kernel_main.c and repl.c the same; item 3). */
sk_v4_identity_check_now();
if (!sk_v4_identity_present()) sk_v4_identity_wait();
#endif
console_puts(SK_V4_PROMPT);
len = sk_v4_readline();
if (len < 0) continue;
how = sk_v4_tell(sk_v4_text, (unsigned)len);
/* The line is waiting for an answer no one here can send. The next
* line typed breaks the wait (MESH.md 7b.6): it is a message to
* Hera, the line that was waiting ends in an error, and then the
* line just typed is done -- all in the one tell, which says how the
* last ended. */
while (how == V4_BOOT_LINE_WAITING) {
len = sk_v4_readline();
if (len < 0) {
/* Zuse's session ended while the line waited: its wait is
* ended now, by an empty line, and not carried into the
* next session (Captain Bob, 2026-10-26: "end the waiting
* line at logout"). How it ended is shown. */
how = sk_v4_tell("", 0u);
if (how == V4_TEXT_ERROR) console_puts(" ERROR\n");
else if (how == V4_TEXT_COMPLETED) console_puts(" ok\n");
logged_out = 1;
break;
}
how = sk_v4_tell(sk_v4_text, (unsigned)len);
if (how == V4_BOOT_LINE_TOO_LONG) { console_puts("Line too long\n ERROR\n"); how = V4_BOOT_LINE_WAITING; continue; }
}
if (logged_out) continue;
if (how == V4_TEXT_COMPLETED) console_puts(" ok\n");
else if (how == V4_TEXT_ERROR) console_puts(" ERROR\n");
else if (how == V4_BOOT_LINE_TOO_LONG) console_puts("Line too long\n ERROR\n");
else if (how == V4_BOOT_LINE_REFUSED) console_puts("Message refused\n ERROR\n"); /* the node's wire is full: error 19's words */
else if (how == V4_BOOT_LINE_STOPPED) {
console_println("StarForth v4: the node stopped on a fault");
sk_v4_stop(); /* #177: halts, not a 100% spin */
}
}
}