Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+13
View File
@@ -0,0 +1,13 @@
# include/starkernel/vm/
Headers for the kernel-side VM subsystem (`src/starkernel/vm/`).
- `arena.h` — the capsule arena allocator: fixed-region allocation for
capsule payload data, separate from `kmalloc`'s general kernel heap.
- `parity.h` — the birth/execution parity-record logger: declares the
logging interface used to record VM ID, capsule content hash, and
dictionary hash on every capsule birth, enabling offline
determinism verification across independent kernel runs.
See `include/starkernel/vm/bootstrap/README.md` for the VM bootstrap
subsystem.
+96
View File
@@ -0,0 +1,96 @@
/*
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.
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.
*/
/**
* arena.h - VM arena management for StarKernel builds.
*
* Hosted builds never include this header. Kernel integration layers use it
* to provision the PMM-backed VM arena before handing memory to the VM core.
*/
#ifndef STARKERNEL_VM_ARENA_H
#define STARKERNEL_VM_ARENA_H
#ifdef __STARKERNEL__
#include <stddef.h>
#include <stdint.h>
uint64_t sk_vm_arena_alloc(void);
void sk_vm_arena_free(void);
uint8_t *sk_vm_arena_ptr(void);
size_t sk_vm_arena_size(void);
int sk_vm_arena_is_initialized(void);
void sk_vm_arena_assert_guards(const char *tag);
/**
* sk_vm_native_stack_t - one VM's own dedicated native (C) stack.
*
* FABRIC-3.md §XXVIII (Stage 1, preemptive-context-switch per-VM native
* stacks, 2026-09-13). Unlike sk_vm_arena_alloc() -- whose PMM+guard-page
* path is exercised only for Hera; every baby VM's "arena" is actually a
* plain kmalloc block, per host_services.c's kernel_alloc() -- every native
* stack, for every VM without exception, gets its own real, independent
* pmm_alloc_contiguous() allocation with guard pages. A stack overflow is
* exactly the failure mode guard pages exist for, and unlike the dictionary
* arena, a corrupted stack can also corrupt whatever saved context Stage 2
* later trusts -- worth the extra PMM pages every VM, not just Hera.
*
* Caller owns this struct (stored directly on the VM, mirroring how
* vm->memory already holds its own arena pointer directly rather than going
* through any module-level registry) and passes it back unchanged to
* sk_vm_native_stack_free().
*/
typedef struct {
uint64_t paddr; /* physical base (for pmm_free_contiguous) */
uint64_t guard_vaddr; /* virtual base of the whole guarded region */
uint64_t stack_top; /* initial SP value -- stacks grow down on all
* 3 arches, so this is guard_vaddr + one guard
* page + the full stack size */
} sk_vm_native_stack_t;
int sk_vm_native_stack_alloc(sk_vm_native_stack_t *out);
void sk_vm_native_stack_free(const sk_vm_native_stack_t *stack);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_ARENA_H */
@@ -0,0 +1,6 @@
# include/starkernel/vm/bootstrap/
- `sk_vm_bootstrap.h` — declares the kernel VM bootstrap entry point
implemented in `src/starkernel/vm/bootstrap/sk_vm_bootstrap.c`. Owns
capsule loading and VM initialization wiring; deliberately kept separate
from `kernel_main.c`, which owns only hardware milestones.
@@ -0,0 +1,75 @@
/*
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.
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.
*/
/**
* sk_vm_bootstrap.h - Kernel VM bootstrap/parity entrypoints
*
* Gated by STARFORTH_ENABLE_VM so kernel images stay lean by default.
* Provides a single entry point that initializes the VM using kernel
* HAL services and emits the M7 parity packet.
*/
#ifndef STARKERNEL_VM_BOOTSTRAP_SK_VM_BOOTSTRAP_H
#define STARKERNEL_VM_BOOTSTRAP_SK_VM_BOOTSTRAP_H
#ifdef __STARKERNEL__
#include "starkernel/vm/parity.h"
/**
* sk_vm_bootstrap_parity - Initialize VM and emit parity packet.
*
* @param out Optional parity packet to fill (may be NULL to use a local)
* @return 0 on success, -1 on failure (see parity packet for details)
*/
int sk_vm_bootstrap_parity(ParityPacket *out);
/**
* sk_get_mama_vm - Return opaque pointer to Mama's VM context.
*
* Valid after sk_vm_bootstrap_parity() succeeds.
*/
void *sk_get_mama_vm(void);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_BOOTSTRAP_SK_VM_BOOTSTRAP_H */
@@ -0,0 +1,850 @@
/*
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.
*/
/**
* kernel_hermes.h - Kernel-resident Hermes: message and membership
* structures (FABRIC-3.6.md task 2.1, item 28; design: FABRIC-3.5.md
* SIII/SXXXIII/SXXXIV).
*
* Phase 2, task 2.1 ONLY: type definitions, wired to nothing, drawing no
* heat. No allocator, no send/deliver/reap logic, no registration
* anywhere -- those are tasks 2.2 onward, each its own commit. This file
* existing and compiling changes no VM's dictionary and no runtime
* behaviour; that is deliberate (FABRIC-3.5.md SXXII.4: Phase 2 structures
* come first and prove nothing until the allocator is built on top, SXL.4
* item 41).
*
* SkHermesMessage mirrors capsules/common/messaging.4th's live MSG-CELLS
* layout (9 cells: MSG-TYPE@/FROM@/TO@/PADDR@/PLEN@/STADIUM-CELL@/SEQ@/
* CH@/ORIG-TYPE@) field-for-field, per FABRIC-3.5.md SXXXIII.4 item 1 --
* "roughly half the file is accessors that become struct fields." The
* Stadium-cell field is the heat coupling itself: a message's heat is not
* a field of its own, it IS the Stadium cell it occupies (SXL.4's
* consumption model, SXXXIX.4's per-VM invariant) -- there is deliberately
* no separate heat field here to keep that single-source-of-truth.
*
* SkHermesMembership is the "one broadcast membership list" SXXXIII.4
* item 3 and SXXXIII.5 recommend in place of messaging.4th's 28-word
* channel abstraction (CH-REQUEST/ACCEPT/CONFIRM/CLOSE/MINT-ID and the
* CH-NEGOTIATING/OPEN/CLOSING state machine) -- traced to have exactly one
* live caller, CH-ADD-MBR, everything else channel-shaped is unexercised.
* Whether kernel-Hermes ever adds negotiation on top is item 27, an open
* Phase 3 ruling (FABRIC-3.6.md B1) -- this structure does not answer
* that question, it only holds a flat list, which is correct either way.
*/
#ifndef STARKERNEL_VM_KERNEL_HERMES_H
#define STARKERNEL_VM_KERNEL_HERMES_H
#ifdef __STARKERNEL__
#include <stddef.h>
#include <stdint.h>
#include "starkernel/vm_uuid.h" /* VMUuid */
#include "starkernel/q48_16.h" /* Q48_ONE */
/*
* SK_HERMES_MSG_MAX / SK_HERMES_MEMBER_MAX - sizing. Mirrors
* messaging.4th's own MSG-MAX (32) and MBR-MAX (64) as a starting point --
* kernel-Hermes is a single central pool rather than N per-VM arenas, so
* these may need revisiting once real traffic exists to size against. Not
* a ruling, just where the FORTH precedent already was.
*/
#define SK_HERMES_MSG_MAX 32
#define SK_HERMES_MEMBER_MAX 64
/* SK_HERMES_CHUNK_MAX_PAYLOAD - one block (1024 bytes, 64x16, SXLIV.1),
* moved up here (was originally defined much further down, next to
* SkHermesChunkHeader) so SkHermesMessage's own payload_buf field below
* can be sized against it. See this file's chunking section, further
* down, for the full sizing rationale -- not restated here. */
#define SK_HERMES_CHUNK_MAX_PAYLOAD 1024
/*
* SK_HERMES_Q_SLOT - per-message admission heat, task 2.2 (item 28).
* messaging.4th:44-46 derives Q.SLOT as the reservoir remaining after
* COMMON-CH's own Q.1/3 floor, split across MSG-MAX + (CH-MAX-1) slots --
* a floor that exists to reserve heat for the one live channel object
* itself. Kernel-Hermes has no such object: SXXXIII.4 item 3 and
* SXXXIII.5 replace the channel abstraction with a flat membership list
* that carries no Stadium heat of its own, so there is nothing left for a
* floor to protect. Deliberately simpler here rather than carrying the
* old formula's now-unmotivated term forward: reservoir split evenly
* across message slots only.
*/
#define SK_HERMES_Q_SLOT ((uint64_t)Q48_ONE / SK_HERMES_MSG_MAX)
/*
* SkHermesMessage - one message slot, field-for-field mirror of
* messaging.4th's 9-cell MSG layout.
*
* @field type Message type code (SPAWN/PAUSE/RESUME/KILL-style
* codes are Category A/dead per task 0.1/0.4; live
* types today are CONSOLE-CMD-EVENT(7),
* ELEVATE-REQUEST(8), BLK-ATTACH-EVENT(9), messaging.4th
* reserved sentinels MSG-NACKED(253)/MSG-DELIVERED(255)).
* @field from Sending VM (mirrors MSG-FROM@).
* @field to Target VM (mirrors MSG-TO@).
* @field payload_addr Payload address a reader dereferences (mirrors
* MSG-PADDR@). CORRECTED 2026-09-22 (real defect,
* FABRIC-3.6.md task 3.8's own findings log, "a real
* payload-aliasing defect"): this used to be the
* caller's own out-of-line pointer, stored as-is --
* two sends before either drained meant both
* messages pointed at the same caller-owned buffer,
* whichever send wrote last silently winning. Now
* always points at this SAME message's own
* `payload_buf` below, which sk_hermes_send_one()
* copies into at send time -- every message owns its
* payload bytes, independent of whatever the caller
* does with its own buffer afterward. Every existing
* reader (sk_hermes_drain_checkpoint(),
* sk_hermes_reassemble()) is unaffected: `payload_addr`
* still points at valid payload bytes of the same
* length, just message-owned instead of caller-owned.
* FABRIC-3.5.md SXLIII.6.1 (item 44, still open): a
* payload above INPUT_BUFFER_SIZE-1 (1024) bytes cannot
* be drained in one interpret call -- bound or chunk it
* before Phase 3, not here.
* @field payload_buf Backing storage for `payload_addr` above -- see its
* own doc comment. Sized to SK_HERMES_CHUNK_MAX_PAYLOAD,
* the uniform bound sk_hermes_send_one()/
* sk_hermes_publish() already enforce on `payload_len`
* (defined just above, moved up in this same fix so it
* is in scope here). No reader should touch this field
* directly -- go through `payload_addr`, which is what
* every existing reader already does.
* @field payload_len Payload length in bytes (mirrors MSG-PLEN@).
* @field stadium_cell Index into the Stadium cell array this message's
* heat currently occupies, or a sentinel meaning "none"
* (mirrors MSG-STADIUM-CELL@) -- the heat coupling
* itself; see this file's own top comment.
* @field seq Monotonic send sequence (mirrors MSG-SEQ@).
* @field channel Broadcast/channel marker; unused today (mirrors
* MSG-CH@) -- item 27 territory, not decided here.
* @field orig_type Original type before a NACK/redeliver rewrite
* (mirrors MSG-ORIG-TYPE@).
* @field in_use Free-list occupancy flag for task 2.2's allocator.
* Not present in the FORTH layout (which uses
* MSG-TYPE@ 0<> as its own live/free test) -- kept
* explicit here rather than overloading `type == 0`,
* since kernel-Hermes's held/pulled/returned/consumed
* ledger (task 2.4, SXL.4) needs an unambiguous
* occupancy bit independent of the type field's value.
*/
typedef struct {
uint32_t type;
VMUuid from;
VMUuid to;
void *payload_addr;
uint8_t payload_buf[SK_HERMES_CHUNK_MAX_PAYLOAD];
uint32_t payload_len;
int32_t stadium_cell;
uint32_t seq;
uint32_t channel;
uint32_t orig_type;
int in_use;
VMUuid owner; /* VM whose reservoir funded this message (task 2.7) */
} SkHermesMessage;
/*
* SkHermesMembership - one flat broadcast membership list: every VM that
* has joined, no per-member state beyond identity. Replaces the 28-word
* channel abstraction; see this file's own top comment for why.
*
* @field members Member VM identities, valid for indices < count.
* @field count Number of valid entries in members[].
*/
typedef struct {
VMUuid members[SK_HERMES_MEMBER_MAX];
size_t count;
} SkHermesMembership;
/*
* sk_hermes_alloc - Heat-coupled allocate (task 2.2, item 28): pull
* SK_HERMES_Q_SLOT from vm_id's own Stadium reservoir, admit a real
* Stadium-floor patron with that heat (behaviour DELIVER, matching
* messaging.4th's own `SB-DELIVER STADIUM-ADMIT`), and claim a free
* message slot recording the admitted cell. Refuses cleanly, rolling
* back anything already pulled/admitted, if reservoir, arena, or the
* Stadium floor itself refuses.
*
* CORRECTED in task 2.3 (see this file's .c counterpart's own top
* comment): the first cut of this function never admitted a real
* Stadium patron, which left held message heat invisible to SXL.4's
* conservation invariant while allocated. It now does, which is what
* makes sk_hermes_release()'s eviction meaningful.
*
* Wired to nothing outside this file's own self-test (kernel_main.c) as
* of task 2.2/2.3 -- no FORTH word, no capsule interaction, no protocol
* logic (send/deliver/reap are later tasks). Exercised only by a
* synthetic-VM self-test; does not change any real VM's dictionary.
*
* @param vm_id Caller whose reservoir is charged.
* @param out_msg On success, set to the claimed slot. Untouched on
* refusal.
* @return 0 on success, -1 on refusal (insufficient reservoir, no free
* slot, or the Stadium floor itself refused admission -- this
* function does not distinguish the three in the return value;
* all three leave all state exactly as it was).
*/
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg);
/*
* sk_hermes_release - Return a message's heat via the Stadium eviction
* path (task 2.3, item 28), mirroring MSG-FREE-NODE
* ("DUP 5 CELLS + @ STADIUM-EVICT DROP"). stadium_evict() itself returns
* the departing patron's remaining heat to its owning VM's reservoir --
* this function does not touch the reservoir directly, matching the
* FORTH shape exactly. Clears the slot (in_use = 0) whether or not
* eviction succeeds, since a message this function was asked to release
* should not remain allocated either way.
*
* @param msg A slot previously returned by sk_hermes_alloc(). Refused
* (returns -1, no effect) if NULL or already released.
* @return 0 on successful eviction, -1 if msg was invalid or eviction
* itself was refused by the Stadium floor.
*/
int sk_hermes_release(SkHermesMessage *msg);
/*
* sk_hermes_ledger - Task 2.4 (item 28), the four counters FABRIC-3.5.md
* SXXXVII.3/SXL.4 name: held, pulled, returned, consumed. Each is touched
* at exactly one call site:
*
* held += pulled amount -- sk_hermes_alloc(), on success only
* held -= returned amount -- sk_hermes_release(), on success only
* pulled += pulled amount -- sk_hermes_alloc(), same site as held's
* returned += returned amount -- sk_hermes_release(), same site as held's
* held -= decay delta -- sk_hermes_decay(), same site as consumed's
* consumed += decay delta -- sk_hermes_decay() (task 2.5)
*
* The audit invariant this ledger exists to make checkable (task 2.6,
* epsilon zero): held == pulled - returned - consumed. All four are
* exact integers (SXXXVII.3: "Nothing here is a measurement. There is no
* noise to tolerate.") -- this accessor is how task 2.6's audit, task
* 2.7's Stage B proof, and task 2.8's scan cross-check all read the same
* four numbers, rather than each keeping its own copy.
*
* @param held Set to the current live total (may be NULL).
* @param pulled Set to the cumulative total ever pulled (may be NULL).
* @param returned Set to the cumulative total ever returned (may be
* NULL).
* @param consumed Set to the cumulative total ever consumed by decay
* (may be NULL).
*/
void sk_hermes_ledger(uint64_t *held, uint64_t *pulled, uint64_t *returned, uint64_t *consumed);
/*
* SK_HERMES_Q_DECAY - per-application decay factor, Q48.16. Identical to
* messaging.4th's `65208 CONSTANT Q-DECAY` (65208/65536 ~= 0.99499):
* FABRIC-3.5.md SXL.4 rules that the consumption economy is preserved
* exactly, not re-tuned.
*/
#define SK_HERMES_Q_DECAY ((uint64_t)65208)
/*
* sk_hermes_decay - Apply one decay step to a live message (task 2.5,
* item 28), mirroring MSG-COOL-ONE (`MSG-HEAT@ Q-DECAY Q.* MSG-HEAT!`):
* the message's Stadium-cell heat becomes q48_mul(heat, Q_DECAY). Unlike
* MSG-COOL-ONE, the destroyed difference is RECORDED (SXXXVII.3): the
* delta heat_before - heat_after is added to `consumed` and subtracted
* from `held`, so held == pulled - returned - consumed stays exact.
*
* @param msg A live slot from sk_hermes_alloc(). Refused (returns -1, no
* effect) if NULL, not in use, or holding no Stadium cell.
* @return 0 on success, -1 if refused.
*/
int sk_hermes_decay(SkHermesMessage *msg);
/*
* Task 2.6 (item 28): the self-audit, epsilon zero (SXXXVII.3).
*
* sk_hermes_audit_values - pure predicate: nonzero iff
* held == pulled - returned - consumed exactly. Separate from the live
* ledger so the check can be exercised with deliberately corrupted values
* without mutating real state.
*
* sk_hermes_audit - checks the live ledger (O(1), SXXXVII.4); on failure
* prints a console line and bumps a failure count. Called at the end of
* every successful mutation: alloc, release, decay.
*
* sk_hermes_audit_failure_count - cumulative failures seen by the live
* audit; must be 0 in a healthy kernel.
*/
int sk_hermes_audit_values(uint64_t held, uint64_t pulled, uint64_t returned, uint64_t consumed);
int sk_hermes_audit(void);
uint64_t sk_hermes_audit_failure_count(void);
/*
* Task 2.8 (item 28), SXXXVII.4: the scan that verifies the counters
* themselves. DIAGNOSTIC ONLY -- O(SK_HERMES_MSG_MAX), never called from
* alloc/release/decay.
*
* sk_hermes_scan_held - walks the message arena and sums the live Stadium
* cell heat of every in-use message (the ground truth `held` claims to
* track). Sets *live_count (may be NULL) to the messages counted.
*
* sk_hermes_scan_check - nonzero iff the scan sum equals the `held`
* counter exactly.
*/
uint64_t sk_hermes_scan_held(size_t *live_count);
int sk_hermes_scan_check(void);
/*
* Channel table (FABRIC-3.6.md task 3.2, B1 / FABRIC-3.5.md SXLV.1): B1
* overrules SXXXIII.5's single flat broadcast list -- messaging is
* publish/subscribe, one permanent common channel every VM joins at
* birth, private channels created by request/grant/deny (task 3.6, not
* this one). SkHermesMembership (task 2.1) becomes per-channel: one
* instance per SkHermesChannel table entry instead of one global list.
*
* INERT, same posture as task 2.1: this task wires channel existence and
* membership bookkeeping only. No publish, no dispatch, no ACK/NACK, no
* ACL hook (tasks 3.3, 3.6, 3.7) -- creating/destroying/subscribing a
* channel here changes no VM's dictionary and delivers no message.
*
* Sizing: SXLV.1 says the channel table has "no fixed channel maximum,
* same reasoning as SXLV.2" -- SXLV.2/task 3.1's own ruling is boot-time,
* RAM-derived sizing (Stadium's stadium_max_vm_count_val pattern), not a
* literal unbounded/growable table. No separate numeric sizing rule was
* ruled for the channel table specifically (task 3.0's five sub-items
* covered ACK cadence/ACL-hook-location/switch-table-sizing/chunk-
* framing/drain-cadence, not this). Extending task 3.1's already-ruled
* pattern directly -- same stadium_max_vm_count() bound, same
* kmalloc-at-boot shape -- is the smallest choice consistent with what
* was ruled, flagged here as an engineering extrapolation, not restated
* as a separate Captain Bob ruling.
*/
/* SK_HERMES_CHANNEL_COMMON - the permanent common channel's fixed index.
* Exists (in_use, empty membership) from sk_hermes_channels_boot_init()
* itself, before any VM is born -- every VM joins it at birth (task 3.2's
* own kernel_main.c/capsule_birth.c wiring), never destroyed. */
#define SK_HERMES_CHANNEL_COMMON 0
/*
* SkHermesChannel - one channel/topic: its membership plus an occupancy
* flag for the dynamic table below. No name field -- messaging.4th's own
* channel abstraction (CH-ARENA) has none either; a channel is identified
* by its table index, the same way a Stadium quota slot or a switch-
* signal slot is identified by index, not by string.
*/
typedef struct {
SkHermesMembership membership;
int in_use;
} SkHermesChannel;
/* Boot-time allocation: kmalloc's the channel table to
* stadium_max_vm_count() entries (see this section's own sizing note
* above) and creates the common channel (index SK_HERMES_CHANNEL_COMMON,
* in_use, empty membership -- members join at birth, not pre-populated
* here). Must run after stadium_boot_init() (that bound is 0, and this
* fails, until Stadium has computed it). Soft failure -- returns -1 and
* leaves the table unallocated (capacity 0, so every call below simply
* refuses) rather than halting boot, same posture as
* stadium_boot_init()/session_boot_init()/sk_vm_switch_signal_boot_init().
* Idempotent-unsafe: calling twice leaks the first allocation, so callers
* must call it exactly once. */
int sk_hermes_channels_boot_init(void);
/* sk_hermes_channel_create - allocate a new, empty, inert channel.
* Returns its table index, or -1 if the table is full or
* sk_hermes_channels_boot_init() was never called/failed. */
int sk_hermes_channel_create(void);
/* sk_hermes_channel_destroy - tear down a channel created by
* sk_hermes_channel_create(). Refuses (-1, no effect) if channel_id is
* SK_HERMES_CHANNEL_COMMON (the common channel is permanent, SXLV.1),
* out of range, or not in use. Clears membership. */
int sk_hermes_channel_destroy(int channel_id);
/* sk_hermes_channel_subscribe - add vm_id to channel_id's membership.
* Refuses (-1, no effect) if channel_id is invalid/not in use, vm_id is
* already a member, or the channel's membership is already at
* SK_HERMES_MEMBER_MAX. Idempotent in effect (a second call with the
* same args refuses rather than duplicating), not in return value. */
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id);
/* sk_hermes_channel_unsubscribe - remove vm_id from channel_id's
* membership (compacts the list, same shape as
* sk_vm_switch_signal_unregister()). Refuses (-1, no effect) if
* channel_id is invalid/not in use or vm_id is not a member. */
int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id);
/* sk_hermes_channel_is_member - nonzero iff vm_id is currently a member
* of channel_id. Zero (not an error signal) if channel_id is invalid/not
* in use. */
int sk_hermes_channel_is_member(int channel_id, VMUuid vm_id);
/* sk_hermes_channel_member_count - current membership size of channel_id,
* or -1 if channel_id is invalid/not in use. */
int sk_hermes_channel_member_count(int channel_id);
/* sk_hermes_channel_capacity - the table's boot-time-computed capacity
* (0 if sk_hermes_channels_boot_init() was never called or failed) --
* DoE/test observability, mirrors sk_vm_switch_signal_slot_capacity(). */
int sk_hermes_channel_capacity(void);
/* Boot-time allocation for the per-subscriber pending-queue table
* (FABRIC-3.6.md task 3.3), kmalloc'd to stadium_max_vm_count() entries --
* same sizing pattern as the channel table (task 3.2) and switch table
* (task 3.1); see kernel_hermes.c's own comment on this call for why. Must
* run after stadium_boot_init(). Soft failure -- returns -1 and leaves the
* table unallocated (every queue lookup then finds nothing) rather than
* halting boot. Idempotent-unsafe: call exactly once. */
int sk_hermes_queues_boot_init(void);
/*
* sk_hermes_send_one - point-to-point delivery to exactly one VM
* (FABRIC-3.6.md task 3.6, extracted from sk_hermes_publish()'s own
* per-subscriber body, which now calls this once per channel member).
* One allocation, one queue push, same rollback-on-refusal discipline
* sk_hermes_publish() already had -- one code path, so the ledger can
* never diverge between the two callers.
*
* Exists because task 3.6's negotiation messages (request/grant/deny/
* close/ACK/NACK) are inherently addressed to ONE specific VM, and
* sk_hermes_publish()'s fan-out cannot express that: it sets msg->to to
* whichever member it is currently iterating, so a negotiation message
* "published" to the common channel would be delivered to (and drawn
* heat against) every common-channel member, each believing it was the
* addressee, not just the real target. `messaging.4th`'s own
* `CH-REQUEST ( type from to paddr plen -- )` carried an explicit `to`
* for exactly this reason -- point-to-point addressing was in the
* original protocol from the start.
*
* @param from Sender, whose reservoir funds the allocation.
* @param to The one recipient.
* @param type Message type code.
* @param channel_id Tag copied into the message's own `channel`
* field -- purely informational at this level (no
* membership is consulted or required), letting a
* response encode context (e.g. task 3.6's GRANT
* uses this to tell the requester which private
* channel was just created).
* @param payload_addr Caller-owned buffer, read and copied into the
* message's own storage before this function
* returns (2026-09-22 fix, this struct's own
* payload_addr field doc comment has the full
* account) -- the caller does not need to keep
* it alive past this call.
* @param payload_len Refused (-1) if it exceeds
* SK_HERMES_CHUNK_MAX_PAYLOAD.
* @return 0 on success, -1 on any refusal (bound, reservoir, arena, or
* destination queue full -- all leave state exactly as found).
*/
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len);
/*
* Publish path, no dispatch (FABRIC-3.6.md task 3.3, SXLIII.3; heat-cost
* ruling 2026-09-21: one message per subscriber, separate heat draw each --
* matches the existing heat-coupled allocator 1:1, no refcount machinery).
*
* sk_hermes_publish() loops sk_hermes_send_one() (task 3.6 extraction,
* above) once per channel member, funded by the publisher's own
* reservoir each time. It does not interpret, deliver, or otherwise
* dispatch anything -- draining a queue at a VM's own outermost
* interpret checkpoint is task 3.4's scope, not this one's.
*
* Best-effort, not atomic across subscribers: if a given subscriber's
* send_one() call is refused (reservoir exhausted, message arena full,
* or that subscriber's own pending queue full), that one subscriber is
* skipped -- send_one() has already rolled back its own allocation
* internally, so nothing here needs to -- but delivery to every OTHER
* subscriber already queued is not undone. This was not a
* separate Captain Bob ruling; it is the natural reading of "ledger audit
* and stadium_conserved() hold across N publishes to M subscribers" (task
* 3.3's own check) -- those invariants hold under partial delivery just
* as well as under all-or-nothing, and requiring atomicity across M
* independent reservoir-funded allocations would need a two-phase
* commit/rollback this task's inert scope does not call for.
*
* @param from Publisher, whose reservoir funds every allocation.
* @param channel_id Target channel (SK_HERMES_CHANNEL_COMMON or a
* channel from sk_hermes_channel_create()). Refused
* (-1) if invalid/not in use.
* @param type Message type code, passed through unchanged.
* @param payload_addr Caller-owned buffer, read once per subscriber via
* sk_hermes_send_one() (its own doc comment has the
* full account) -- the caller does not need to keep
* it alive past this call.
* @param payload_len Payload length in bytes. Refused (-1, no
* allocation attempted) if it exceeds
* SK_HERMES_CHUNK_MAX_PAYLOAD (task 3.5, added here
* since task 3.3 deliberately parked this check --
* see that section's own doc comment below).
* @return Count of subscribers successfully enqueued to (0..member count),
* or -1 if channel_id itself was invalid or payload_len exceeded
* the bound.
*/
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len);
/* SK_HERMES_PENDING_MAX - per-subscriber pending-queue depth. The global
* message arena (SK_HERMES_MSG_MAX) is the real ceiling on how many
* messages can ever be in flight system-wide, so sizing each VM's own
* queue to that same bound is a safe, simple upper limit rather than a
* new number to justify. */
#define SK_HERMES_PENDING_MAX SK_HERMES_MSG_MAX
/* sk_hermes_pending_count - number of messages currently queued for
* vm_id (0 if vm_id has no queue yet -- never having received a message
* is not an error). */
int sk_hermes_pending_count(VMUuid vm_id);
/* sk_hermes_pending_peek - the oldest still-queued message for vm_id, or
* NULL if vm_id has no queue or an empty one. Does not remove it -- task
* 3.4's drain logic is expected to peek, interpret, then pop. */
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id);
/* sk_hermes_pending_pop - removes (does not release/interpret) the
* oldest queued entry for vm_id. Callers that also want the message's
* heat returned must call sk_hermes_release() on the value
* sk_hermes_pending_peek() returned, themselves, before or after popping
* -- this function only advances the queue. Refused (-1, no effect) if
* vm_id has no queue or an empty one. */
int sk_hermes_pending_pop(VMUuid vm_id);
/* Forward declaration only -- kernel_hermes.h deliberately does not
* include vm.h (kept decoupled from the full VM struct, same posture as
* every other type in this file), but sk_hermes_drain_checkpoint() below
* needs a VM* parameter. Matches vm.h's own `typedef struct VM VM;`
* shape exactly, so no redefinition conflict. */
typedef struct VM VM;
/*
* Drain at the outermost checkpoint (FABRIC-3.6.md task 3.4, SXLIII.3-.5;
* one message per checkpoint, ruled 2026-09-21). Called from
* execute_colon_word()'s existing cooperative checkpoint in vm_core.c,
* gated the same way the switch-signal checkpoint already is
* (sk_vm_at_outermost_interpret()).
*
* AMENDS SXLIII.5's own claim: "recursive drain is prevented for free"
* via g_vm_interpret_depth is true (the SAME message cannot drain twice),
* but that counter says nothing about a SEPARATE, real hazard SXLIII.5
* never named -- calling vm_interpret() on this same vm, from inside its
* own currently-running vm_interpret() call, overwrites vm->input_buffer/
* input_length/input_pos with the drained payload's own state.
* VMCallState (vm_state_push()/vm_state_pop(), mama_forth_words.c) saves
* only rsp/exit_colon/ecw_nesting -- never these three fields (the exact
* gap FABRIC-3.md SXX documents: "the same class... for input_buffer/
* input_pos not being saved by vm_state_push/pop"). Left unaddressed,
* the enclosing vm_interpret() call's own while loop would silently lose
* the rest of its input line/block the moment a drain fires mid-line --
* the same failure shape as the INPUT_BUFFER_SIZE 256 defect
* .claude/CLAUDE.md calls non-negotiable, and trap #1 in this document's
* own START HERE. Not a divergence from SXLIII.3's ruling (the mechanism
* is exactly as ruled); cursor/mode/error/abort preservation is this
* function's own implementation obligation, not a new design question.
*
* sk_hermes_drain_checkpoint() therefore snapshots vm->input_buffer/
* input_length/input_pos/mode/error/abort_requested before calling
* vm_interpret() on the queued payload, forces vm->mode to
* MODE_INTERPRET for the duration (a checkpoint reached mid-colon-
* definition must not let the payload's words compile into the
* enclosing definition), and restores all six afterward -- fully
* isolating the drain from whatever the enclosing execution was doing.
*
* Payload convention: payload_addr is trusted to point at a
* NUL-terminated C string (vm_interpret()'s own signature takes no
* length) -- payload_len is not consulted here. Bounding/validating that
* is task 3.5's scope (payload bound and chunking), not this one's.
*
* Fast path: sk_hermes_publish()/sk_hermes_pending_pop() maintain a
* single system-wide pending-total counter; this function reads it
* first and returns immediately if it is 0, so the common case (nothing
* in flight) costs one integer read on every word dispatch, not a
* stadium_max_vm_count()-sized queue-table scan.
*
* @param vm The VM at its own outermost checkpoint. NULL is refused.
* @return 1 if a message was drained cleanly, 0 if nothing was pending
* for this vm, -1 if a message was drained but interpreting its
* payload set vm->error (restored to its pre-drain value either
* way -- a bad message must not abort the enclosing execution).
*/
int sk_hermes_drain_checkpoint(VM *vm);
/*
* Payload bound and chunking (FABRIC-3.6.md task 3.5, B4 / FABRIC-3.5.md
* SXLV.3; chunk framing ruled 2026-09-21: each chunk carries (msg_id,
* seq, is_last) ahead of the real content).
*
* Sizing decision (not itself ruled -- see SXLV.4's own note that the
* concrete framing was a task-writing prerequisite, not a ruling):
* SK_HERMES_CHUNK_MAX_PAYLOAD (1024, one block, SXLIV.1) bounds a
* message's payload_len UNIFORMLY -- chunked or not. A chunk carrier's
* own payload is [SkHermesChunkHeader][content slice], so the slice
* itself is capped at 1024 - sizeof(SkHermesChunkHeader), not 1024,
* keeping every message on the wire under the same one-block bound
* SXLIII.6.1 already requires for vm_interpret()'s own drain limit.
* (The alternative -- slice up to 1024, carrier up to
* 1024+sizeof(header) -- was considered and rejected: it would mean a
* chunk carrier can never be handed to vm_interpret() as-is, making a
* later chunk-aware drain a special case instead of the same drain path
* every other message already uses.)
*
* sk_hermes_publish() (task 3.3, above) enforces this bound directly --
* refuses (-1) any single payload_len over SK_HERMES_CHUNK_MAX_PAYLOAD,
* whether or not it's a chunk carrier, since both cases hold the same
* invariant now.
*
* SENDING a payload over SK_HERMES_CHUNK_MAX_SLICE bytes is deliberately
* NOT a kernel-Hermes API here -- it is a loop a caller writes with the
* primitives below plus sk_hermes_publish(), the same way this task's
* own self-test proves reassembly (kernel_main.c: build N static chunk
* buffers, sk_hermes_publish() each). Building a chunking sender would
* need kernel-Hermes to own chunk-buffer memory with a real lifetime
* (kept alive until every subscriber has drained it, freed only once
* kernel-Hermes has no way to know that) -- a genuine new question this
* task's inert scope does not call for. A real multi-chunk sender is a
* later task's problem, when a real message type actually needs one
* (3.8+, the same boundary chunk-aware drain is already deferred past).
*/
/* SK_HERMES_CHUNK_MAX_PAYLOAD (1024 bytes, 64x16, SXLIV.1) -- the
* uniform bound every message's payload_len must satisfy, chunked or
* not (see this section's own sizing note above) -- is defined earlier
* in this file now, next to SkHermesMessage's own payload_buf field,
* which needs it in scope. Not redefined here. */
/* SkHermesChunkHeader - prepended to a chunk carrier's own payload,
* ahead of up to SK_HERMES_CHUNK_MAX_SLICE bytes of real content.
* msg_id is caller-chosen and must be unique per logical multi-chunk
* send from a given sender -- reassembly groups chunks by it. seq is
* 0-based, ascending, no gaps, {0..n_chunks-1}. is_last is set on
* exactly the chunk whose seq is n_chunks-1, no other. */
typedef struct {
uint32_t msg_id;
uint32_t seq;
int is_last;
} SkHermesChunkHeader;
/* Content bytes per chunk -- the one-block bound minus header overhead. */
#define SK_HERMES_CHUNK_MAX_SLICE (SK_HERMES_CHUNK_MAX_PAYLOAD - (uint32_t)sizeof(SkHermesChunkHeader))
/* sk_hermes_chunk_count - number of SK_HERMES_CHUNK_MAX_SLICE-sized
* chunks `len` bytes needs (ceiling division). 0 bytes needs 0 chunks;
* 1..SK_HERMES_CHUNK_MAX_SLICE needs 1; and so on. Pure arithmetic, no
* side effects -- a caller sizing its own chunk-send loop calls this
* first. */
uint32_t sk_hermes_chunk_count(uint32_t len);
/* sk_hermes_reassemble - concatenates n_chunks chunk carrier messages
* (already drained off a subscriber's own pending queue via
* sk_hermes_pending_peek()/sk_hermes_pending_pop(), task 3.3) sharing
* one msg_id back into one byte-exact buffer at out_buf, in ascending
* seq order regardless of the order chunks[] itself is passed in.
*
* Refuses (-1, *out_len untouched, no partial copy ever lands in
* out_buf) if: n_chunks is 0 or exceeds SK_HERMES_MSG_MAX (the system-
* wide message arena size -- chunking can never need more chunk-carrier
* messages than the whole arena holds); any chunk is NULL, has no
* payload, or its payload is shorter than sizeof(SkHermesChunkHeader);
* the chunks' msg_ids disagree; seq values are not exactly
* {0..n_chunks-1} (a duplicate or a gap); is_last is not set on exactly
* the seq==n_chunks-1 chunk and no other; a non-final chunk's content
* slice is not exactly SK_HERMES_CHUNK_MAX_SLICE bytes, or the final
* chunk's is 0 or over that bound; or the total reassembled length
* would exceed out_buf_cap (checked once, from validated seq
* completeness, before any memcpy -- never order-dependent on which
* chunk happens to overflow first).
*
* @param chunks Array of n_chunks chunk carrier message pointers.
* @param n_chunks Chunk count (from sk_hermes_chunk_count() at send
* time, or simply len(chunks[])).
* @param out_buf Caller-owned destination buffer.
* @param out_buf_cap Capacity of out_buf in bytes.
* @param out_len Set to the reassembled length on success only.
* @return 0 on success, -1 on any refusal above.
*/
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len);
/*
* ACK/NACK and private-channel negotiation (FABRIC-3.6.md task 3.6, B1 /
* FABRIC-3.5.md SXLV.1). "A private channel is created by request ->
* grant/deny over the common channel" -- read as: every VM is a
* common-channel member from birth (task 3.2), so a requester can
* always REACH a target without a prior private channel; the exchange
* itself is point-to-point (sk_hermes_send_one(), task 3.6's own
* extraction above), not a fan-out to the whole common-channel
* membership -- see sk_hermes_send_one()'s own doc comment for why
* fan-out is structurally wrong for an addressed request.
* `messaging.4th`'s own `CH-REQUEST` carried an explicit `to` for the
* same reason; this is the same shape, not a new one.
*
* "A deny is a NACK" (SXLV.1) -- there is no separate CH_DENY type;
* denial IS SK_HERMES_MSG_TYPE_NACK. ACK is sent once, for the
* channel-open + delivery moment (the ruled cadence, 2026-09-21), not
* for every message that follows on the new channel.
*
* The GRANT/DENY *decision* is task 3.7's scope -- the ACL policy hook,
* "kernel-Hermes asks ACL.4th, never decides in C, never gates on
* zuse_session" (CLAUDE.md, SXLV.1). sk_hermes_channel_respond() below
* takes that decision as an explicit caller-supplied `approved` flag
* (named for what it is, not `allow`, so it reads as a caller decision
* passed in, never as policy living in C) -- task 3.7 replaces the CALL
* SITE that produces this flag with a real ACL.4th query; this
* function's own signature does not change.
*/
#define SK_HERMES_MSG_TYPE_CH_REQUEST 20
#define SK_HERMES_MSG_TYPE_CH_GRANT 21
#define SK_HERMES_MSG_TYPE_ACK 22
#define SK_HERMES_MSG_TYPE_NACK 23
#define SK_HERMES_MSG_TYPE_CH_CLOSE 24
/* SK_HERMES_MSG_TYPE_BLK_ATTACH (FABRIC-3.6.md task 3.8, Stage C) --
* DELIBERATELY the same numeric value as messaging.4th's own
* BLK-ATTACH-EVENT constant (9), not a fresh kernel-Hermes-space number
* like the five above. SXXXIV.2's partition rule is "one owner per
* message type, never shared" -- it is about which LAYER holds the
* message, not about the two layers needing disjoint numbering. Once
* kernel-Hermes is this type's sole owner, keeping the same value
* documents that this is a cutover of the SAME logical message, not a
* new one invented alongside it. */
#define SK_HERMES_MSG_TYPE_BLK_ATTACH 9
/* SK_HERMES_MSG_TYPE_CONSOLE_CMD (FABRIC-3.6.md task 3.9, Stage D) --
* same reasoning as SK_HERMES_MSG_TYPE_BLK_ATTACH above: deliberately
* reuses messaging.4th's own CONSOLE-CMD-EVENT value (7), since this is
* that type's real cutover, not a fresh kernel-Hermes-space number. */
#define SK_HERMES_MSG_TYPE_CONSOLE_CMD 7
/* SK_HERMES_MSG_TYPE_ELEVATE_REQUEST (FABRIC-3.6.md task 3.10, Stage D)
* -- reuses messaging.4th's own now-historical ELEVATE-REQUEST value (8).
* At task 3.10, SEND-ELEVATE-REQUEST/ELEVATE-GRANT/CH-REQUEST all FIND
* live in Hera's own dictionary and SEND-ELEVATE-REQUEST was a real,
* complete, directly-callable entrypoint (H.5/H.8's own design: any VM
* wanting word-ACL elevation calls it with its own pubkey and the target
* word name). FABRIC-3.6.md Phase 4, Stage E deleted messaging.4th
* (SEND-ELEVATE-REQUEST's only definition) -- KH-ELEVATE-SEND below has
* had no FORTH caller since, and ELEVATE-GRANT (zuse-eligibility.4th,
* still loaded at boot) is unreachable. Found, not fixed -- Phase 8 PKI
* decision needed before Phase 5 close-out, see FABRIC-3.6.md's own
* Phase 4 entry. */
#define SK_HERMES_MSG_TYPE_ELEVATE_REQUEST 8
/* The five negotiation types above (20-24) were chosen clear of
* messaging.4th's own live/reserved type space (PAUSE-EVENT=2,
* RESUME-EVENT=3, KILL-EVENT=4, CONSOLE-CMD-EVENT=7, ELEVATE-REQUEST=8,
* BLK-ATTACH-EVENT=9, MSG-NACKED=253, MSG-DELIVERED=255) precisely
* because none of them had a real cutover yet -- kernel-Hermes stayed
* its own inert, parallel type space for those. SK_HERMES_MSG_TYPE_
* BLK_ATTACH, _CONSOLE_CMD, and _ELEVATE_REQUEST above are the
* deliberate exceptions: tasks 3.8/3.9/3.10 ARE those types' real
* cutovers, so they reuse the FORTH-side value rather than picking a
* fresh one. */
/* sk_hermes_channel_request - requester asks target for a new private
* channel. A point-to-point CH_REQUEST (sk_hermes_send_one(), tagged
* with SK_HERMES_CHANNEL_COMMON purely for reachability context),
* funded by requester's own reservoir. Creates nothing -- the responder
* decides via sk_hermes_channel_respond() below.
* @return 0 on success, -1 if the send itself was refused. */
int sk_hermes_channel_request(VMUuid requester, VMUuid target);
/* sk_hermes_channel_respond - target answers a pending request from
* requester, `approved` supplied by the caller (see this section's own
* top comment on why that is task 3.7's future call site, not a policy
* decision living here).
*
* Approved: creates a new private channel, subscribes both requester
* and target, sends CH_GRANT to requester (msg->channel = the new
* channel id -- how the requester learns which channel to use) followed
* by one ACK (the ruled channel-open+delivery cadence). If channel
* creation fails (table full) or either subscription fails, the
* partial channel is destroyed and this call falls through to the
* denied path below instead of leaving a half-open channel or a silent
* false grant -- the same "leave no half-state" discipline
* sk_hermes_publish()'s own per-subscriber rollback already uses.
*
* Denied (approved==0, or a failed grant attempt as above): sends NACK
* to requester ("a deny is a NACK", SXLV.1 -- no separate type). No
* channel exists afterward either way.
*
* @return The new channel id (>= 0) on a real grant, or -1 on any deny
* -- both a policy deny and a failed grant attempt leave no
* channel behind, which is what this task's own check asks for.
*/
int sk_hermes_channel_respond(VMUuid target, VMUuid requester, int approved);
/* sk_hermes_channel_close - tears down a private channel. Authority is
* membership alone: either party may close a channel it belongs to
* (narrowest defensible rule given both are already trusted members of
* it -- not gated on anything beyond that, and not itself a ruling).
* Refuses (-1, no effect) if closer is not a member of channel_id, or
* channel_id is SK_HERMES_CHANNEL_COMMON (sk_hermes_channel_destroy()
* itself already refuses the common channel).
* @return 0 on success, -1 on refusal. */
int sk_hermes_channel_close(VMUuid closer, int channel_id);
/*
* Channel-open policy hook (FABRIC-3.6.md task 3.7, B1, ruling 3.0b:
* "kernel-Hermes asks ACL.4th, never decides in C, never gates on
* zuse_session"). sk_hermes_channel_open_policy() is the CALL SITE task
* 3.6's own doc comment named as replacing sk_hermes_channel_respond()'s
* caller-supplied `approved` bool with a real query -- this function IS
* that query; sk_hermes_channel_respond()'s own signature stays
* unchanged, as promised.
*
* Deliberately NOT vm_interpret() (contrast task 3.4's drain, which
* genuinely needs to run arbitrary payload text): HERMES-CHANNEL-OPEN?
* is a FIXED, known word name, so this uses the plain-word-dispatch
* pattern FABRIC-3.md SXX already established for Hera's own MSG-TICK
* self-drain ("a plain word dispatch in her own dictionary, not a
* VM-EXEC dispatch into anyone else's input buffer -- no reentrancy
* risk") -- vm_find_word() + calling the entry's own func pointer
* directly, with the requester passed via the target's own data stack.
* This touches neither target_vm->input_buffer nor input_pos, so it
* carries none of task 3.4's cursor-preservation hazard; it DOES still
* touch target_vm's own data stack (dsp), which is the correct and
* expected thing for a word call to do. Calling this from within a live
* checkpoint on a VM that is itself mid-dispatch is deferred, undecided
* territory -- this task (like every Phase 3 task before Stage C/3.8+)
* is self-test only, not wired into any real checkpoint yet.
*
* Fails CLOSED, not open: if target_vm has no HERMES-CHANNEL-OPEN? word
* (e.g. ACL.4th was never loaded into it) or the word itself sets
* target_vm->error or leaves too few stack items, this returns 0
* (denied) -- absence of policy must never silently mean "always
* allow" (CLAUDE.md's own posture on ACL policy). A policy-word bug's
* own error state is cleared on target_vm before returning, so a
* broken policy word cannot leak an error into whatever else target_vm
* is doing.
*
* @param target_vm The VM being asked -- its own dictionary is
* consulted, its own data stack is used for the call.
* @param requester Passed to HERMES-CHANNEL-OPEN? as (hi, lo) on
* target_vm's own stack.
* @return 1 if HERMES-CHANNEL-OPEN? approved (returned a nonzero top of
* stack), 0 otherwise (denied, missing, or errored).
*/
int sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_KERNEL_HERMES_H */
+169
View File
@@ -0,0 +1,169 @@
/*
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.
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.
*/
/**
* parity.h - Parity packet for VM validation
*
* Defines structures and functions for comparing hosted vs kernel VM state.
* Parity is verified via canonical dictionary hash, not raw memory comparison.
*
* M7 Normative Rules:
* - word_id is monotonic creation index starting at 0
* - Dictionary traversal is creation order (oldest to newest)
* - Hash excludes pointers, padding, runtime fields
* - Colon bodies are hashed as word_id sequences, not addresses
*/
#ifndef STARKERNEL_PARITY_H
#define STARKERNEL_PARITY_H
#include <stdint.h>
#include <stddef.h>
/**
* Bootstrap result codes
*/
#define SK_BOOTSTRAP_OK 0
#define SK_BOOTSTRAP_ARENA_FAIL 1
#define SK_BOOTSTRAP_INIT_FAIL 2
#define SK_BOOTSTRAP_DICT_FAIL 3
/**
* ParityPacket - Summary of VM state for comparison
*
* M7.1a fields are sufficient for bootstrap validation.
* M7.1b fields are added for POST validation.
*/
typedef struct {
/* === M7.1a: Bootstrap Parity === */
uint32_t word_count; /* Number of dictionary entries */
uint32_t here_offset; /* vm->here (bytes used in dictionary) */
uint32_t latest_word_id; /* vm->latest->word_id (stable ID) */
uint64_t header_hash64; /* Canonical dictionary hash (FNV-1a) */
/* === M7.1b: POST Parity === */
uint32_t tests_total; /* Total tests executed */
uint32_t tests_passed; /* Tests passed */
uint32_t tests_failed; /* Tests failed */
uint32_t tests_skipped; /* Tests skipped */
uint32_t tests_errors; /* Tests with errors */
/* === Optional: Rolling Window Hash === */
uint64_t window_hash64; /* Hash of execution history (if deterministic) */
/* === Status === */
int bootstrap_result; /* SK_BOOTSTRAP_* code */
} ParityPacket;
/**
* Forward declaration of VM struct
*/
struct VM;
/*
* sk_parity_collect - Collect parity data from VM
*
* Traverses dictionary in creation order, computes canonical hash.
* Does NOT include runtime fields (execution_heat, physics, etc.).
*
* @param vm Pointer to VM instance
* @param out Pointer to ParityPacket to fill
*/
void sk_parity_collect(struct VM *vm, ParityPacket *out);
/*
* sk_parity_print - Print parity packet to console
*
* Output format:
* PARITY:M7.1a word_count=N here=0xHHHH latest_id=N hash=0xHHHHHHHHHHHHHHHH
* PARITY:M7.1b tests=N pass=N fail=N skip=N err=N
*
* @param pkt Pointer to ParityPacket to print
*/
void sk_parity_print(const ParityPacket *pkt);
/*
* sk_dict_canonical_hash - Compute canonical dictionary hash
*
* Hashes structural fields only:
* - flags, name_len, name[], acl_default, word_id
* - For colon words: body as word_id sequence
*
* Excludes:
* - link (pointer)
* - func (function pointer)
* - execution_heat (runtime)
* - physics (runtime)
* - transition_metrics (pointer)
*
* @param vm Pointer to VM instance
* @return 64-bit FNV-1a hash
*/
uint64_t sk_dict_canonical_hash(struct VM *vm);
/*
* sk_dict_word_count - Count dictionary entries
*
* Traverses from vm->latest to NULL.
*
* @param vm Pointer to VM instance
* @return Number of dictionary entries
*/
uint32_t sk_dict_word_count(struct VM *vm);
/**
* FNV-1a constants
*/
#define FNV1A_64_OFFSET_BASIS 0xCBF29CE484222325ULL
#define FNV1A_64_PRIME 0x100000001B3ULL
/*
* fnv1a_64 - FNV-1a 64-bit hash function
*
* @param data Pointer to data to hash
* @param len Length of data
* @param hash Current hash value (use FNV1A_64_OFFSET_BASIS for initial)
* @return Updated hash value
*/
uint64_t fnv1a_64(const uint8_t *data, size_t len, uint64_t hash);
#endif /* STARKERNEL_PARITY_H */
+589
View File
@@ -0,0 +1,589 @@
/*
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-0.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-0.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 */
#include "starkernel/vm_uuid.h" /* VMUuid -- FABRIC-0.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)
/* `flags` bit 0 -- pinned, exempt from eviction/reap. Moved here from a
* stadium.c-private #define (FABRIC-2.md §H.12 step 3) so session.c's pin-
* authority choke point (session_set_pinned()/session_is_pinned()) can
* write/read this same bit without a duplicate definition. Session is
* authoritative for every EXTERNAL reader (FABRIC-2.md §H.10) -- this bit
* on the raw patron header stays a mirrored copy purely for the Stadium
* engine's own internal eviction/admission logic (stadium.c), which must
* stay self-contained and not call back into session.c. */
#define STADIUM_FLAG_PIN 0x01u
/*
* StadiumPatronHeader - one member of the closed two-valued cell union
* (FABRIC-0.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-0.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. STADIUM_CONTAINS_DEPTH_MAX still has no consumer (item 3.5
* for the depth cap, not yet implemented). STADIUM_CAPACITY_TICK was wired
* in 2026-08-15 (capsule_vm_physics.c's vm_physics_heartbeat_tick(), see
* FABRIC-1.md F.2/§12 Q5) -- this check now proves a real, live constant
* is sane, not just a placeholder, 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, revised 2026-08-15: the per-cell owner array stores a quota-slot
* index. The VM population bound is no longer a compile-time constant (see
* stadium_max_vm_count() below), so this can no longer be a compile-time
* assert -- the owner element type is now uint16_t (65535 slots of
* headroom), and stadium_boot_init() itself clamps the computed count to
* that range at runtime, logging if it ever has to.
*/
/*
* stadium_boot_init - Boot-time allocation (FABRIC-0.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 kmalloc_get_stats().free_bytes at the
* point of the call, rounded down to whole STADIUM_CELL_BYTES cells -- rather
* than a hardcoded count. (Corrected 2026-08-15 from pmm_get_stats(): PMM's
* free-byte figure reflects physical pages not yet handed to any subsystem,
* but kmalloc_init() (M6) already carved its own fixed-size heap out of PMM
* before this ever runs, and every allocation in this function actually
* draws from that kmalloc heap, not raw PMM -- pmm_get_stats() was budgeting
* against a pool nothing here actually allocates from.) Also computes the
* outer Stadium's VM population bound the same way, from the kmalloc heap's
* *remaining* free bytes after the cell array's own allocation: see
* stadium_max_vm_count() below. 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 array (which VM's quota a cell
* belongs to; uint16_t as of 2026-08-15, see the note above) 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.
*
* Also allocates the VM quota array (stadium_quotas), sized to the
* computed stadium_max_vm_count() rather than a compile-time bound.
*
* 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 four 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_max_vm_count - The outer Stadium's VM population bound, computed
* at stadium_boot_init() from the kmalloc heap's remaining free bytes
* (replaces the old compile-time STADIUM_MAX_VM_COUNT, 2026-08-15 -- see
* stadium_boot_init()'s own doc). 0 if not initialized. capsule_birth.c's
* birth gate reads this instead of a macro.
*/
size_t stadium_max_vm_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-0.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-0.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_birth_hera - Admits Hera as a real resident of cell 0 (FABRIC-0.md
* item 3.6's invariant, actually enforced -- item 4.1 found that nothing had
* ever called this until a word patron was about to become the first-ever
* occupant of cell 0 by accident via the free list). Candidate: identity 0,
* heat 0, mass 1, pinned (STADIUM_FLAG_PIN), behaviour COOL. Heat 0 means no
* reservoir transfer is needed -- conservation holds trivially (the
* reservoir keeps the VM's whole share; Hera's own cell contributes 0).
* Being pinned excludes her from every eviction-candidate scan (§3), so the
* stadium_evict() panic guard at STADIUM_HERA_CELL_INDEX stays correctly
* dormant rather than reachable-by-accident.
*
* Idempotent: a second call is a no-op (returns 0) if she is already
* resident. Must be called after stadium_boot_init() and before any word
* ever dispatches (§6) -- kernel_main.c calls it immediately after
* stadium_boot_init(), before M7's VM bootstrap.
*
* @return 0 on success (or already born), -1 if the Stadium is not
* initialized or the admission was refused (should not happen: her
* quota is granted in full, empty, at stadium_boot_init()).
*/
int stadium_birth_hera(void);
/*
* stadium_reservoir_pull - Transfers up to `amount` (Q48.16) out of vm_id's
* reservoir (FABRIC-0.md §17.7's reservoir mechanism). Clamped to what the
* reservoir actually holds -- never goes negative, never invents heat.
* Returns the amount actually pulled, which may be less than requested (or
* 0, e.g. a drained reservoir or an unknown vm_id). Callers that go on to
* fail their own operation (e.g. a refused stadium_admit()) MUST push the
* pulled amount back via stadium_reservoir_push() to preserve
* Σ(resident heat) + reservoir == Q48_ONE across the failed attempt.
*
* @param vm_id Owning VM's id.
* @param amount Requested Q48.16 amount.
* @return Amount actually pulled (0..amount).
*/
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount);
/*
* stadium_reservoir_push - Credits `amount` (Q48.16) back into vm_id's
* reservoir. The other half of every reservoir transfer (§17.7): cooling
* returns heat here, a refused starter-grant rolls back here, and
* stadium_evict() credits a departing patron's remaining heat here before
* the cell returns to the free list -- the invariant is a transfer, never a
* reset. No-op if vm_id has no quota (caller contract; mirrors
* stadium_admit()'s silent refusal for the same case).
*
* @param vm_id Owning VM's id.
* @param amount Q48.16 amount to credit.
*/
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount);
/*
* stadium_reservoir_peek - Read-only: vm_id's current reservoir balance
* (Q48.16), for diagnostics/conservation checks. Does not mutate state.
* Returns 0 for an unknown vm_id -- indistinguishable from a genuinely
* drained reservoir, same as stadium_reservoir_pull()'s 0 return; callers
* that need to tell those apart must already know whether vm_id has a
* quota (e.g. via the same check they'd use before calling stadium_admit()).
*
* @param vm_id Owning VM's id.
* @return Current reservoir balance, or 0 if vm_id has no quota.
*/
uint64_t stadium_reservoir_peek(VMUuid vm_id);
/*
* stadium_quota_slot_for_vm - Read-only: vm_id's quota slot index (0 to
* stadium_max_vm_count()-1), for callers outside stadium.c that need to key
* their own per-VM state the same way stadium.c's internal arrays already
* do (FABRIC-0.md §25.5 item 4.2 -- stadium_words.c's word_id -> cell_index
* map needs this to stop colliding across VMs; word_id is scoped per-VM,
* not globally unique, so a single shared map aliases different VMs' words
* onto each other's Stadium cells and reservoirs).
*
* @param vm_id VM to look up.
* @return Quota slot index, or -1 if vm_id holds no quota.
*/
int stadium_quota_slot_for_vm(VMUuid vm_id);
/*
* stadium_resident_sum - Read-only: sum of heat across every cell currently
* resident AND owned by vm_id's own quota (FABRIC-0.md §25.5 item 4.2 --
* boot diagnostics need this filtered per-VM once a second VM holds a
* quota; summing every resident cell regardless of owner, as the pre-4.2
* diagnostic did, mixes two VMs' conservation totals together).
* Returns 0 for an unknown vm_id, same convention as stadium_reservoir_peek().
*
* @param vm_id Owning VM's id.
* @return Sum of resident heat owned by vm_id (Q48.16), or 0 if vm_id has no quota.
*/
uint64_t stadium_resident_sum(VMUuid vm_id);
/*
* stadium_conserved - Boolean analogue of vm_physics_conserved(), for the
* Stadium per-VM quota invariant rather than fleet-wide execution heat
* (FABRIC-3.5.md §XXXIX.4, item 41). Checks, epsilon zero:
*
* stadium_resident_sum(vm_id) + stadium_reservoir_peek(vm_id) == Q48_ONE
*
* FABRIC-3.5.md §XL.4: the full invariant also has a per-VM `consumed`
* term (task 2.7), recorded by kernel-Hermes when its messages decay:
*
* stadium_resident_sum + stadium_reservoir_peek + stadium_consumed_peek
* == Q48_ONE
*
* The two-term form above is the special case consumed == 0.
*
* Returns 0 (not conserved) for an unknown vm_id, same convention as
* stadium_reservoir_peek()/stadium_resident_sum() returning 0 for one.
*
* @param vm_id VM to check.
* @return Non-zero if vm_id's Stadium quota is exactly conserved, 0 otherwise.
*/
int stadium_conserved(VMUuid vm_id);
/*
* stadium_consumed_record - Add amount to vm_id's per-VM consumed total
* (heat destroyed by decay; FABRIC-3.5.md §XL.4). No-op if vm_id has no
* quota. Caller (kernel-Hermes decay) must have removed the same amount
* from a resident patron's heat.
*/
void stadium_consumed_record(VMUuid vm_id, uint64_t amount);
/* stadium_consumed_peek - vm_id's consumed total, 0 for an unknown vm_id. */
uint64_t stadium_consumed_peek(VMUuid vm_id);
/*
* stadium_evict - Reap the patron header at cell_index (FABRIC-0.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-0.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-0.md §22.3, item 3.7: "each VM holds its own free-list head index
* into the global array"). Linearly searched by vm_id -- a VMUuid (item 3.8)
* can't be used as a direct array index anyway. Was a small, compile-time-
* bounded table (linear scan "costs nothing" at the old default of 4);
* since 2026-08-15 the table is sized at boot from stadium_max_vm_count()
* and could genuinely be large, so this scan is no longer assumed free --
* flagged here rather than silently carried forward as still-obviously-fine,
* though no algorithmic change was made in this pass. 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-0.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);
/*
* stadium_grant_quota - One-time initial quota grant for a newly born VM
* (FABRIC-0.md item 4.1a). NOT item 1.3's recurring capacity-transfer
* arbitration -- that mechanism (density-gradient-driven, per capacity-tick)
* stays unbuilt and its "how much moves" question stays open. This is the
* narrower, one-time event: the same shape as Hera's own whole-pool grant at
* stadium_boot_init(), just from an existing VM's free list instead of the
* boot-time global one.
*
* Splits from_vm_id's free list evenly by cell count (new VM gets the first
* half by list-walk order; from_vm_id keeps the remainder, including any odd
* cell). Reassigns stadium_owner[] for every cell that moves. Touches no
* resident cell on either side -- only free-list cells move, so from_vm_id's
* residents (including a pinned cell 0, if from_vm_id is Hera) are
* unaffected. Grants new_vm_id a fresh Q48_ONE reservoir -- NOT a fraction of
* from_vm_id's, since conservation is per-VM (see StadiumVMQuota.reservoir's
* doc in stadium.c), not a shared pool split across VMs.
*
* REFUSES (returns -1, does not crash) if: the Stadium is not initialized;
* new_vm_id already holds a quota; from_vm_id holds no quota; from_vm_id's
* free list has fewer than 2 cells (nothing to split); or no empty quota
* slot remains (stadium_max_vm_count() exhausted).
*
* @param new_vm_id The VM receiving a fresh quota. Must not already have one.
* @param from_vm_id The VM whose free list is split. Must already hold a quota.
* @return 0 on success, -1 if refused.
*/
int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id);
/*
* stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: the currently
* in-use VM with the largest free (unclaimed) cell count right now, i.e.
* the VM stadium_grant_quota()'s `from_vm_id` argument should be for a new
* VM's initial grant. Callers should NOT hardcode vm_uuid_hera() here --
* always splitting from Hera specifically converges her own free list
* toward empty after a bounded number of grants (each halves what remains)
* even while other, previously-granted VMs still hold nearly all of their
* own share untouched, silently starving later births though the Stadium
* as a whole has plenty of spare capacity. O(stadium_max_vm_count()) --
* a scan over quota slots (bounded by live VM population), not cells.
*
* @return The VM with the most free cells, or vm_uuid_none() if no VM
* holds a quota yet (Stadium not initialized, or called before
* Hera's own boot-time grant/admission).
*/
VMUuid stadium_best_donor(void);
/*
* stadium_cell_heat_get - Read a resident cell's own heat (FABRIC-0.md item
* 4.2's fourth ruling). Requires cell_index to be resident AND owned by
* vm_id's quota -- returns 0 otherwise (out of range, not resident, or
* belongs to a different VM), same ambiguity-with-a-genuine-zero already
* accepted by stadium_reservoir_peek()'s doc: callers that need to
* distinguish "refused" from "actually zero" must already know the cell is
* theirs (e.g. from their own resident-cell tracking), same contract as
* every other implicit-self primitive here.
*
* @param vm_id Calling VM's own identity.
* @param cell_index Index of the resident patron header to read.
* @return The cell's current heat (Q48.16), or 0 if refused.
*/
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index);
/*
* stadium_cell_heat_set - Write a resident cell's own heat, reconciling the
* reservoir delta atomically (FABRIC-0.md item 4.2's fourth ruling). Same
* ownership requirement as stadium_cell_heat_get(). If new_heat is higher
* than the cell's current heat, pulls the exact difference from vm_id's own
* reservoir first -- refuses (returns -1, no mutation) if the reservoir
* cannot cover the full increase, never a partial credit that would invent
* heat. If new_heat is lower, pushes the exact difference back to the
* reservoir after writing. Equal is a no-op success. This is the only
* sanctioned way to change a resident cell's heat post-admission -- doing
* the reservoir accounting here, not leaving it to the FORTH caller, is the
* whole reason this primitive exists rather than a raw field poke.
*
* @param vm_id Calling VM's own identity.
* @param cell_index Index of the resident patron header to write.
* @param new_heat The heat value to set (Q48.16).
* @return 0 on success, -1 if refused (not owned/resident, or insufficient
* reservoir for an increase).
*/
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_STADIUM_H */
@@ -0,0 +1,124 @@
/*
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_blocks.h - Block patrons on the Stadium (FABRIC-2.md §B/§D,
* MIGRATE punch-list item)
*
* The block-specific layer on top of the generic L0 engine (stadium.h), same
* relationship stadium_words.h/.c already has: nothing in stadium.c/.h knows
* a block patron exists -- it only ever sees cell_index, VMUuid, and
* StadiumPatronHeader. This file is where "block" becomes a concrete
* meaning: an (owning quota slot, LBN) -> cell_index map, the starter-grant
* admission rule (mirrors stadium_word_dispatch()'s Option B exactly), and
* the reservoir-quantum touch/cool that feeds and drains a resident block's
* Stadium heat.
*
* Unlike words, LBN is not densely bounded (block_subsystem.c's unified LBN
* space spans RAM/RAMDRIVE/DISK/USB and can be large), so the map here is a
* fixed-capacity open-addressing hash table sized off stadium_cell_count()
* at init, not a dense per-VM array -- see stadium_blocks.c for the layout.
* A block's actual 1024 content bytes are never copied into a Stadium cell;
* they stay exactly where block_subsystem.c already keeps them. The cell
* only ever carries identity (the LBN) and heat/bookkeeping, same as a word
* patron's cell never carries the word's own dictionary entry.
*/
#ifndef STARKERNEL_VM_STADIUM_BLOCKS_H
#define STARKERNEL_VM_STADIUM_BLOCKS_H
#ifdef __STARKERNEL__
#include <stdint.h>
#include "starkernel/vm_uuid.h"
/*
* stadium_blocks_init - Allocates and zeroes the (quota slot, LBN) ->
* cell_index hash table (capacity computed from stadium_cell_count() *
* STADIUM_BLOCK_TRACK_CAP_MULT at call time, kmalloc'd). Must be called
* after stadium_boot_init() -- so stadium_cell_count() is non-zero -- and
* before any block ever dispatches; the real boot site is immediately after
* the existing stadium_words_init() call (kernel_main.c), same M7/M7.1
* ordering. Not safe to call twice -- guarded internally as a no-op if
* already initialized, same convention as stadium_words_init().
*/
void stadium_blocks_init(void);
/*
* stadium_block_dispatch - The per-touch entry point, called from
* block_word_block()/block_word_buffer()/block_word_update()
* (src/word_source/block_words.c) -- mirroring stadium_word_dispatch()'s
* call pattern and cooling/admission logic 1:1, keyed by LBN instead of
* word_id.
*
* If (vm_id, lbn) is already resident: applies the same redirected Loop #3
* cooling stadium_word_dispatch() applies (fraction of the cell's own
* current heat, scaled by elapsed_ticks since this block's own last touch --
* STADIUM_BLOCK_COOL_RATE_Q48), crediting the cooled amount back to vm_id's
* reservoir, then pulls STADIUM_BLOCK_HEAT_QUANTUM from the reservoir into
* the cell -- clamped to the reservoir's actual balance AND to the same
* Q48_ONE / 3 floor stadium_word_dispatch() enforces, so block-touch
* admission alone can never starve other reservoir consumers sharing the
* same VM.
*
* If not resident (or the table's entry is stale -- self-healing check
* against the cell's discriminator bit and identity, same pattern
* resolve_resident_cell() uses in stadium_words.c): attempts starter-grant
* admission -- pulls STADIUM_BLOCK_HEAT_QUANTUM (same floor), builds an
* unpinned MIGRATE candidate (identity = lbn, mass = 1, payload unused --
* the block's real content is never copied here), calls stadium_admit(). On
* refusal, pushes the pulled quantum back (rollback). On success, records
* the mapping. If the hash table itself is full and has no slot for this
* (vm_id, lbn) pair, this touch is silently skipped -- Stadium's own
* capacity already bounds real residency, so a table miss under load is
* graceful degradation, not an error.
*
* No-op if vm_id holds no Stadium quota, or stadium_blocks_init() has not
* run.
*
* @param vm_id Owning VM -- vm->stadium_vm_id at every call site,
* same quota-isolation reasoning stadium_word_
* dispatch() already documents (LBN numbering is
* global, not per-VM, but quota scoping still keeps
* two VMs' admissions from evicting each other).
* @param lbn Logical block number being touched.
* @param heartbeat_ticks Current vm->heartbeat.tick_count.
*/
void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks);
/*
* stadium_blocks_print_boot_diagnostics - Console output mirroring
* stadium_words_print_boot_diagnostics(): promotions/evictions for vm_id's
* own block-touch table, plus a conservation check
* (Σ(resident block heat) + reservoir against Q48_ONE is NOT a standalone
* invariant here -- word-execution heat and any other resident shares the
* same reservoir, so this prints the block-only resident sum as a
* diagnostic term, not a claim that it alone should equal Q48_ONE).
*
* @param vm_id The VM whose block-touch table/reservoir to read.
*/
void stadium_blocks_print_boot_diagnostics(VMUuid vm_id);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_STADIUM_BLOCKS_H */
@@ -0,0 +1,180 @@
/*
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.h - Word patrons on the Stadium (FABRIC-0.md §17.3/§17.7,
* punch list item 4.1)
*
* The word-specific layer on top of the generic L0 engine (stadium.h).
* Nothing in stadium.c/.h knows a word patron exists -- it only ever sees
* cell_index, VMUuid, and StadiumPatronHeader. This file is where "word"
* becomes a concrete meaning: a word_id -> cell_index map (kernel-side,
* deliberately NOT a DictEntry field, decided 2026-08-05), the starter-grant
* admission rule (Option B), and the reservoir-quantum touch/cool that feed
* and drain a resident word's Stadium heat.
*
* `execution_heat` and `dict_hash` are untouched by anything in this file
* (§17.7) -- this is a second, independent conserved quantity living in the
* Stadium cell's `heat` field, not a representation of the first.
*/
#ifndef STARKERNEL_VM_STADIUM_WORDS_H
#define STARKERNEL_VM_STADIUM_WORDS_H
#ifdef __STARKERNEL__
#include <stdint.h>
#include "starkernel/vm_uuid.h"
/*
* stadium_words_init - Allocates and zeroes the word_id -> cell_index map
* (DICTIONARY_SIZE entries per VM quota slot, stadium_max_vm_count() slots,
* kmalloc'd -- was a flat static array before 2026-08-15, when the VM count
* bound became RAM-derived rather than a compile-time constant). Must be
* called after stadium_boot_init() and stadium_birth_hera(), before any word
* ever dispatches. NOT safe to call twice -- unlike the old zero-only
* version, a second call would kmalloc a second set of tables and leak the
* first; guarded internally as a no-op if already initialized. Nothing
* calls it twice today.
*
* item 4.2 (FABRIC-0.md §25.5): the map is keyed by quota slot, not just
* word_id -- word_id is assigned per-VM (vm->next_word_id), not globally
* unique, so a single shared word_id -> cell_index map aliased different
* VMs' words onto each other's Stadium cells and reservoirs the moment a
* second VM (Hermes) held a quota. One system-wide init call still covers
* every slot; no per-VM init call is needed.
*/
void stadium_words_init(void);
/*
* stadium_word_dispatch - The per-dispatch entry point (FABRIC-0.md §17.7),
* called once per DictEntry touched at each of vm_core.c's three
* physics_execution_heat_increment() call sites -- deliberately mirroring
* that function's existing call pattern 1:1, including the entry != canon
* double-touch case, rather than inventing a different shape.
*
* If word_id is already resident: applies the redirected Loop #3 cooling
* (fraction of the cell's own current heat, scaled by elapsed_ticks since
* this word's own last touch -- STADIUM_WORD_COOL_RATE_Q48) crediting the
* cooled amount back to vm_id's reservoir, then pulls
* STADIUM_WORD_HEAT_QUANTUM from the reservoir into the cell -- clamped to
* what the reservoir actually holds AND to a floor of Q48_ONE / 3 that
* word-execution admission alone may never dip the reservoir below
* (FABRIC-0.md §25.7, Captain Bob's ruling 2026-08-06: this pull fires on
* EVERY dispatch, not just first admission, and without a floor exhausts a
* VM's entire reservoir in ~32 dispatches, starving any application-level
* economy -- e.g. item 4.2's Hermes -- sharing the same VM's reservoir).
* Application-level pulls (stadium_reservoir_pull() called directly) are
* not subject to this floor.
*
* If word_id is not resident (or the map's entry is stale -- self-healing
* check against the cell's discriminator bit and identity, covers both a
* prior eviction and a FORGET/redefine word_id reuse this function did not
* itself clear): attempts Option B starter-grant admission -- pulls
* STADIUM_WORD_HEAT_QUANTUM from the reservoir (same floor as above),
* builds an unpinned COOL candidate, calls stadium_admit(). On refusal,
* pushes the pulled quantum back (rollback, preserves conservation across
* the failed attempt). On success, records the mapping and increments the
* promotion counter.
*
* No-op if word_id == WORD_ID_INVALID, word_id >= DICTIONARY_SIZE, or vm_id
* holds no Stadium quota.
*
* @param vm_id Owning VM -- vm->stadium_vm_id at every call site.
* Scopes the word_id -> cell_index lookup to this
* VM's own quota slot (item 4.2, FABRIC-0.md §25.5) so
* two VMs' independently-numbered word_ids cannot
* alias onto each other's cells/reservoirs.
* @param word_id The dispatching DictEntry's stable word_id.
* @param heartbeat_ticks Current vm->heartbeat.tick_count (virtual tick,
* never wall-clock -- same convention as every other
* decay computation in this tree).
*/
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks);
/*
* stadium_word_forget - Coherence hook for FORGET (word_id recycling).
* vm_dictionary_untrack_entry() must call this BEFORE the word_id is pushed
* onto vm->recycled_word_ids -- otherwise the next word assigned the same
* recycled id would alias onto the forgotten word's still-resident cell and
* its stale heat (same failure class as the 2026-08-02 block_words.c
* aliasing bug). Evicts the cell if word_id is resident (crediting its heat
* back to the reservoir via stadium_evict()'s own credit path) and clears
* the map entry. No-op if word_id is not resident, out of range, or vm_id
* holds no Stadium quota.
*
* @param vm_id Owning VM -- vm->stadium_vm_id (item 4.2, FABRIC-0.md §25.5:
* scopes the lookup to this VM's own word_slots, same
* reason stadium_word_dispatch() takes it).
* @param word_id The DictEntry's word_id, about to be recycled.
*/
void stadium_word_forget(VMUuid vm_id, uint32_t word_id);
/*
* stadium_words_resident_heat - Sum of heat held by vm_id's own
* word-execution residents only (item 4.1's cells, tracked in this file's
* own word_slots map) -- NOT messages/channels/other application residents,
* which stadium_resident_sum() (stadium.h, item 4.2) mixes in alongside
* everything else a VM owns. Exists so a VM's own application-level
* conservation check (e.g. Hermes's HERMES-K, FABRIC-0.md §25.7, Captain
* Bob's ruling 2026-08-06) can add this as an explicit term instead of
* silently omitting word-execution heat it has no other way to see.
*
* Walks all DICTIONARY_SIZE word_slots for vm_id's quota slot; each
* resident entry contributes its cell's current heat, verified live against
* the discriminator bitmap (same self-healing pattern as
* resolve_resident_cell() -- a stale map entry contributes 0, not garbage).
*
* @param vm_id The VM whose word-execution residents to sum.
*/
uint64_t stadium_words_resident_heat(VMUuid vm_id);
/*
* stadium_words_stats - Promotion/eviction counters (same shape as the old
* cache's HotwordsStats.promotions/.evictions, not that struct -- §25.5's
* acceptance for item 4.1). Promotion = a successful starter-grant
* admission. Eviction = this word's cell was reaped by another admission's
* eviction fallback (stadium_admit()'s density comparison), detected
* lazily via the self-healing stale check in stadium_word_dispatch(), or
* explicitly via stadium_word_forget(). Scoped to vm_id's own quota slot
* (item 4.2) -- counters are no longer system-wide.
*/
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions);
/*
* stadium_words_print_boot_diagnostics - Console output satisfying item
* 4.1's "observable via a diagnostic word or boot console output"
* acceptance line. Prints promotions/evictions, then
* Σ(resident heat) + reservoir against Q48_ONE as a conservation check --
* not required by the acceptance text, but the mechanism proves nothing if
* this silently doesn't hold. The heat sum is scoped to vm_id's own quota
* (stadium_resident_sum(), item 4.2, FABRIC-0.md §25.5) so two VMs' checks
* close independently instead of mixing both VMs' resident heat together.
*
* @param vm_id The VM whose reservoir to read (vm_uuid_hera() today).
*/
void stadium_words_print_boot_diagnostics(VMUuid vm_id);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_STADIUM_WORDS_H */
+60
View File
@@ -0,0 +1,60 @@
/*
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.
*/
/**
* switch.h - Cooperative VM context switch (FABRIC-3.md §XXVIII, Stage 2,
* 2026-09-13).
*
* sk_vm_context_switch() suspends `from`'s execution exactly where it is
* (mid-C-call-stack, on from's own native stack -- Stage 1) and resumes
* `to` -- either for the first time ever (a synthesized initial frame,
* entering sk_vm_switch_entry()) or exactly where `to` was itself last
* switched out. Returns to the caller only once something later switches
* back to `from` -- from from's own point of view, this call simply
* takes a while to return, like any blocking call.
*
* Cooperative only: nothing here is interrupt-driven yet (that's Stage
* 3), so this is safe to call only from ordinary FORTH word dispatch,
* never from ISR context.
*/
#ifndef STARKERNEL_VM_SWITCH_H
#define STARKERNEL_VM_SWITCH_H
#ifdef __STARKERNEL__
struct VM;
int sk_vm_context_switch(struct VM *from, struct VM *to);
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently physically running, tracked by the switch mechanism itself --
* see switch.c's own doc comment on g_switch_current_vm for why this
* exists instead of reusing vm_log_attributed_vm(). sk_vm_switch_set_
* current() seeds the initial value (whoever is running before any
* switch has ever happened) -- call once, at Stage 3 registration time. */
struct VM *sk_vm_switch_current_vm(void);
void sk_vm_switch_set_current(struct VM *vm);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_SWITCH_H */