Files
LithosAnanake/src/starkernel/vm/stadium.c
T
Robert Allan JamesandClaude Sonnet 5 3d0b9351bd starkernel: item 4.1 -- hot words onto the Stadium, density-ranked eviction
Punch list §25 item 4.1 complete.
Replaces the round-robin hotwords cache with Stadium density-ranked
admission/eviction on the kernel side, via the §17.7 reservoir mechanism and a
kernel-side word_id -> cell_index map (no DictEntry change, dict_hash
untouched). Adds stadium_birth_hera() to close the cell-0 panic hazard,
STADIUM_WORD_HEAT_QUANTUM/STADIUM_WORD_COOL_RATE_Q48 Kconfig knobs (flagged
untuned), and a stadium_word_forget() FORGET coherence hook to close a
recycled-word_id aliasing gap.

Verified: all five hotwords_cache_* call sites in dictionary_management.c
bypassed under __STARKERNEL__; word dispatch feeds the Stadium at all three
vm_core.c physics_execution_heat_increment() sites; hosted make unaffected;
all three architectures booted to ok> with matching dict_hash
(0x3d4e1daf289da94f) and matching conservation stats (promotions=354
evictions=0, resident_sum=65536 reservoir=0 sum=65536).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 13:37:10 -04:00

461 lines
16 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) 20232025 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.c - The Stadium cell definitions (item 3.1) and boot-time
* allocation (item 3.2).
*
* Also the translation unit that makes stadium.h's compile-time assertions
* actually get compiled on all three architectures, not merely included by
* something that never builds.
*/
#include "starkernel/vm/stadium.h"
#ifdef __STARKERNEL__
#include "starkernel/kmalloc.h"
#include "starkernel/pmm.h"
#include "starkernel/console.h"
#include "starkernel/hal/hal.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- item 4.1's reservoir starts each VM's quota at 1.0 */
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
static uint8_t *stadium_bitmap = (uint8_t *)0;
static uint8_t *stadium_owner = (uint8_t *)0;
static size_t stadium_ncells = 0;
static int stadium_initialized = 0;
/* Sentinel for the header's `link` field while it is reused as a free-list
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
* uint32_t truncates to the same bit pattern as this constant (safe), but
* casting THIS constant back up to size_t does not sign-extend to
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
* size_to_link() conversions below rather than a raw cast either direction. */
#define STADIUM_LINK_NONE ((uint32_t)-1)
static uint32_t size_to_link(size_t v) {
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
}
static size_t link_to_size(uint32_t v) {
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
}
/*
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC.md §22.3).
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
* rather than direct indexing.
*/
typedef struct {
VMUuid vm_id;
int in_use;
size_t free_head;
uint64_t reservoir; /* item 4.1, FABRIC.md §17.7 -- Q48.16, heat this VM's
* quota holds but no resident patron has claimed.
* Invariant: Σ(resident patron heat) + reservoir ==
* Q48_ONE, checked the same way vm_physics_conserved()
* checks the fleet sum. */
} StadiumVMQuota;
static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT];
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
static int quota_slot_for_vm(VMUuid vm_id) {
int i;
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
}
return -1;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading zeros. */
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]);
}
int stadium_boot_init(void) {
pmm_stats_t pmm = pmm_get_stats();
uint64_t budget_bytes = (pmm.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
size_t ncells = (size_t)(budget_bytes / STADIUM_CELL_BYTES);
size_t bitmap_bytes = (ncells + 7u) / 8u;
if (ncells == 0) {
console_println("Stadium: boot-time allocation skipped (0 cells from memory budget)");
return -1;
}
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
uint8_t *owner = (uint8_t *)kmalloc(ncells);
if (!cells || !bitmap || !owner) {
console_println("Stadium: kmalloc failed for boot-time allocation");
if (cells) kfree(cells);
if (bitmap) kfree(bitmap);
if (owner) kfree(owner);
return -1;
}
{
uint8_t *raw = (uint8_t *)cells;
size_t n = ncells * STADIUM_CELL_BYTES;
size_t i;
for (i = 0; i < n; i++) raw[i] = 0;
}
{
size_t i;
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
}
/* Item 3.7: chain every cell into one free list, ascending index order
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
* cell's own `link` field as the next-free-cell pointer while
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
{
size_t i;
for (i = 0; i < ncells; i++) {
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
cells[i].header.contains = STADIUM_CONTAINS_NONE;
owner[i] = 0;
}
}
{
int i;
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
stadium_quotas[i].vm_id = vm_uuid_none();
stadium_quotas[i].in_use = 0;
stadium_quotas[i].free_head = STADIUM_CELL_NONE;
stadium_quotas[i].reservoir = 0;
}
}
stadium_quotas[0].vm_id = vm_uuid_hera();
stadium_quotas[0].in_use = 1;
stadium_quotas[0].free_head = 0;
/* item 4.1, §17.7: at quota-grant time, before any resident patron
* exists, the reservoir holds the VM's entire conserved share -- mirrors
* Hera holding the fleet's whole Q48_ONE before any other VM is born. */
stadium_quotas[0].reservoir = Q48_ONE;
stadium_cell_array = cells;
stadium_bitmap = bitmap;
stadium_owner = owner;
stadium_ncells = ncells;
stadium_initialized = 1;
console_puts("Stadium: ");
console_put_u64((uint64_t)ncells);
console_puts(" cells (");
console_put_u64((uint64_t)(ncells * STADIUM_CELL_BYTES) / 1024u);
console_println(" KB)");
return 0;
}
int stadium_is_initialized(void) {
return stadium_initialized;
}
size_t stadium_cell_count(void) {
return stadium_ncells;
}
StadiumCell *stadium_cells(void) {
return stadium_cell_array;
}
uint8_t *stadium_header_bitmap(void) {
return stadium_bitmap;
}
/*
* Exhaustive switch, no default: §13/§18.3 require the behaviour set to be
* closed and fixed at build time. With -Wall -Werror, omitting a case here
* for a tag that exists is a build failure, not a silent gap -- the compiler
* enforces closedness, not just this comment.
*
* Handlers are stubs: the real actions belong to subsystems not yet migrated
* onto the Stadium (Phase 4, §25.5). Nothing calls this yet either -- item
* 3.5 is the first consumer.
*/
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour) {
console_puts("Stadium: dispatch cell=");
console_put_u64((uint64_t)cell_index);
console_puts(" behaviour=");
switch (behaviour) {
case STADIUM_BEHAVIOUR_MIGRATE:
console_println("MIGRATE (stub)");
break;
case STADIUM_BEHAVIOUR_DELIVER:
console_println("DELIVER (stub)");
break;
case STADIUM_BEHAVIOUR_EXPIRE:
console_println("EXPIRE (stub)");
break;
case STADIUM_BEHAVIOUR_COOL:
console_println("COOL (stub)");
break;
}
}
/*
* FABRIC.md §19.2/§19.3: density is heat / mass, read on demand from fields
* already in the header -- not a value a scheduler maintains. mass == 0
* (an empty or never-admitted slot; everything is zero-initialized until
* something is actually born into the Stadium, which nothing yet does)
* returns 0 rather than dividing by zero. An out-of-range cell_index also
* returns 0 -- there is no patron there to be dense.
*/
uint64_t stadium_density(size_t cell_index) {
StadiumPatronHeader *header;
if (cell_index >= stadium_ncells) return 0;
header = &stadium_cell_array[cell_index].header;
if (header->mass == 0) return 0;
return header->heat / (uint64_t)header->mass;
}
#define STADIUM_FLAG_PIN 0x01u
static int bitmap_get(size_t cell_index) {
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
}
static void bitmap_set(size_t cell_index) {
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
}
static void bitmap_clear(size_t cell_index) {
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
}
/*
* FABRIC.md §17.2: reap means leaves the floor, not destroyed. Refuses a
* pinned header (§3) or one with a non-none `contains` (item 1.1: a patron
* holding another cannot be reaped). Refuses an out-of-range index or a cell
* whose discriminator bit is not set -- nothing resident there to reap.
*
* §20.5 #3 / item 3.6: panics, does not return, if a resident cell 0 (Hera,
* patron zero) is ever selected -- checked before pin/contains below, on
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
* via the ordinary refusal path instead of surfacing it.
*/
int stadium_evict(size_t cell_index) {
StadiumPatronHeader *header;
uint8_t slot;
if (cell_index >= stadium_ncells) return -1;
if (!bitmap_get(cell_index)) return -1;
if (cell_index == STADIUM_HERA_CELL_INDEX) {
sk_hal_panic("Stadium: eviction selected patron zero (Hera) -- governor invariant broken");
}
header = &stadium_cell_array[cell_index].header;
if (header->flags & STADIUM_FLAG_PIN) return -1;
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
bitmap_clear(cell_index);
slot = stadium_owner[cell_index];
/* item 4.1, §17.7: the departing patron's remaining heat must flow back
* to its owner's reservoir before the cell returns to the free list, or
* every reap leaks heat and Σ(resident) + reservoir drifts below
* Q48_ONE. Captured BEFORE the zero-fill below, which would otherwise
* destroy it. */
stadium_quotas[slot].reservoir += header->heat;
{
uint8_t *raw = (uint8_t *)header;
size_t i;
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
}
/* Item 3.7: return the freed cell to its owning VM's free list. contains
* must be set to the real "none" sentinel here, not left at the zero
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
* would make this freed cell look permanently "contains Hera" to the
* very next admit() that pops it. */
header->link = size_to_link(stadium_quotas[slot].free_head);
header->contains = STADIUM_CONTAINS_NONE;
stadium_quotas[slot].free_head = cell_index;
return 0;
}
/*
* FABRIC.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
* rule only governs the full case). Only if that list is empty does this
* fall back to eviction, scoped to that SAME VM's own residents (quota
* isolation), finding the least-dense evictable one (pinned and
* contains-gated residents are skipped, never eviction candidates) and
* evicting it only if the candidate is strictly denser.
*/
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
int slot;
size_t i;
size_t idx;
size_t least_dense_index = STADIUM_CELL_NONE;
uint64_t least_dense_value = 0;
uint64_t candidate_density;
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
/* Multi-cell patrons need a continuation chain, and no header field is
* documented as carrying one's first index -- see this function's doc
* in stadium.h. Refuse rather than admit only the header and leak the
* rest, which would break capacity conservation. */
if (candidate->mass != 1) return STADIUM_CELL_NONE;
/* contains must be a real "none" or a real index -- catches garbage/
* uninitialized values, though not a zero-initialized candidate that
* meant "none": 0 is Hera's valid index, so that case is a caller
* contract issue this function cannot detect (see the header doc). */
if (candidate->contains != STADIUM_CONTAINS_NONE &&
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
slot = quota_slot_for_vm(vm_id);
if (slot < 0) return STADIUM_CELL_NONE;
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
return idx;
}
for (i = 0; i < stadium_ncells; i++) {
StadiumPatronHeader *h;
if (!bitmap_get(i)) continue;
if (stadium_owner[i] != (uint8_t)slot) continue;
h = &stadium_cell_array[i].header;
if (h->flags & STADIUM_FLAG_PIN) continue;
if (h->contains != STADIUM_CONTAINS_NONE) continue;
{
uint64_t d = stadium_density(i);
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
least_dense_index = i;
least_dense_value = d;
}
}
}
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
* list -- its owner is `slot`, the same quota we scoped the search to.
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
* yet built), so nothing else can have touched the list meanwhile; pop
* it straight back off. */
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
return idx;
}
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
uint64_t pulled;
if (slot < 0) return 0;
pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
stadium_quotas[slot].reservoir -= pulled;
return pulled;
}
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return;
stadium_quotas[slot].reservoir += amount;
}
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
return stadium_quotas[slot].reservoir;
}
/*
* FABRIC.md item 3.6 / item 4.1: see stadium.h's doc. Idempotent via the
* item-3.1 discriminator bitmap -- if cell 0 already reads as resident,
* something already birthed her (or, if it isn't actually Hera, something
* else already claimed cell 0 -- either way this must not clobber it).
*/
int stadium_birth_hera(void) {
StadiumPatronHeader candidate;
size_t idx;
if (!stadium_initialized) return -1;
if (stadium_ncells > 0 && bitmap_get(STADIUM_HERA_CELL_INDEX)) return 0;
{
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
}
candidate.identity = 0;
candidate.heat = 0;
candidate.ttl = 0;
candidate.link = STADIUM_LINK_NONE;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = STADIUM_FLAG_PIN;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_uuid_hera(), &candidate);
if (idx == STADIUM_CELL_NONE) return -1; /* refused; should not happen (quota is fresh and empty) */
if (idx != STADIUM_HERA_CELL_INDEX) {
sk_hal_panic("Stadium: birth_hera did not land on cell 0 -- patron-zero invariant broken");
}
return 0;
}
#endif /* __STARKERNEL__ */