/* 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.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 #include #include "starforth_config.h" /* STADIUM_CONTAINS_DEPTH_MAX, STADIUM_CAPACITY_TICK, STADIUM_MEMORY_PERCENT */ #include "starkernel/vm_uuid.h" /* VMUuid -- FABRIC.md item 3.8 */ #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 -- a * VMUuid (item 3.8) can't be used as a direct array index anyway, 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. Slot emptiness is tracked by an internal * `in_use` flag, not a vm_id sentinel value -- there is no unused vm_id bit * pattern to reserve for it. */ /* * 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_uuid_hera(), 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(VMUuid vm_id, const StadiumPatronHeader *candidate); #endif /* __STARKERNEL__ */ #endif /* STARKERNEL_VM_STADIUM_H */