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:
@@ -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.
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user