/* 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 */ } } }