Files
LithosAnanake/kernel/src/vm/stadium_words.c
T
rajamesandJunie a8b70e88d3 Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-01 15:40:09 -04:00

342 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* stadium_words.c - Word patrons on the Stadium (FABRIC-0.md §17.3/§17.7,
* punch list item 4.1). See stadium_words.h for the design rationale.
*/
#include "starkernel/vm/stadium_words.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium.h"
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- diagnostic print only */
#include "vm.h" /* DICTIONARY_SIZE, WORD_ID_INVALID */
/*
* StadiumWordSlot - the kernel-side word_id -> cell_index map (decided
* 2026-08-05: no DictEntry field). `last_decay_tick` is this layer's own
* bookkeeping, separate from DictEntry.physics.last_decay_tick -- that field
* belongs to execution_heat's decay, which item 4.1 does not touch.
*
* item 4.2 fix (FABRIC-0.md §25.5): keyed by [quota slot][word_id], not just
* word_id. word_id is assigned per-VM (vm->next_word_id in
* dictionary_management.c), not globally unique -- a single shared
* word_id -> cell_index map let two VMs' independently-numbered word_ids
* (e.g. both VMs' own "DUP") alias onto the same slot, so one VM's dispatch
* could cool/heat-pump a cell it did not own and credit/debit the wrong
* VM's reservoir. Exposed only because item 4.2 restored a second VM
* (Hermes) with her own dictionary; invisible with Hera alone.
*/
typedef struct {
size_t cell_index; /* STADIUM_CELL_NONE if not resident */
uint64_t last_decay_tick;
} StadiumWordSlot;
/* Row-per-quota-slot, kmalloc'd at stadium_words_init() to stadium_max_vm_
* count() rows of DICTIONARY_SIZE entries each -- replaces the old static
* word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE] (2026-08-15: the VM
* count bound is computed from RAM, not a compile-time constant, so this
* can no longer be a flat static array). */
static StadiumWordSlot **word_slots = (StadiumWordSlot **)0;
static int words_initialized = 0;
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
void stadium_words_init(void) {
size_t slot;
uint32_t i;
size_t count = stadium_max_vm_count();
if (words_initialized) return; /* not re-entrant -- see stadium_words.h */
if (count == 0) {
console_println("Stadium words: init skipped (Stadium not initialized)");
return;
}
word_slots = (StadiumWordSlot **)kmalloc(count * sizeof(StadiumWordSlot *));
stat_promotions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
if (!word_slots || !stat_promotions || !stat_evictions) {
console_println("Stadium words: kmalloc failed for per-VM tables");
if (word_slots) kfree(word_slots);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (slot = 0; slot < count; slot++) {
word_slots[slot] = (StadiumWordSlot *)kmalloc(DICTIONARY_SIZE * sizeof(StadiumWordSlot));
if (!word_slots[slot]) {
console_println("Stadium words: kmalloc failed for a word-slot row");
/* Free everything allocated so far, including this row's
* predecessors, and bail the same all-or-nothing way
* stadium_boot_init() does. */
{
size_t j;
for (j = 0; j < slot; j++) kfree(word_slots[j]);
}
kfree(word_slots);
kfree(stat_promotions);
kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[slot][i].cell_index = STADIUM_CELL_NONE;
word_slots[slot][i].last_decay_tick = 0;
}
stat_promotions[slot] = 0;
stat_evictions[slot] = 0;
}
words_initialized = 1;
}
static int cell_is_resident(size_t idx) {
const uint8_t *bm = stadium_header_bitmap();
if (!bm) return 0;
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* word_dispatch_pull - Reservoir pull for word-execution admission, clamped
* to leave a floor for application-level use (FABRIC-0.md §25.5/§25.7,
* Captain Bob's ruling 2026-08-06). Without this, stadium_word_dispatch()
* pulling STADIUM_WORD_HEAT_QUANTUM on every dispatch -- not just the first
* admission of a given word -- exhausts a VM's entire reservoir within
* roughly 32 total dispatches (65536 / 2048), starving any item-4.2-style
* application economy sharing the same VM's reservoir before it gets a
* chance to pull anything. The floor is Q48_ONE / 3, the same "VM-COUNT=3
* fair share" reasoning capsules/hermes/init.4th's COMMON-CH floor already
* uses -- not a new invented number. Application-level pulls
* (stadium_reservoir_pull() called directly, e.g. via STADIUM-RES-PULL) are
* NOT floored -- only word-execution admission respects this ceiling on
* its own consumption.
*/
static uint64_t word_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
/*
* resolve_resident_cell - Self-healing lookup (advisor-flagged reverse
* coherence gap): the map may claim word_id is resident at a cell that was
* actually reclaimed by someone else's stadium_admit() eviction fallback
* since the map was last written. Detected here, lazily, against ground
* truth already public via stadium_cells()/stadium_header_bitmap() --
* no new coupling from stadium.c into this file. A stale mapping is cleared
* and counted as an eviction on discovery.
*/
static size_t resolve_resident_cell(int slot, uint32_t word_id) {
size_t cell = word_slots[slot][word_id].cell_index;
StadiumCell *cells;
if (cell == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
if (cell >= stadium_cell_count() || !cell_is_resident(cell)) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
cells = stadium_cells();
if (cells[cell].header.identity != (uint64_t)word_id) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
return cell;
}
uint64_t stadium_words_resident_heat(VMUuid vm_id) {
int slot;
uint32_t i;
uint64_t sum = 0;
StadiumCell *cells;
if (!words_initialized) return 0;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
cells = stadium_cells();
for (i = 0; i < DICTIONARY_SIZE; i++) {
size_t cell = resolve_resident_cell(slot, i);
if (cell == STADIUM_CELL_NONE) continue;
sum += cells[cell].header.heat;
}
return sum;
}
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell != STADIUM_CELL_NONE) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - word_slots[slot][word_id].last_decay_tick;
if (elapsed > 0) {
/* Redirected Loop #3 (§17.7): a FRACTION of the cell's own
* current heat per elapsed tick -- unit-safe for a conserved
* share of 1.0, unlike execution_heat's flat per-tick amount.
* STADIUM_WORD_COOL_RATE_Q48 / 65536 is that fraction. */
uint64_t per_tick = (h->heat * (uint64_t)STADIUM_WORD_COOL_RATE_Q48) >> 16;
uint64_t cooled = per_tick * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
}
h->heat += word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
return;
}
/* Not resident: Option B starter-grant admission (§17.7). execution_heat
* plays no role -- density is decided entirely by the pulled quantum. */
{
uint64_t pulled = word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
size_t idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)word_id;
candidate.heat = pulled;
candidate.ttl = 0;
candidate.link = 0; /* unused for word patrons; no continuation/consumer yet */
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0; /* unpinned -- words carry no pin exception (§17.3) */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_id, &candidate);
if (idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
return;
}
word_slots[slot][word_id].cell_index = idx;
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell == STADIUM_CELL_NONE) return;
if (stadium_evict(cell) == 0) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
}
}
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions) {
int slot = stadium_quota_slot_for_vm(vm_id);
if (promotions) *promotions = (slot >= 0) ? stat_promotions[slot] : 0;
if (evictions) *evictions = (slot >= 0) ? stat_evictions[slot] : 0;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small utility stadium.c already duplicates locally. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
void stadium_words_print_boot_diagnostics(VMUuid vm_id) {
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum;
uint64_t reservoir;
stadium_words_stats(vm_id, &promotions, &evictions);
/* item 4.2 fix (FABRIC-0.md §25.5): filtered per-VM -- with two VMs
* holding quotas, summing every resident cell regardless of owner
* (the pre-4.2 behavior) mixed both VMs' conservation totals together. */
resident_sum = stadium_resident_sum(vm_id);
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium words: promotions=");
console_put_u64(promotions);
console_puts(" evictions=");
console_put_u64(evictions);
console_println("");
console_puts("Stadium conservation: resident_sum=");
console_put_u64(resident_sum);
console_puts(" reservoir=");
console_put_u64(reservoir);
console_puts(" sum=");
console_put_u64(resident_sum + reservoir);
console_puts(" (Q48_ONE=");
console_put_u64((uint64_t)Q48_ONE);
console_println(")");
console_puts("Stadium conservation: ");
console_println(stadium_conserved(vm_id) ? "CONSERVED" : "DRIFTED");
}
#endif /* __STARKERNEL__ */