starkernel: item 4.2 -- Hermes native on the Stadium (complete)
Migrates Hermes's message/channel lifecycle onto the Stadium's unified heat/capacity economy: MSG-ALLOC/FREE-NODE and CH-ALLOC/FREE-NODE now route entirely through stadium_admit()/stadium_evict(), replacing the old local free-list + independent heat-field mechanism. Eight kernel-only STADIUM-* FORTH primitives (ADMIT, EVICT, RES@, RES-PULL, RES-PUSH, HEAT@, HEAT!, WORD-HEAT), VM.stadium_vm_id threaded through all three vm_core.c dispatch sites (replacing item 4.1's hardcoded vm_uuid_hera()), and the stadium_owner[idx] fix so evict-credit lands in the VM that actually admitted a patron, not whoever owned cell 0. This session's own contribution, on top of that pre-existing implementation: found and fixed two bugs blocking the item's own K≡1.0 conservation self-check (HERMES-K was reading 0, not 65536): - Q.SLOT admission-heat fix (capsules/hermes/init.4th): MSG-SEND/ CH-ACCEPT admitted with Q.1 (the entire fleet-wide "1.0" unit) per item, a leftover from before the Stadium migration when each message/channel had its own unconstrained heat field. Instantly drained the shared, finite reservoir. - Reservoir floor for word-execution admission (stadium_words.c): stadium_word_dispatch() (item 4.1) pulls STADIUM_WORD_HEAT_QUANTUM on every word dispatch, not just first admission -- exhausts a VM's entire reservoir in ~32 dispatches, starving any application-level economy sharing that VM's reservoir before it gets a chance to pull anything. word_dispatch_pull() now clamps word-execution's own pulls to leave a Q48_ONE/3 floor (same fair-share figure COMMON-CH's own floor already uses); application-level pulls are unaffected. - STADIUM-WORD-HEAT primitive + stadium_words_resident_heat(): the floor deliberately leaves word-execution residents holding real heat, invisible to HERMES-K's original formula (MSG+CH+reservoir, no term for word patrons). Adding this term closes K to exactly 65536 on all three architectures. Also rules on two open scope questions in FABRIC.md: MBR-ALLOC/ MBR-FREE-NODE stay off the Stadium (membership records have no heat field, never did -- the acceptance bullet's inclusion of them was a completeness gesture predating a check of the actual layout), and records the effort number (12 implementation files, +759/-120 lines). Verified: all three architectures boot clean, full self-test passes, Stadium conservation closes exactly (resident_sum + reservoir = Q48_ONE) at both the C/Stadium level and the FORTH-level HERMES-K check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0a7f144367
commit
5a28458b21
@@ -135,7 +135,7 @@ void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
|
||||
* is recycled below -- otherwise the next word assigned this same id
|
||||
* would alias onto the forgotten word's stale cell (same failure class
|
||||
* as the 2026-08-02 block_words.c aliasing bug). */
|
||||
stadium_word_forget(word_id);
|
||||
stadium_word_forget(vm->stadium_vm_id, word_id);
|
||||
#endif
|
||||
|
||||
if (vm->word_id_map[word_id] == entry) {
|
||||
|
||||
@@ -475,6 +475,11 @@ CapsuleRunResult capsule_birth_baby(
|
||||
return CAPSULE_RUN_ERR_STILLBORN;
|
||||
}
|
||||
|
||||
/* item 4.2: set before the IDENTITY exec below, so any word this baby
|
||||
* dispatches during her own init capsule already attributes heat to her
|
||||
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
|
||||
((VM *)new_vm)->stadium_vm_id = vm_id;
|
||||
|
||||
const uint8_t *payload = capsule_get_payload(cap, arena);
|
||||
if (!payload) {
|
||||
entry->state = VM_STATE_STILLBORN;
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
#include "starkernel/capsule_loader.h"
|
||||
#include "starkernel/capsule_vm_physics.h"
|
||||
#include "starkernel/vm/vm_internal.h"
|
||||
#include "starkernel/vm/stadium.h" /* item 4.2 -- STADIUM-* words */
|
||||
#include "starkernel/vm/stadium_words.h" /* item 4.2 -- stadium_words_resident_heat() */
|
||||
#include "starkernel/repl.h"
|
||||
#include "starkernel/capsule_generated.h"
|
||||
#include "starkernel/console.h"
|
||||
@@ -1156,16 +1158,205 @@ void register_mama_forth_words(VM *vm)
|
||||
vocabulary_word_definitions(vm);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Stadium Words (FABRIC.md punch list item 4.2)
|
||||
*
|
||||
* The entire C surface item 4.2 is permitted to add, per HERMES.md's
|
||||
* language constraint: all eight operate on the CALLING VM's own identity
|
||||
* (vm->stadium_vm_id) implicitly, never a FORTH-supplied vm-id. A
|
||||
* stack-passed vm-id could only ever be the caller's own (redundant) or
|
||||
* another VM's (stadium_admit()/stadium_evict() would refuse it via quota
|
||||
* isolation, except STADIUM-RES-PUSH, which has no such guard and would be
|
||||
* an outright heat-forgery primitive against another VM's reservoir).
|
||||
* Conservation is the invariant this item is verified against, so implicit
|
||||
* self is not an optimization -- it's the only version that can't break it.
|
||||
*
|
||||
* STADIUM-ADMIT does not itself pull the candidate's heat from the
|
||||
* reservoir -- that's STADIUM-RES-PULL's job, composed in FORTH by the
|
||||
* caller (e.g. a rewritten MSG-ALLOC): pull first, admit with the pulled
|
||||
* amount, and STADIUM-RES-PUSH it back if admission refuses. Mirrors
|
||||
* stadium_words.c's C-side Option B starter-grant pattern, but the
|
||||
* composition itself lives in StarForth, not here, per HERMES.md.
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* @brief STADIUM-ADMIT ( identity heat behaviour -- cell | -1 )
|
||||
* Admits a mass-1 patron into the calling VM's own Stadium quota.
|
||||
* `behaviour` must be a valid StadiumBehaviour tag (0..3); anything else
|
||||
* refuses without calling stadium_admit() at all. `contains` is always
|
||||
* explicitly STADIUM_CONTAINS_NONE -- stadium_admit()'s own doc warns a
|
||||
* zero-initialized `contains` reads as "contains Hera" (index 0) and
|
||||
* permanently blocks eviction, so this is never left to a zero-fill.
|
||||
*/
|
||||
static void mama_word_stadium_admit(VM *vm)
|
||||
{
|
||||
cell_t behaviour_cell, heat_cell, identity_cell;
|
||||
StadiumPatronHeader candidate;
|
||||
uint8_t *raw = (uint8_t *)&candidate;
|
||||
size_t i;
|
||||
size_t idx;
|
||||
|
||||
if (vm->dsp < 2) { vm->error = 1; return; }
|
||||
|
||||
behaviour_cell = vm_pop(vm);
|
||||
heat_cell = vm_pop(vm);
|
||||
identity_cell = vm_pop(vm);
|
||||
|
||||
if (behaviour_cell < STADIUM_BEHAVIOUR_MIGRATE || behaviour_cell > STADIUM_BEHAVIOUR_COOL) {
|
||||
vm_push(vm, (cell_t)-1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
|
||||
candidate.identity = (uint64_t)identity_cell;
|
||||
candidate.heat = (uint64_t)heat_cell;
|
||||
candidate.ttl = 0;
|
||||
candidate.link = 0;
|
||||
candidate.contains = STADIUM_CONTAINS_NONE;
|
||||
candidate.mass = 1;
|
||||
candidate.flags = 0;
|
||||
candidate.behaviour = (uint8_t)behaviour_cell;
|
||||
|
||||
idx = stadium_admit(vm->stadium_vm_id, &candidate);
|
||||
vm_push(vm, (idx == STADIUM_CELL_NONE) ? (cell_t)-1 : (cell_t)idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-EVICT ( cell -- flag )
|
||||
* Reaps the patron header at `cell`. flag is FORTH true (-1) on success,
|
||||
* false (0) if refused (out of range, not resident, pinned, or contains-
|
||||
* gated) -- stadium_evict()'s own refusal set, unchanged here.
|
||||
*/
|
||||
static void mama_word_stadium_evict(VM *vm)
|
||||
{
|
||||
cell_t cell_cell;
|
||||
|
||||
if (vm->dsp < 0) { vm->error = 1; return; }
|
||||
|
||||
cell_cell = vm_pop(vm);
|
||||
if (cell_cell < 0) {
|
||||
vm_push(vm, (cell_t)0);
|
||||
return;
|
||||
}
|
||||
vm_push(vm, (stadium_evict((size_t)cell_cell) == 0) ? (cell_t)-1 : (cell_t)0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-RES@ ( -- heat )
|
||||
* Read-only peek at the calling VM's own reservoir balance (Q48.16).
|
||||
*/
|
||||
static void mama_word_stadium_res_fetch(VM *vm)
|
||||
{
|
||||
vm_push(vm, (cell_t)stadium_reservoir_peek(vm->stadium_vm_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-WORD-HEAT ( -- heat )
|
||||
* Sum of heat held by the calling VM's own word-execution residents
|
||||
* (item 4.1's cells) -- the term a VM's own application-level conservation
|
||||
* check (e.g. Hermes's HERMES-K) needs to close exactly, since word patrons
|
||||
* are otherwise invisible to FORTH (FABRIC.md §25.7, ruling 2026-08-06).
|
||||
*/
|
||||
static void mama_word_stadium_word_heat(VM *vm)
|
||||
{
|
||||
vm_push(vm, (cell_t)stadium_words_resident_heat(vm->stadium_vm_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-RES-PULL ( qty -- heat )
|
||||
* Pulls up to `qty` (Q48.16) from the calling VM's own reservoir. Returns
|
||||
* the amount actually pulled, which may be less than requested -- never
|
||||
* negative, never invents heat, mirrors stadium_reservoir_pull()'s own
|
||||
* clamping exactly.
|
||||
*/
|
||||
static void mama_word_stadium_res_pull(VM *vm)
|
||||
{
|
||||
cell_t qty_cell;
|
||||
|
||||
if (vm->dsp < 0) { vm->error = 1; return; }
|
||||
|
||||
qty_cell = vm_pop(vm);
|
||||
if (qty_cell < 0) {
|
||||
vm_push(vm, (cell_t)0);
|
||||
return;
|
||||
}
|
||||
vm_push(vm, (cell_t)stadium_reservoir_pull(vm->stadium_vm_id, (uint64_t)qty_cell));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-RES-PUSH ( heat -- )
|
||||
* Credits `heat` (Q48.16) back into the calling VM's own reservoir -- the
|
||||
* other half of every reservoir transfer (cooling, refused-admission
|
||||
* rollback, or a departing patron's remaining heat after eviction).
|
||||
*/
|
||||
static void mama_word_stadium_res_push(VM *vm)
|
||||
{
|
||||
cell_t heat_cell;
|
||||
|
||||
if (vm->dsp < 0) { vm->error = 1; return; }
|
||||
|
||||
heat_cell = vm_pop(vm);
|
||||
if (heat_cell < 0) return;
|
||||
stadium_reservoir_push(vm->stadium_vm_id, (uint64_t)heat_cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-HEAT@ ( cell -- heat )
|
||||
* Reads a resident cell's own heat. Requires the cell to be resident and
|
||||
* owned by the calling VM's own quota -- returns 0 otherwise (out of range,
|
||||
* not resident, or belongs to a different VM).
|
||||
*/
|
||||
static void mama_word_stadium_heat_fetch(VM *vm)
|
||||
{
|
||||
cell_t cell_cell;
|
||||
|
||||
if (vm->dsp < 0) { vm->error = 1; return; }
|
||||
|
||||
cell_cell = vm_pop(vm);
|
||||
if (cell_cell < 0) {
|
||||
vm_push(vm, (cell_t)0);
|
||||
return;
|
||||
}
|
||||
vm_push(vm, (cell_t)stadium_cell_heat_get(vm->stadium_vm_id, (size_t)cell_cell));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief STADIUM-HEAT! ( new-heat cell -- )
|
||||
* Writes a resident cell's own heat, reconciling the reservoir delta
|
||||
* atomically in C (pulls on an increase, refusing silently if the
|
||||
* calling VM's reservoir can't cover it; pushes back on a decrease).
|
||||
* Requires the cell to be resident and owned by the calling VM's own
|
||||
* quota -- silently refused otherwise, same as every other write here.
|
||||
*/
|
||||
static void mama_word_stadium_heat_store(VM *vm)
|
||||
{
|
||||
cell_t cell_cell, new_heat_cell;
|
||||
|
||||
if (vm->dsp < 1) { vm->error = 1; return; }
|
||||
|
||||
cell_cell = vm_pop(vm);
|
||||
new_heat_cell = vm_pop(vm);
|
||||
if (cell_cell < 0 || new_heat_cell < 0) return;
|
||||
(void)stadium_cell_heat_set(vm->stadium_vm_id, (size_t)cell_cell, (uint64_t)new_heat_cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* register_child_vm_words - Register the minimal word set needed by child VMs.
|
||||
*
|
||||
* Child VMs are not bootstrapped through sk_vm_bootstrap_parity, so they
|
||||
* do not get register_mama_forth_words(). They only need STOP (self-halt)
|
||||
* and EXEC (load a capsule). Keeping the registrations here — in the same
|
||||
* translation unit as the word functions — avoids cross-TU function-pointer
|
||||
* loads that produce R_X86_64_REX_GOTPCRELX relocations; those are not
|
||||
* relaxed by the PE32+ linker, causing the function code bytes to be read
|
||||
* as the pointer value instead of the actual address.
|
||||
* and EXEC (load a capsule) -- plus, as of item 4.2, the eight STADIUM-*
|
||||
* primitives Hermes needs to migrate her message/channel lifecycle onto the
|
||||
* Stadium. Keeping the registrations here — in the same translation unit
|
||||
* as the word functions — avoids cross-TU function-pointer loads that
|
||||
* produce R_X86_64_REX_GOTPCRELX relocations; those are not relaxed by the
|
||||
* PE32+ linker, causing the function code bytes to be read as the pointer
|
||||
* value instead of the actual address.
|
||||
*
|
||||
* Deliberately NOT added to register_mama_forth_words(): that would put
|
||||
* these words in Hera's own dictionary too and move dict_hash off item
|
||||
* 4.1's baseline (0x3d4e1daf289da94f) -- a deliberate baseline change to
|
||||
* state this item does not make as a side effect.
|
||||
*/
|
||||
void register_child_vm_words(VM *vm)
|
||||
{
|
||||
@@ -1173,6 +1364,14 @@ void register_child_vm_words(VM *vm)
|
||||
register_word(vm, "EXEC", mama_word_exec);
|
||||
register_word(vm, "VM-EXEC", mama_word_vm_exec);
|
||||
register_word(vm, "VM-CALL", mama_word_vm_call);
|
||||
register_word(vm, "STADIUM-ADMIT", mama_word_stadium_admit);
|
||||
register_word(vm, "STADIUM-EVICT", mama_word_stadium_evict);
|
||||
register_word(vm, "STADIUM-RES@", mama_word_stadium_res_fetch);
|
||||
register_word(vm, "STADIUM-RES-PULL", mama_word_stadium_res_pull);
|
||||
register_word(vm, "STADIUM-RES-PUSH", mama_word_stadium_res_push);
|
||||
register_word(vm, "STADIUM-HEAT@", mama_word_stadium_heat_fetch);
|
||||
register_word(vm, "STADIUM-HEAT!", mama_word_stadium_heat_store);
|
||||
register_word(vm, "STADIUM-WORD-HEAT", mama_word_stadium_word_heat);
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
@@ -165,6 +165,30 @@ static void print_uint(const char *label, uint64_t value) {
|
||||
console_println(buf);
|
||||
}
|
||||
|
||||
#ifdef STARFORTH_ENABLE_VM
|
||||
/**
|
||||
* @brief Verify a known word is still reachable in a VM's dictionary.
|
||||
*
|
||||
* Diagnostic-only: confirms @c vm_find_word() can still walk the chain
|
||||
* from @c vm->latest to a word defined early in Hermes's capsule
|
||||
* (MSG-COOL-ALL, block 4108). Added after item 4.2's self-test bisection
|
||||
* found an amd64-only in-place corruption of an existing dictionary entry
|
||||
* during MSG-DELIVER-ALL -- here/latest stay unchanged (no reallocation),
|
||||
* so a lookup is a cheap signal. NOTE: an earlier version of this comment
|
||||
* attributed the corruption to GDB perturbing execution timing; that is
|
||||
* unconfirmed and more likely just a parity/dict-hash boot-gate failure
|
||||
* triggered by the debugger session itself (a software breakpoint's 0xCC
|
||||
* patch landing in memory the loader then overwrote) -- don't propagate
|
||||
* "timing-sensitive" as an established finding.
|
||||
*/
|
||||
static void hermes_dict_check(VM *hermes_vm, const char *checkpoint) {
|
||||
DictEntry *e = vm_find_word(hermes_vm, "MSG-COOL-ALL", 12);
|
||||
console_puts(" Dict-check ");
|
||||
console_puts(checkpoint);
|
||||
console_println(e ? ": OK" : ": FAIL (MSG-COOL-ALL unreachable)");
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Print a boot-information summary from the UEFI memory map to the console.
|
||||
*
|
||||
@@ -648,6 +672,101 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
}
|
||||
}
|
||||
|
||||
/* item 4.2 self-test: a REAL birth (not synthetic, unlike 4.1a's --
|
||||
* this exercises capsules/hermes/init.4th's actual migrated code),
|
||||
* exercised then killed again so the resting boot state stays
|
||||
* Hera-alone, per item 0.1's intent. Diagnostic only -- production
|
||||
* boot still never auto-births Hermes (init.4th's BIRTH stays
|
||||
* commented out). */
|
||||
console_println("Hermes 4.2 migration self-test: birthing...");
|
||||
vm_interpret(mama, "S\" Hermes\" BIRTH");
|
||||
{
|
||||
VMRegistryEntry entry;
|
||||
if (capsule_vm_find_by_name_nocase("Hermes", &entry) == 0 &&
|
||||
entry.state == VM_STATE_LIVE) {
|
||||
VM *hermes_vm = (VM *)entry.vm_ptr;
|
||||
console_println("Hermes 4.2 self-test: exercising migrated words...");
|
||||
hermes_dict_check(hermes_vm, "at self-test start");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "CD-INIT");
|
||||
print_uint(" DBG err after CD-INIT=", (uint64_t)hermes_vm->error);
|
||||
hermes_dict_check(hermes_vm, "after CD-INIT");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "HERMES-MSG-TEST . CR");
|
||||
print_uint(" DBG err after MSG-TEST=", (uint64_t)hermes_vm->error);
|
||||
hermes_dict_check(hermes_vm, "after MSG-TEST");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "HERMES-STATUS");
|
||||
print_uint(" DBG err after STATUS=", (uint64_t)hermes_vm->error);
|
||||
hermes_dict_check(hermes_vm, "after STATUS");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "MSG-DELIVER-ALL");
|
||||
print_uint(" DBG err after DELIVER-ALL=", (uint64_t)hermes_vm->error);
|
||||
hermes_dict_check(hermes_vm, "after DELIVER-ALL");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "MSG-REDELIVER-NACKED");
|
||||
print_uint(" DBG err after REDELIVER=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "MSG-COOL-ALL");
|
||||
print_uint(" DBG err after MSG-COOL-ALL=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "MSG-REAP");
|
||||
print_uint(" DBG err after MSG-REAP=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "CH-COOL-ALL CH-REAP-SAFE");
|
||||
print_uint(" DBG err after CH-COOL/REAP=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "Q.1 3 / COMMON-CH @ CH-HEAT!");
|
||||
print_uint(" DBG err after CH-HEAT!=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "HERMES-STATUS");
|
||||
print_uint(" DBG err after TICK=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
console_puts("Hermes 4.2 self-test: HERMES-K=");
|
||||
vm_interpret(hermes_vm, "HERMES-K .");
|
||||
console_println("");
|
||||
print_uint(" DBG err after HERMES-K=", (uint64_t)hermes_vm->error);
|
||||
/* Side-by-side with the Stadium's own view, same checkpoint --
|
||||
* if these two disagree, HERMES-K's FORTH-side arena scan is
|
||||
* seeing different residents than stadium_resident_sum()'s
|
||||
* ownership+bitmap view (a real, separate finding, not the
|
||||
* Q.SLOT admission-heat fix's job to explain). */
|
||||
print_uint(" DBG stadium_resident_sum(Hermes)=", stadium_resident_sum(entry.vm_id));
|
||||
print_uint(" DBG stadium_reservoir_peek(Hermes)=", stadium_reservoir_peek(entry.vm_id));
|
||||
/* item 4.2 Done-when: "a resident cell's evict-credit landing
|
||||
* in the correct VM's reservoir ... not just asserted from
|
||||
* reading the code" -- explicitly evict the common channel
|
||||
* (CH-FREE-NODE -> STADIUM-EVICT), a known resident from
|
||||
* CD-INIT's own COMMON-INIT, same credit path stadium_admit()'s
|
||||
* density-fallback eviction uses. resident_sum dropping and
|
||||
* reservoir rising by the same amount is the proof: the freed
|
||||
* cell's heat landed back in Hermes's own reservoir, not lost
|
||||
* or credited to Hera. */
|
||||
console_println("Hermes 4.2 self-test: before eviction:");
|
||||
print_uint(" Hermes resident_sum=", stadium_resident_sum(entry.vm_id));
|
||||
print_uint(" Hermes reservoir=", stadium_reservoir_peek(entry.vm_id));
|
||||
console_println("Hermes 4.2 self-test: forcing an explicit eviction (COMMON-CH)...");
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "COMMON-CH @ . CR");
|
||||
print_uint(" DBG err after COMMON-CH@=", (uint64_t)hermes_vm->error);
|
||||
hermes_vm->error = 0;
|
||||
vm_interpret(hermes_vm, "COMMON-CH @ CH-FREE-NODE");
|
||||
print_uint(" DBG err after CH-FREE-NODE=", (uint64_t)hermes_vm->error);
|
||||
console_println("Hermes 4.2 self-test: after eviction:");
|
||||
print_uint(" Hermes resident_sum=", stadium_resident_sum(entry.vm_id));
|
||||
print_uint(" Hermes reservoir=", stadium_reservoir_peek(entry.vm_id));
|
||||
/* item 4.2 Done-when: both VMs' conservation checks close
|
||||
* independently -- Hermes's own resident+reservoir sum first,
|
||||
* then Hera's again (unaffected by Hermes's activity above). */
|
||||
stadium_words_print_boot_diagnostics(entry.vm_id);
|
||||
vm_interpret(mama, "S\" Hermes\" KILL");
|
||||
console_println("Hermes 4.2 self-test: killed, resting state restored");
|
||||
stadium_words_print_boot_diagnostics(vm_uuid_hera());
|
||||
} else {
|
||||
console_println("Hermes 4.2 self-test: birth registry lookup FAILED");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Runtime --doe flag: inject "EXEC-DOE BYE" if requested via boot args.
|
||||
* Checked before SK_STARTUP_FORTH so a runtime --doe takes precedence.
|
||||
|
||||
@@ -271,6 +271,7 @@ int sk_vm_bootstrap_parity(ParityPacket *out) {
|
||||
* capsule_birth_baby() which never calls vm_init_with_host(). */
|
||||
capsule_vm_hooks_register();
|
||||
capsule_vm_registry_init(vm); /* establishes [Hera] console prefix */
|
||||
vm->stadium_vm_id = vm_uuid_hera(); /* item 4.2 */
|
||||
vm_physics_init(vm_uuid_hera()); /* Hera: the fleet's root, seeded Q48_ONE */
|
||||
capsule_run_log_init();
|
||||
register_mama_forth_words(vm); /* BIRTH KILL START STOP USE + capsule words */
|
||||
|
||||
@@ -358,6 +358,10 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||||
stadium_cell_array[idx].header = *candidate;
|
||||
bitmap_set(idx);
|
||||
/* Item 4.2 fix (§25.7): record ownership so stadium_evict()'s
|
||||
* reservoir credit and free-list return land on the VM that actually
|
||||
* admitted this patron, not whatever owner[idx] held at boot. */
|
||||
stadium_owner[idx] = (uint8_t)slot;
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -395,6 +399,11 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||||
stadium_cell_array[idx].header = *candidate;
|
||||
bitmap_set(idx);
|
||||
/* Same fix as the free-list-pop path above -- stadium_evict() just wrote
|
||||
* owner[idx] = slot as part of reaping least_dense_index, so this is
|
||||
* currently a no-op in practice, but it must not be assumed to stay a
|
||||
* no-op: this is the correctness statement, not a redundant write. */
|
||||
stadium_owner[idx] = (uint8_t)slot;
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -453,6 +462,45 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Shared by stadium_cell_heat_get()/_set(): resident AND owned by vm_id's
|
||||
* own quota slot. Returns the quota slot on success, -1 on any refusal. */
|
||||
static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
|
||||
int slot = quota_slot_for_vm(vm_id);
|
||||
if (slot < 0) return -1;
|
||||
if (cell_index >= stadium_ncells) return -1;
|
||||
if (!bitmap_get(cell_index)) return -1;
|
||||
if (stadium_owner[cell_index] != (uint8_t)slot) return -1;
|
||||
return slot;
|
||||
}
|
||||
|
||||
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
|
||||
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
|
||||
return stadium_cell_array[cell_index].header.heat;
|
||||
}
|
||||
|
||||
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
|
||||
int slot = owned_resident_slot(vm_id, cell_index);
|
||||
uint64_t old_heat, delta, pulled;
|
||||
|
||||
if (slot < 0) return -1;
|
||||
|
||||
old_heat = stadium_cell_array[cell_index].header.heat;
|
||||
if (new_heat == old_heat) return 0;
|
||||
|
||||
if (new_heat > old_heat) {
|
||||
delta = new_heat - old_heat;
|
||||
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
|
||||
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
|
||||
stadium_quotas[slot].reservoir -= pulled;
|
||||
} else {
|
||||
delta = old_heat - new_heat;
|
||||
stadium_quotas[slot].reservoir += delta;
|
||||
}
|
||||
|
||||
stadium_cell_array[cell_index].header.heat = new_heat;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
|
||||
int slot = quota_slot_for_vm(vm_id);
|
||||
uint64_t pulled;
|
||||
@@ -478,6 +526,25 @@ uint64_t stadium_reservoir_peek(VMUuid vm_id) {
|
||||
return stadium_quotas[slot].reservoir;
|
||||
}
|
||||
|
||||
int stadium_quota_slot_for_vm(VMUuid vm_id) {
|
||||
return quota_slot_for_vm(vm_id);
|
||||
}
|
||||
|
||||
uint64_t stadium_resident_sum(VMUuid vm_id) {
|
||||
int slot = quota_slot_for_vm(vm_id);
|
||||
uint64_t sum = 0;
|
||||
size_t i;
|
||||
|
||||
if (slot < 0) return 0;
|
||||
|
||||
for (i = 0; i < stadium_ncells; i++) {
|
||||
if (!bitmap_get(i)) continue;
|
||||
if (stadium_owner[i] != (uint8_t)slot) continue;
|
||||
sum += stadium_cell_array[i].header.heat;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
@@ -40,25 +40,37 @@
|
||||
* 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.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;
|
||||
|
||||
static StadiumWordSlot word_slots[DICTIONARY_SIZE];
|
||||
static StadiumWordSlot word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE];
|
||||
static int words_initialized = 0;
|
||||
static uint64_t stat_promotions = 0;
|
||||
static uint64_t stat_evictions = 0;
|
||||
static uint64_t stat_promotions[STADIUM_MAX_VM_COUNT];
|
||||
static uint64_t stat_evictions[STADIUM_MAX_VM_COUNT];
|
||||
|
||||
void stadium_words_init(void) {
|
||||
int slot;
|
||||
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;
|
||||
for (slot = 0; slot < STADIUM_MAX_VM_COUNT; slot++) {
|
||||
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;
|
||||
}
|
||||
stat_promotions = 0;
|
||||
stat_evictions = 0;
|
||||
words_initialized = 1;
|
||||
}
|
||||
|
||||
@@ -68,6 +80,29 @@ static int cell_is_resident(size_t idx) {
|
||||
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.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
|
||||
@@ -77,36 +112,59 @@ static int cell_is_resident(size_t idx) {
|
||||
* 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;
|
||||
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[word_id].cell_index = STADIUM_CELL_NONE;
|
||||
stat_evictions++;
|
||||
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[word_id].cell_index = STADIUM_CELL_NONE;
|
||||
stat_evictions++;
|
||||
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;
|
||||
|
||||
cell = resolve_resident_cell(word_id);
|
||||
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[word_id].last_decay_tick;
|
||||
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
|
||||
@@ -120,17 +178,17 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
|
||||
h->heat -= cooled;
|
||||
stadium_reservoir_push(vm_id, cooled);
|
||||
}
|
||||
word_slots[word_id].last_decay_tick = heartbeat_ticks;
|
||||
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
|
||||
}
|
||||
|
||||
h->heat += stadium_reservoir_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
|
||||
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 = stadium_reservoir_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_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;
|
||||
@@ -152,30 +210,36 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
|
||||
return;
|
||||
}
|
||||
|
||||
word_slots[word_id].cell_index = idx;
|
||||
word_slots[word_id].last_decay_tick = heartbeat_ticks;
|
||||
stat_promotions++;
|
||||
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(uint32_t word_id) {
|
||||
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;
|
||||
|
||||
cell = resolve_resident_cell(word_id);
|
||||
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[word_id].cell_index = STADIUM_CELL_NONE;
|
||||
stat_evictions++;
|
||||
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
|
||||
stat_evictions[slot]++;
|
||||
}
|
||||
}
|
||||
|
||||
void stadium_words_stats(uint64_t *promotions, uint64_t *evictions) {
|
||||
if (promotions) *promotions = stat_promotions;
|
||||
if (evictions) *evictions = stat_evictions;
|
||||
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
|
||||
@@ -197,19 +261,16 @@ static void console_put_u64(uint64_t v) {
|
||||
|
||||
void stadium_words_print_boot_diagnostics(VMUuid vm_id) {
|
||||
uint64_t promotions = 0, evictions = 0;
|
||||
uint64_t resident_sum = 0;
|
||||
uint64_t resident_sum;
|
||||
uint64_t reservoir;
|
||||
size_t ncells = stadium_cell_count();
|
||||
size_t i;
|
||||
|
||||
stadium_words_stats(&promotions, &evictions);
|
||||
stadium_words_stats(vm_id, &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);
|
||||
/* item 4.2 fix (FABRIC.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);
|
||||
|
||||
@@ -684,9 +684,10 @@ void execute_colon_word(VM* vm)
|
||||
|
||||
/* item 4.1, FABRIC.md §17.7: feed the Stadium's independent
|
||||
* conserved heat wire. execution_heat above is untouched by
|
||||
* this call. vm_uuid_hera() is hardcoded here -- Tripod is
|
||||
* pruned to Hera alone (item 0.1); revisit at item 4.2. */
|
||||
stadium_word_dispatch(vm_uuid_hera(), w->word_id, vm->heartbeat.tick_count);
|
||||
* this call. item 4.2: dispatching VM's own identity, not the
|
||||
* item-4.1 hardcoded vm_uuid_hera() -- refused harmlessly by
|
||||
* stadium_admit() for any VM without a granted quota. */
|
||||
stadium_word_dispatch(vm->stadium_vm_id, w->word_id, vm->heartbeat.tick_count);
|
||||
|
||||
uint32_t word_id = w->word_id;
|
||||
if (word_id < DICTIONARY_SIZE)
|
||||
@@ -881,7 +882,7 @@ void vm_interpret_word(VM* vm, const char* word_str, size_t len)
|
||||
entry->physics.last_decay_ns = lookup_ns;
|
||||
|
||||
physics_execution_heat_increment(entry);
|
||||
stadium_word_dispatch(vm_uuid_hera(), entry->word_id, vm->heartbeat.tick_count);
|
||||
stadium_word_dispatch(vm->stadium_vm_id, entry->word_id, vm->heartbeat.tick_count);
|
||||
if (canon && canon != entry)
|
||||
{
|
||||
/* Apply decay to canonical entry as well */
|
||||
@@ -892,7 +893,7 @@ void vm_interpret_word(VM* vm, const char* word_str, size_t len)
|
||||
canon->physics.last_decay_ns = lookup_ns;
|
||||
|
||||
physics_execution_heat_increment(canon);
|
||||
stadium_word_dispatch(vm_uuid_hera(), canon->word_id, vm->heartbeat.tick_count);
|
||||
stadium_word_dispatch(vm->stadium_vm_id, canon->word_id, vm->heartbeat.tick_count);
|
||||
physics_metadata_touch(canon, canon->execution_heat, lookup_ns);
|
||||
}
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
|
||||
Reference in New Issue
Block a user