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