Files
LithosAnanake/include/starkernel/vm/stadium.h
T
Robert Allan JamesandClaude Sonnet 5 e55111c2c5 starkernel: item 3.7 -- per-VM free lists (Phase 3 core complete, for real)
Punch list §25 item 3.7 complete. Added to §25.4 after starting item
4.1 surfaced it as an unbuilt prerequisite -- 3.6's earlier "Phase 3
core complete" claim is corrected in this same commit.

StadiumVMQuota table (size STADIUM_MAX_VM_COUNT, linearly searched by
vm_id -- capsule_birth.c's vm_id is monotonic and never reused, so it
cannot index a table directly, and a 4-entry scan costs nothing). New
per-cell stadium_owner byte array records which quota a cell belongs
to, needed so eviction returns a freed cell to the correct VM's list
and so eviction search stays scoped to the evicting VM's own residents
(quota isolation).

Free-list linkage reuses each cell's `link` field as a next-free
pointer while unresident -- link is documented only as generic "index
into the Stadium, not a pointer," so this is a repurposing, not a
header change. Does not answer the separate, still-open question of
which field carries a multi-cell patron's first continuation-cell
index; item 3.5's mass != 1 refusal stands exactly as it was.

Boot-time: every cell chained into one list in ascending index order,
granted whole to vm_id 0 (Hera), the only VM that exists. Ascending
order preserves item 3.6's "Hera is patron zero" invariant once real
birth-wiring lands.

stadium_admit()'s signature changed to take vm_id -- a change to code
shipped in item 3.5, amended there. Pops the calling VM's free-list
head first (O(1)); only falls back to a same-VM-scoped eviction search
if empty.

Caught a real bug before the boot run: the header zero-fill on
eviction (and the initial free-list build) both left contains == 0,
but 0 is Hera's valid index -- the same collision item 3.1's
STADIUM_CONTAINS_NONE fix addressed, recurring at a new site. Fixed by
explicitly setting contains = STADIUM_CONTAINS_NONE at both free-list
sites.

Explicitly out of scope, reported not invented: granting quota to any
VM other than Hera is capacity arbitration (item 1.3 left "how much
moves per transfer" open). stadium_owner is set once at boot and never
rewritten, so quota_slot_for_vm() refuses every vm_id != 0 permanently
until item 4.2 adds the grant path and owner-array writes.

Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.6 baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 18:09:34 -04:00

343 lines
17 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.h - The Stadium cell and header (FABRIC.md §3, punch list item 3.1)
*
* A cell is one of exactly two things: a patron header, or a continuation
* cell owned by exactly one patron. The union is closed, two-valued, and
* fixed at build time -- not a type field. See FABRIC.md §3.
*/
#ifndef STARKERNEL_VM_STADIUM_H
#define STARKERNEL_VM_STADIUM_H
#ifdef __STARKERNEL__
#include <stddef.h>
#include <stdint.h>
#include "starforth_config.h" /* STADIUM_CONTAINS_DEPTH_MAX, STADIUM_CAPACITY_TICK, STADIUM_MEMORY_PERCENT */
#define STADIUM_CELL_BYTES 64
/* Sentinel for `contains` meaning "holds no patron." Not 0 -- cell index 0 is
* a valid index (Hera, item 3.6), so 0 cannot double as "none" without
* conflating "contains Hera" with "contains nothing." */
#define STADIUM_CONTAINS_NONE ((uint32_t)-1)
/*
* StadiumPatronHeader - one member of the closed two-valued cell union
* (FABRIC.md §3). Nine wires: identity, heat, TTL, pin (a bit in `flags`),
* link, code field (`behaviour`), mass, payload, contains. `flags` bit 0 is
* `pin`; the remaining bits are reserved. `behaviour` is the closed code-field
* enumeration (§18.3) -- not yet defined, item 3.3's scope.
*
* Field order is largest-to-smallest so natural C99 alignment adds zero
* padding: every offset below is already a multiple of that field's own
* alignment, and the struct's total size (64) is a multiple of its max
* alignment (8), so no compiler inserts trailing padding either. Do not
* reorder without re-checking this holds on all three ISAs.
*/
typedef struct {
uint64_t identity; /* offset 0 -- handle or name, never a content hash while resident (§24.4) */
uint64_t heat; /* offset 8 -- Q48.16, conserved share of 1.0 (§19.1) */
uint32_t ttl; /* offset 16 -- remaining lifetime; messages and ACLs only (§17.1) */
uint32_t link; /* offset 20 -- index into the Stadium, not a pointer */
uint32_t contains; /* offset 24 -- index of the patron held inside this one, or
* STADIUM_CONTAINS_NONE (item 1.1). Cell index 0 is a valid
* index (Hera, item 3.6) so 0 cannot mean "none" -- item 3.5
* caught this and picked UINT32_MAX instead. Chains up to
* STADIUM_CONTAINS_DEPTH_MAX deep; reap-gating enforcement of
* that bound is item 3.5's scope. */
uint16_t mass; /* offset 28 -- cells this patron occupies (§19.2) */
uint8_t flags; /* offset 30 -- bit 0 = pin; remaining bits reserved */
uint8_t behaviour; /* offset 31 -- code field. Valid values are StadiumBehaviour (§18.3)
* tags cast to uint8_t -- kept as uint8_t rather than the enum type
* itself since C does not guarantee an enum's underlying type, and
* this field's offset is load-bearing for the 64-byte layout item
* 3.1 validated. */
uint8_t payload[32]; /* offset 32 -- inline payload, used when mass == 1 */
} StadiumPatronHeader;
/*
* StadiumContinuationCell - the other member of the union. Owned by exactly
* one patron header, chained by `next`. Never ranked, never reaped, never
* dispatched (§3) -- pure floor space, accounted for in its owner's mass.
*/
typedef struct {
uint32_t next; /* offset 0 -- index of the next continuation cell, or none */
uint8_t payload[60]; /* offset 4 */
} StadiumContinuationCell;
/*
* StadiumCell - the closed two-valued union itself (§3). Which member is
* valid for a given array slot is NOT stored in the cell -- FABRIC.md's item
* 3.1 amendment to §3 rules this an external side bitmap, one bit per cell,
* kept outside the cell array. Declared here as the indexing contract this
* type expects; item 3.2 (boot-time allocation) allocates the bitmap itself.
*/
typedef union {
StadiumPatronHeader header;
StadiumContinuationCell continuation;
} StadiumCell;
/* C99-portable compile-time size assertions (no _Static_assert -- that's C11). */
typedef char stadium_header_size_check[(sizeof(StadiumPatronHeader) == STADIUM_CELL_BYTES) ? 1 : -1];
typedef char stadium_continuation_size_check[(sizeof(StadiumContinuationCell) == STADIUM_CELL_BYTES) ? 1 : -1];
typedef char stadium_cell_size_check[(sizeof(StadiumCell) == STADIUM_CELL_BYTES) ? 1 : -1];
/*
* Items 1.1 and 1.4 named this item as where their Kconfig symbols would be
* implemented. Neither has a consumer yet (item 3.5 for the depth cap,
* capacity arbitration -- not yet on the punch list -- for the tick); these
* checks only prove the symbols are defined and sane, the same discipline
* already applied to the byte-count checks above.
*/
typedef char stadium_contains_depth_configured_check[(STADIUM_CONTAINS_DEPTH_MAX > 0) ? 1 : -1];
typedef char stadium_capacity_tick_configured_check[(STADIUM_CAPACITY_TICK > 0) ? 1 : -1];
/*
* Item 3.7: the per-cell owner array stores a quota-slot index in a single
* uint8_t, so STADIUM_MAX_VM_COUNT must fit in one byte. Default 4, so this
* holds by a wide margin -- checked because it is depended on, not because
* it is expected to fail.
*/
typedef char stadium_max_vm_count_fits_owner_byte_check[(STADIUM_MAX_VM_COUNT <= 255) ? 1 : -1];
/*
* stadium_boot_init - Boot-time allocation (FABRIC.md item 3.2, §17.6 position
* (b)). Sizes the global cell array from the memory budget actually observed
* at boot -- STADIUM_MEMORY_PERCENT of pmm_get_stats().free_bytes at the
* point of the call, rounded down to whole STADIUM_CELL_BYTES cells -- rather
* than a hardcoded count. Also allocates the header/continuation discriminator
* bitmap item 3.1 declared but did not allocate: one bit per cell, bit set
* means the cell at that index is a patron header, clear means continuation
* or not yet in use. Both are kmalloc'd (freestanding kernel, no separate
* PMM-backed region needed for this) and explicitly zero-filled, since
* kmalloc does not zero.
*
* (Item 3.7) Also allocates a per-cell owner byte array (which VM's quota a
* cell belongs to) and chains every cell into a single free list, in
* ascending index order, granted in full to vm_id 0 (Hera) -- the only VM
* that exists (item 0.1). Ascending order guarantees the first-ever
* admission pops cell 0, preserving item 3.6's "Hera is patron zero"
* invariant once real birth-wiring calls stadium_admit() for the first
* time. The free-list next-pointer reuses each cell's own `link` field
* while unresident -- a repurposing of documented-but-unspecified storage,
* not a header change; see stadium_admit()'s doc for why this doesn't
* answer the separate, still-open continuation-chain question.
*
* Must be called after M6 (kmalloc_init) and before any VM is born (§6). Does
* not halt boot on failure -- nothing downstream consumes the Stadium yet.
*
* @return 0 on success, -1 if kmalloc failed for any of the three allocations.
*/
int stadium_boot_init(void);
/* stadium_is_initialized - Whether stadium_boot_init() has succeeded. */
int stadium_is_initialized(void);
/* stadium_cell_count - Number of cells in the array, 0 if not initialized. */
size_t stadium_cell_count(void);
/* stadium_cells - Pointer to the cell array, NULL if not initialized. */
StadiumCell *stadium_cells(void);
/*
* stadium_header_bitmap - Pointer to the discriminator bitmap declared in
* item 3.1, NULL if not initialized. ceil(stadium_cell_count() / 8) bytes.
*/
uint8_t *stadium_header_bitmap(void);
/*
* StadiumBehaviour - the closed code-field enumeration (FABRIC.md §13, §18.3).
* The engine dispatches on this tag and never asks a patron what kind it is
* -- §3's entire point. Two patrons may share a tag: a VM's tag is COOL, the
* same tag a word carries (§18.3). Mapped from §17.1's patron table:
*
* MIGRATE -- blocks: reap event is migration back to Artemis (§17.2)
* DELIVER -- messages: reap event is delivery
* EXPIRE -- ACLs: reap event is TTL expiry
* COOL -- words and VMs: reap event is cooling off the floor
*
* Closed and fixed at build time -- see stadium_dispatch()'s exhaustive
* switch for how the compiler enforces that.
*/
typedef enum {
STADIUM_BEHAVIOUR_MIGRATE = 0,
STADIUM_BEHAVIOUR_DELIVER,
STADIUM_BEHAVIOUR_EXPIRE,
STADIUM_BEHAVIOUR_COOL
} StadiumBehaviour;
/*
* stadium_dispatch - Calls the behaviour handler for a patron's code field.
* The engine calls this at reap and never asks what kind of patron departed
* (§3, §18.3) -- only cell_index and behaviour cross this boundary.
*
* Handlers are stubs today: the real migrate-to-Artemis / deliver / expire /
* cool actions belong to their own subsystems, which have not been migrated
* onto the Stadium yet (Phase 4, §25.5). Nothing calls stadium_dispatch()
* yet either -- that begins with item 3.5 (admission and eviction).
*
* @param cell_index Index into the Stadium of the patron header dispatching.
* @param behaviour Which of the closed tag set to invoke.
*/
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour);
/*
* stadium_density - Heat / mass for the patron header at cell_index (FABRIC.md
* §19.2, §19.3). Read, not computed by a scheduler: both operands already
* live in the header, so this is a division on demand, not maintained
* bookkeeping. Result stays valid Q48.16, since heat is already Q48.16 and
* mass is a plain integer divisor.
*
* Returns 0 if mass is 0 -- an empty or never-admitted slot (everything is
* zero-initialized by stadium_boot_init() until something is actually born
* into the Stadium, which nothing yet does) has no footprint to be dense
* within, rather than a division by zero.
*
* Does not validate that cell_index actually holds a header rather than a
* continuation cell or an out-of-range index -- callers are expected to
* consult the item-3.1 discriminator bitmap first. Ranking (finding the
* densest or least-dense resident) is item 3.5's scope, not this one's;
* this function only supplies the per-cell value that comparison reads.
*
* @param cell_index Index into the Stadium of the patron header to measure.
* @return Density in Q48.16, or 0 if the header's mass is 0.
*/
uint64_t stadium_density(size_t cell_index);
/* Sentinel returned by stadium_admit() on refusal -- no cell index is this large. */
#define STADIUM_CELL_NONE ((size_t)-1)
/*
* Hera is patron zero by construction of §6's boot order: she is the first
* entry admitted into the Stadium. This is a positional invariant, not a
* runtime check of who currently occupies cell 0 -- nothing yet births
* anything, Hera included, so today this index is never actually occupied.
* Used only by stadium_evict()'s item-3.6 assertion below.
*/
#define STADIUM_HERA_CELL_INDEX ((size_t)0)
/*
* stadium_evict - Reap the patron header at cell_index (FABRIC.md §17.2:
* "reap means leaves the floor, not destroyed"). Dispatches its behaviour
* (§18.3), clears its item-3.1 discriminator bit, zeroes its header, and
* (item 3.7) returns the freed cell to the free list of whichever VM's
* quota it was drawn from -- looked up via the internal per-cell owner
* record, not passed by the caller.
*
* PANICS (does not return) if cell_index == STADIUM_HERA_CELL_INDEX and the
* cell is actually resident -- FABRIC.md §20.5 #3: Hera is pinned (§3), but
* pinning alone is a silent guarantee, and item 3.6 requires a hard
* assertion at the eviction site rather than relying on pin holding. This
* check runs BEFORE the pin/contains checks below, deliberately: if pin were
* ever wrongly cleared, the ordinary pin-refusal path would quietly return
* -1 instead of surfacing the break, defeating the point of a second,
* independent check. Selecting patron zero for eviction means the invariant
* is already broken; continuing would run the system without a governor.
*
* Refuses (returns -1, does not panic) if the header is pinned (`flags` bit
* 0, §3's invariance wire) or has a non-none `contains` (item 1.1: a patron
* holding another cannot be reaped, full stop). Also refuses for an
* out-of-range index or a cell whose discriminator bit is not set (nothing
* resident there to reap).
*
* @param cell_index Index of the patron header to reap.
* @return 0 on success, -1 if refused.
*/
int stadium_evict(size_t cell_index);
/*
* StadiumVMQuota - per-VM ownership of a subset of the global cell array
* (FABRIC.md §22.3, item 3.7: "each VM holds its own free-list head index
* into the global array"). A small table, linearly searched by vm_id --
* capsule_birth.c's vm_id is monotonic and never reused (next_vm_id only
* increments, even across VM death), so it cannot index this table
* directly, and STADIUM_MAX_VM_COUNT is small enough (default 4) that a
* linear scan costs nothing. Not exposed outside stadium.c: nothing outside
* needs to inspect quota state directly yet.
*/
/* Sentinel meaning "no VM owns this slot yet." Distinct from a real vm_id
* (capsule_birth.c reserves 0 for Hera, so 0 cannot double as "unused" here
* either -- same shape of mistake STADIUM_CONTAINS_NONE was fixed for). */
#define STADIUM_QUOTA_SLOT_EMPTY ((uint32_t)-1)
/*
* stadium_admit - Place a candidate patron header into the Stadium, scoped
* to vm_id's quota (FABRIC.md §19.3, §22.3, item 3.7).
*
* Pops vm_id's free-list head first (O(1)) if non-empty. Only if that VM's
* free list is exhausted does this fall back to eviction -- scoped to that
* SAME VM's own resident patrons only (quota isolation: a VM's admission can
* never evict another VM's patron), finding the least-dense evictable
* resident (not pinned, not `contains`-gated -- per §3 and item 1.1) and
* evicting it via stadium_evict() only if the candidate is strictly denser
* (§19.3: "denser than," not "at least as dense as"). Otherwise refuses.
*
* Does not itself assert anything about which resident this turns out to be
* -- the item-3.6 rule that patron zero (Hera) must never actually be
* selected is a separate, later check at the eviction site.
*
* REFUSES if vm_id has no quota granted (only Hera, vm_id 0, has one today
* -- granted the entire array at stadium_boot_init(), since she is the only
* VM that exists per item 0.1). Granting quota to additional VMs, and
* transferring capacity between them, is capacity ARBITRATION -- item 1.3
* left "how much capacity moves per eligible transfer" explicitly open, so
* this item does not invent it. Only the boot-time all-to-Hera grant exists.
*
* REFUSES any candidate with mass != 1. A multi-cell patron (mass > 1, e.g.
* §23.3's 1024-byte block at mass 19) needs a continuation chain, and no
* header field is documented anywhere as carrying the index of a patron's
* first continuation cell -- `link` is described only as generic "index
* into the Stadium, not a pointer." This item repurposes `link` for a
* different, non-conflicting use (the free-list next-pointer, while a cell
* is unresident -- see stadium.c), but does not invent an answer to the
* continuation-chain question, which stays open. Item 3.5's refusal
* therefore stands exactly as it was.
*
* REQUIRES candidate->contains to be either STADIUM_CONTAINS_NONE or a valid
* index (< the current cell count) -- refuses otherwise. This does NOT catch
* a zero-initialized candidate that was meant to contain nothing: 0 is a
* valid index (Hera), so a caller that forgets to set `contains` explicitly
* to STADIUM_CONTAINS_NONE will admit a patron that reads as "contains
* Hera" and is therefore permanently un-evictable. There is no way to tell
* "meant to be 0" from "forgot to set it" from inside this function --
* callers must set every field, `contains` included.
*
* @param vm_id Owning VM's id (capsule_birth.c's registry). Allocation
* is scoped to this VM's own quota.
* @param candidate Header to admit. Copied into the winning cell as-is;
* caller fills in every field including mass and heat.
* @return The cell index admitted into, or STADIUM_CELL_NONE if refused
* (vm_id has no quota, mass != 1, invalid contains, that VM's
* quota full and candidate not denser than its least-dense
* evictable resident, or it has no evictable resident at all).
*/
size_t stadium_admit(uint32_t vm_id, const StadiumPatronHeader *candidate);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_STADIUM_H */