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>
This commit is contained in:
Robert Allan James
2026-08-05 13:37:10 -04:00
co-authored by Claude Sonnet 5
parent bd92c57834
commit 3d0b9351bd
19 changed files with 32009 additions and 24 deletions
+231
View File
@@ -0,0 +1,231 @@
/*
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_words.c - Word patrons on the Stadium (FABRIC.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/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.
*/
typedef struct {
size_t cell_index; /* STADIUM_CELL_NONE if not resident */
uint64_t last_decay_tick;
} StadiumWordSlot;
static StadiumWordSlot word_slots[DICTIONARY_SIZE];
static int words_initialized = 0;
static uint64_t stat_promotions = 0;
static uint64_t stat_evictions = 0;
void stadium_words_init(void) {
uint32_t i;
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[i].cell_index = STADIUM_CELL_NONE;
word_slots[i].last_decay_tick = 0;
}
stat_promotions = 0;
stat_evictions = 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;
}
/*
* 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(uint32_t word_id) {
size_t cell = word_slots[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[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
return STADIUM_CELL_NONE;
}
cells = stadium_cells();
if (cells[cell].header.identity != (uint64_t)word_id) {
word_slots[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
return STADIUM_CELL_NONE;
}
return cell;
}
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks) {
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
cell = resolve_resident_cell(word_id);
if (cell != STADIUM_CELL_NONE) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - word_slots[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[word_id].last_decay_tick = heartbeat_ticks;
}
h->heat += stadium_reservoir_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 = stadium_reservoir_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[word_id].cell_index = idx;
word_slots[word_id].last_decay_tick = heartbeat_ticks;
stat_promotions++;
}
}
void stadium_word_forget(uint32_t word_id) {
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
cell = resolve_resident_cell(word_id);
if (cell == STADIUM_CELL_NONE) return;
if (stadium_evict(cell) == 0) {
word_slots[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
}
}
void stadium_words_stats(uint64_t *promotions, uint64_t *evictions) {
if (promotions) *promotions = stat_promotions;
if (evictions) *evictions = stat_evictions;
}
/* 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 = 0;
uint64_t reservoir;
size_t ncells = stadium_cell_count();
size_t i;
stadium_words_stats(&promotions, &evictions);
for (i = 0; i < ncells; i++) {
if (cell_is_resident(i)) {
resident_sum += stadium_cells()[i].header.heat;
}
}
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(")");
}
#endif /* __STARKERNEL__ */