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
339 lines
13 KiB
C
339 lines
13 KiB
C
/*
|
||
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__ */ |