Files
LithosAnanake/src/starkernel/vm/stadium_words.c
T
Robert Allan JamesandClaude Sonnet 5 b031b802e3 Rename FABRIC series: FABRIC.md->0, FABRIC-2.md->1, FABRIC-3.md->2, FABRIC-4.md unchanged
FABRIC.md -> FABRIC-0.md
FABRIC-2.md -> FABRIC-1.md
FABRIC-3.md -> FABRIC-2.md (the current/living document)
FABRIC-4.md unchanged (new #3 to follow separately)

Every cross-reference repo-wide updated to match, including doc-comment
citations inside kernel source (.c/.h) files -- done via an ordered
placeholder substitution (FABRIC-3.md->placeholder2, FABRIC-2.md->
placeholder1, FABRIC.md->placeholder0, then placeholders resolved to
final names) in a single pass per file to avoid double-shifting
already-renamed references.

One line in capsules/font.4th grew past the 64-char block-format limit
as a side effect of the longer filename; shortened it and reverified
with mkcapsule --lint (34/34 pass) before rebuilding.

Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the
foreground) after the fix; logs and DoE CSVs from this session's
verification runs included per this repo's own audit-artifact
convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
2026-09-04 11:22:51 -04:00

339 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) 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-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(")");
}
#endif /* __STARKERNEL__ */