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

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

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

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

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

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

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+30
View File
@@ -0,0 +1,30 @@
# src/starkernel/vm/
Kernel-side StarForth VM subsystem.
- `vm_core.c` — kernel port of the core interpreter loop.
- `vm_runtime.c` — kernel VM runtime support.
- `vm_bootstrap.c` — kernel VM bootstrap (distinct from
`bootstrap/sk_vm_bootstrap.c`, which owns the higher-level subsystem
wiring called from `kernel_main.c`).
- `arena.c` — capsule arena allocator (fixed-region allocation for capsule
payload data).
- `parity.c` — birth/execution parity logging (VM ID + capsule hash +
dictionary hash), enabling offline determinism verification.
- `alloc_kernel.c` — the kernel-side implementation of the VM's allocator
interface actually referenced by `Makefile.starkernel`'s build (see
`src/platform/README.md` for a note on a second, unwired copy).
- `q48_stubs.c` — deliberate no-op stubs for `q48_from_double()`/
`q48_to_double()` in the freestanding kernel build, where floating point
is unavailable; always returns zero, safe because shared VM code only
uses these results for logging/diagnostic paths that are inactive in
the kernel build.
- `vm_internal.h` — internal-only declarations shared across this
directory's files.
Subdirectories:
- `bootstrap/sk_vm_bootstrap.c` — owns VM init + capsule loading wiring,
called from `kernel_main.c`; `kernel_main.c` itself only owns hardware
milestones.
- `host/shim.c` — host-service shim bridging the kernel VM to
`src/starkernel/hal/host_services.c`.
+233
View File
@@ -0,0 +1,233 @@
/*
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.
*/
/*
* platform/alloc_kernel.c - Kernel (bare-metal) memory allocator
*
* FABRIC-3.md §X.4, 2026-09-07: this used to be its own isolated static
* 4MB arena (first-fit free list, no splitting/coalescing) -- sized small
* enough, and fragile enough under concurrent VM churn, that a live
* identity-heap-capacity test found the arena topping out around 6
* concurrent WIREBIND-born VMs, failing *before* true capacity exhaustion
* (fragmentation from concurrent background allocation, no coalescing to
* recover from it). Meanwhile `kmalloc.c` -- the kernel's general heap,
* already initialised at boot (M6, well before any VM is ever born) --
* sits right next to it: reserved from real PMM-tracked physical memory
* (not a fixed compile-time array), defaults to a 2 GiB floor explicitly
* sized "for 256+ baby VMs" per its own comment, overridable via the
* `--heap=` boot flag, and its free list *does* coalesce neighboring
* blocks on every free.
*
* Every VM's word dictionary (`vm_create_word()`, the shared/vendored VM
* core) allocates through this file's `sf_malloc()`/`sf_free()` --
* `platform_alloc.h`'s portable allocator abstraction, kernel-side. This
* file now simply delegates to `kmalloc()`/`kfree()` instead of managing
* its own separate, much smaller arena: same API contract callers already
* depend on, real headroom (whatever the boot-time heap ends up being,
* not a hardcoded 4MB), and real coalescing. No new allocator was
* invented -- `kmalloc.c` already existed, was already boot-tested on all
* three arches, and was simply never wired up as the backing store for
* VM dictionaries specifically.
*
* `sf_alloc_init()` is now a no-op (see its own doc comment below): with
* a *shared* heap serving every kernel subsystem, not an isolated arena
* used only for VM dictionaries, "reset" is no longer a sane operation --
* nothing external ever called it anyway (confirmed: no callers besides
* `sf_malloc()`'s own lazy-init guard, which this file's `sf_malloc()`
* still keeps, now as a readiness check rather than an initializer).
*
* This file's own alignment guarantee (`SF_ALIGN`, 8 bytes) is preserved
* via `kmalloc_aligned()` -- `kmalloc()`'s own default alignment
* (`KMALLOC_MIN_ALIGN`, 16 bytes) already satisfies it, but asking
* explicitly keeps this file's contract self-documenting rather than
* relying on kmalloc.c's own default not changing under it.
*/
#include "platform_alloc.h"
#include "starkernel/kmalloc.h"
#include <stdint.h>
#include <stddef.h>
/* Alignment for all allocations (8 bytes for 64-bit safety) -- unchanged
* from this file's previous arena-based implementation. */
#define SF_ALIGN 8
/* alloc_count/free_count have no kmalloc.c equivalent (it tracks bytes,
* not call counts) -- kept here as simple diagnostic counters layered on
* top of kmalloc's own byte-accurate stats, which sf_alloc_get_stats()
* below reads fresh on every call rather than shadowing them locally. */
static size_t g_alloc_count = 0;
static size_t g_free_count = 0;
/**
* @brief No-op: kept only for API compatibility with `platform_alloc.h`.
*
* The backing store is now the shared kernel heap (`kmalloc.c`), already
* initialised at boot (M6) well before any VM allocation can occur --
* there is nothing left for this function to set up, and "resetting" a
* heap shared by every other kernel subsystem would be actively wrong.
* Kept callable (matches its documented "safe to call at any point"
* contract) so no caller needs to change.
*
* @return 0 always (cannot fail)
*/
int sf_alloc_init(void)
{
return 0;
}
/*
* @brief Allocate memory from the shared kernel heap.
*
* Delegates to `kmalloc_aligned()` (`SF_ALIGN`-byte aligned, matching
* this file's previous guarantee). Returns @c NULL for zero-size
* requests and whenever the shared heap itself returns NULL (not yet
* initialised, or genuinely out of memory) -- both already part of this
* function's documented contract.
*
* @param size Number of bytes to allocate.
* @return Pointer to the allocated block on success, @c NULL on failure.
*/
void* sf_malloc(size_t size)
{
if (size == 0) return (void*)0;
void *ptr = kmalloc_aligned(size, SF_ALIGN);
if (ptr) g_alloc_count++;
return ptr;
}
/*
* @brief Allocate and zero-initialise a contiguous array from the kernel heap.
*
* Unchanged from this file's previous implementation: computes
* @p count × @p size, checks for integer overflow via
* @c total/size != count, then delegates to @c sf_malloc(). The returned
* block is zeroed with a manual byte loop rather than @c memset so that
* the kernel build remains freestanding with no libc dependency.
* Returns @c NULL when either argument is zero, on overflow, or on heap
* exhaustion.
*
* @param count Number of elements to allocate.
* @param size Size of each element in bytes.
* @return Pointer to the zeroed block on success, @c NULL on failure.
*/
void* sf_calloc(size_t count, size_t size)
{
if (count == 0 || size == 0) return (void*)0;
size_t total = count * size;
/* Check for overflow */
if (size != 0 && total / size != count)
{
return (void*)0;
}
void* ptr = sf_malloc(total);
if (ptr)
{
/* Zero-initialize */
uint8_t* p = (uint8_t*)ptr;
for (size_t i = 0; i < total; i++)
{
p[i] = 0;
}
}
return ptr;
}
/*
* @brief Resize an allocation in the kernel heap.
*
* Unchanged from this file's previous implementation: `kmalloc.c` has no
* realloc-equivalent, so a fresh block of @p new_size bytes is allocated
* and returned; the original block at @p ptr is left for the caller to
* free explicitly (this function does not free it, matching this file's
* prior documented behavior exactly -- not a regression introduced by
* the kmalloc.c switch).
*
* Special cases match the C standard:
* - @p ptr == @c NULL → equivalent to @c sf_malloc(@p new_size).
* - @p new_size == 0 → returns @c NULL (caller treats old block as freed).
*
* The caller is responsible for copying content from the old block
* before discarding the old pointer; this function does not perform the
* copy.
*
* @param ptr Pointer to the existing allocation (may be @c NULL).
* @param new_size Desired size of the new block in bytes.
* @return Pointer to the new block on success, @c NULL on failure or when
* @p new_size is zero.
*/
void* sf_realloc(void* ptr, size_t new_size)
{
if (!ptr) return sf_malloc(new_size);
if (new_size == 0) return (void*)0;
return sf_malloc(new_size);
}
/*
* @brief Release a kernel heap allocation, making it available for reuse.
*
* Delegates to @c kfree(), which coalesces the freed block with any free
* neighbors -- real reclamation, unlike this file's previous first-fit-
* no-splitting free list (FABRIC-3.md §X.4: that lack of coalescing was
* the proximate cause of a live-observed allocation failure under
* concurrent VM churn even with more than enough free bytes overall).
*
* @param ptr Pointer previously returned by @c sf_malloc() / @c sf_calloc()
* (may be @c NULL; silently ignored, matching @c kfree()'s own
* contract). Must not be used again by the caller after this
* call, and must not be freed twice.
*/
void sf_free(void* ptr)
{
if (!ptr) return;
kfree(ptr);
g_free_count++;
}
/*
* @brief Retrieve a snapshot of kernel allocator statistics.
*
* Reads fresh from `kmalloc_get_stats()` on every call -- the shared heap
* is the real source of truth, not a value shadowed locally. alloc_count/
* free_count (which kmalloc.c does not track) come from this file's own
* counters instead.
*
* @param stats Output buffer to receive the statistics; silently returns
* without writing if @p stats is @c NULL.
*/
void sf_alloc_get_stats(sf_alloc_stats_t* stats)
{
if (!stats) return;
kmalloc_stats_t k = kmalloc_get_stats();
stats->total_bytes = (size_t)k.total_bytes;
stats->used_bytes = (size_t)k.used_bytes;
stats->peak_bytes = (size_t)k.peak_bytes;
stats->alloc_count = g_alloc_count;
stats->free_count = g_free_count;
}
+367
View File
@@ -0,0 +1,367 @@
/*
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.c - StarKernel VM arena initialization
*
* Provides kernel-specific VM initialization with PMM-backed arena.
* This replaces the hosted vm_init() for kernel builds.
*
* M7 Non-Negotiable: VM arena is PMM-backed contiguous pages.
*/
#ifdef __STARKERNEL__
#include "starkernel/vm/arena.h"
#include "vm_host.h"
#include "pmm.h"
#include "vmm.h"
#include "console.h"
#include "starkernel/hal/hal.h"
#include "dictionary_management.h"
#include <stddef.h>
#include <stdint.h>
/* Forward declaration - we'll need the VM struct */
/* For now, define minimal interface; full integration comes later */
/* aarch64/riscv64: UEFI provides 1:1 phys=virt mapping after ExitBootServices.
* VMM installs page tables only on amd64 (via CR3); on aarch64/riscv64 the
* high virtual address is never mapped. Use physical address directly. */
#if defined(ARCH_AARCH64) || defined(__riscv)
#define SK_VM_ARENA_VADDR 0x0ULL /* sentinel: use paddr as vaddr */
#else
#define SK_VM_ARENA_VADDR 0xFFFF900000000000ULL
#endif
#define SK_VM_GUARD_SIZE PMM_PAGE_SIZE
#define SK_VM_GUARD_PATTERN_HEAD 0x5ac0ffeed0c0ffeeULL
#define SK_VM_GUARD_PATTERN_TAIL 0x0bad0badf00df00dULL
/* VM memory size from hosted VM (5 MB) */
#define SK_VM_MEMORY_SIZE (5ULL * 1024ULL * 1024ULL)
#define SK_VM_TOTAL_SIZE (SK_VM_MEMORY_SIZE + (2ULL * SK_VM_GUARD_SIZE))
#define SK_VM_ARENA_PAGES ((SK_VM_TOTAL_SIZE + PMM_PAGE_SIZE - 1) / PMM_PAGE_SIZE)
/* Arena state */
static uint64_t vm_arena_paddr = 0;
static uint64_t vm_arena_guard_vaddr = 0;
static uint64_t vm_arena_client_vaddr = 0;
static int vm_arena_initialized = 0;
static void guard_fill(uint64_t base)
{
uint64_t *ptr = (uint64_t *)(uintptr_t)base;
ptr[0] = SK_VM_GUARD_PATTERN_HEAD;
ptr[(SK_VM_GUARD_SIZE / sizeof(uint64_t)) - 1] = SK_VM_GUARD_PATTERN_TAIL;
}
static int guard_check(uint64_t base)
{
const uint64_t *ptr = (const uint64_t *)(uintptr_t)base;
if (ptr[0] != SK_VM_GUARD_PATTERN_HEAD) {
return 0;
}
if (ptr[(SK_VM_GUARD_SIZE / sizeof(uint64_t)) - 1] != SK_VM_GUARD_PATTERN_TAIL) {
return 0;
}
return 1;
}
/**
* sk_vm_arena_alloc - Allocate PMM-backed VM arena
*
* Allocates contiguous physical pages and maps them RW+NX.
* Returns virtual address of arena, or 0 on failure.
*/
uint64_t sk_vm_arena_alloc(void) {
uint64_t paddr, guard_vaddr, client_vaddr;
/* Fresh PMM allocation every call — each VM owns its own arena.
* The singleton guard was removed because baby VMs (Artemis, Hermes)
* share Mama's physical pages when sk_vm_arena_alloc returns the cached
* address; their vm->here=0 at init overwrites Mama's compiled words and
* their FILL operations zero Mama's word bodies (riscv64 K-PUSH crash). */
paddr = pmm_alloc_contiguous(SK_VM_ARENA_PAGES);
if (paddr == 0) {
console_println("sk_vm_arena_alloc: PMM allocation failed");
return 0;
}
#if defined(__aarch64__) || defined(_M_ARM64) || defined(ARCH_AARCH64) || defined(__riscv)
/* On aarch64/riscv64, UEFI's identity-mapped page tables remain active
* after ExitBootServices(); physical address == virtual address for all RAM.
* Each pmm_alloc_contiguous call returns a distinct paddr, so each VM gets
* a distinct vaddr automatically. */
guard_vaddr = paddr;
client_vaddr = guard_vaddr + SK_VM_GUARD_SIZE;
(void)(VMM_FLAG_WRITABLE); (void)(VMM_FLAG_NX);
#else
/* On amd64, walk a per-call virtual address pointer forward so each arena
* gets a unique VMM mapping rather than re-mapping the same fixed address. */
static uint64_t next_arena_vaddr = SK_VM_ARENA_VADDR;
guard_vaddr = next_arena_vaddr - SK_VM_GUARD_SIZE;
client_vaddr = guard_vaddr + SK_VM_GUARD_SIZE;
next_arena_vaddr += SK_VM_TOTAL_SIZE;
uint64_t flags = VMM_FLAG_WRITABLE | VMM_FLAG_NX;
if (vmm_map_range(guard_vaddr, paddr,
SK_VM_ARENA_PAGES * PMM_PAGE_SIZE, flags) != 0) {
console_println("sk_vm_arena_alloc: VMM mapping failed");
pmm_free_contiguous(paddr, SK_VM_ARENA_PAGES);
next_arena_vaddr -= SK_VM_TOTAL_SIZE;
return 0;
}
#endif
/* Zero the arena */
uint8_t *arena = (uint8_t *)(uintptr_t)client_vaddr;
for (size_t i = 0; i < SK_VM_MEMORY_SIZE; i++) {
arena[i] = 0;
}
guard_fill(guard_vaddr);
guard_fill(guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_MEMORY_SIZE);
/* Track first (Mama) arena for sk_vm_arena_free / sk_vm_arena_ptr / assert_guards */
if (!vm_arena_initialized) {
vm_arena_paddr = paddr;
vm_arena_guard_vaddr = guard_vaddr;
vm_arena_client_vaddr = client_vaddr;
vm_arena_initialized = 1;
}
console_puts("VM arena allocated: ");
/* Print address (simple hex) */
{
char buf[20];
uint64_t v = client_vaddr;
int i = 18;
buf[19] = '\0';
buf[18] = '\n';
while (i > 2) {
int d = v & 0xF;
buf[--i] = (d < 10) ? ('0' + d) : ('a' + d - 10);
v >>= 4;
}
buf[1] = 'x';
buf[0] = '0';
console_puts(buf);
}
console_puts(" (");
/* Print size in MB */
{
char buf[16];
uint64_t mb = SK_VM_MEMORY_SIZE / (1024 * 1024);
int i = 0;
if (mb == 0) {
buf[i++] = '0';
} else {
char tmp[16];
int j = 0;
while (mb > 0) {
tmp[j++] = '0' + (mb % 10);
mb /= 10;
}
while (j > 0) {
buf[i++] = tmp[--j];
}
}
buf[i] = '\0';
console_puts(buf);
}
console_println(" MB)");
return client_vaddr;
}
/**
* sk_vm_arena_free - Free the VM arena
*
* Unmaps and frees the PMM-backed arena.
*/
void sk_vm_arena_free(void) {
if (!vm_arena_initialized) {
return;
}
#if !(defined(__aarch64__) || defined(_M_ARM64) || defined(ARCH_AARCH64) || defined(__riscv))
/* Unmap each page (not needed on aarch64/riscv64 — no VMM remapping was done) */
for (uint64_t offset = 0; offset < SK_VM_TOTAL_SIZE; offset += PMM_PAGE_SIZE) {
vmm_unmap_page(vm_arena_guard_vaddr + offset);
}
#endif
/* Free physical memory */
pmm_free_contiguous(vm_arena_paddr, SK_VM_ARENA_PAGES);
vm_arena_paddr = 0;
vm_arena_guard_vaddr = 0;
vm_arena_client_vaddr = 0;
vm_arena_initialized = 0;
}
/**
* sk_vm_arena_ptr - Get pointer to VM arena
*
* Returns NULL if arena not allocated.
*/
uint8_t* sk_vm_arena_ptr(void) {
if (!vm_arena_initialized) {
return (uint8_t*)0;
}
return (uint8_t *)(uintptr_t)vm_arena_client_vaddr;
}
/**
* sk_vm_arena_size - Get VM arena size
*/
size_t sk_vm_arena_size(void) {
return SK_VM_MEMORY_SIZE;
}
/**
* sk_vm_arena_is_initialized - Check if arena is ready
*/
int sk_vm_arena_is_initialized(void) {
return vm_arena_initialized;
}
/* ===========================================================================
* Per-VM native stack (FABRIC-3.md §XXVIII, Stage 1, 2026-09-13)
* ===========================================================================
*
* 2 MiB, matching this project's own established kernel-stack convention
* (g_kernel_stack / g_rpi5_native_stack in each arch's entry assembly) --
* not a guessed number. That single 2 MiB stack today accommodates the
* ENTIRE shared kernel C stack's worth of nested VM-EXEC/execute_colon_word
* recursion across every VM combined, so 2 MiB for one VM's own stack alone
* is generous headroom, not a guess at the edge of what's needed.
*/
#define SK_VM_STACK_SIZE (2ULL * 1024ULL * 1024ULL)
#define SK_VM_STACK_PAGES ((SK_VM_STACK_SIZE + (2ULL * SK_VM_GUARD_SIZE) + PMM_PAGE_SIZE - 1) / PMM_PAGE_SIZE)
#if !(defined(ARCH_AARCH64) || defined(__riscv))
/* amd64 needs its own virtual-address walk, separate from arena's
* next_arena_vaddr -- different logical region, would otherwise collide. */
#define SK_VM_STACK_ARENA_VADDR 0xFFFF980000000000ULL
static uint64_t next_stack_vaddr = SK_VM_STACK_ARENA_VADDR;
#endif
/**
* sk_vm_native_stack_alloc - allocate one VM's own dedicated native stack.
*
* Always a fresh, independent PMM allocation with real guard pages -- no
* singleton, no kmalloc fallback, unlike sk_vm_arena_alloc()'s Mama-only
* special case. Returns 0 on success (fields of *out populated), -1 on
* failure.
*/
int sk_vm_native_stack_alloc(sk_vm_native_stack_t *out) {
uint64_t paddr, guard_vaddr;
if (!out) return -1;
paddr = pmm_alloc_contiguous(SK_VM_STACK_PAGES);
if (paddr == 0) {
console_println("sk_vm_native_stack_alloc: PMM allocation failed");
return -1;
}
#if defined(ARCH_AARCH64) || defined(__riscv)
guard_vaddr = paddr;
(void)(VMM_FLAG_WRITABLE); (void)(VMM_FLAG_NX);
#else
guard_vaddr = next_stack_vaddr;
next_stack_vaddr += SK_VM_STACK_PAGES * PMM_PAGE_SIZE;
uint64_t flags = VMM_FLAG_WRITABLE | VMM_FLAG_NX;
if (vmm_map_range(guard_vaddr, paddr,
SK_VM_STACK_PAGES * PMM_PAGE_SIZE, flags) != 0) {
console_println("sk_vm_native_stack_alloc: VMM mapping failed");
pmm_free_contiguous(paddr, SK_VM_STACK_PAGES);
next_stack_vaddr -= SK_VM_STACK_PAGES * PMM_PAGE_SIZE;
return -1;
}
#endif
guard_fill(guard_vaddr);
guard_fill(guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_STACK_SIZE);
out->paddr = paddr;
out->guard_vaddr = guard_vaddr;
out->stack_top = guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_STACK_SIZE;
return 0;
}
/**
* sk_vm_native_stack_free - release a VM's native stack.
*
* Mirrors sk_vm_arena_free()'s unmap/free sequence, but per-call (this is
* not a singleton) -- safe to call once per successful
* sk_vm_native_stack_alloc(), and the caller (vm_cleanup()) is responsible
* for not calling it twice on the same struct.
*/
void sk_vm_native_stack_free(const sk_vm_native_stack_t *stack) {
if (!stack || stack->paddr == 0) return;
#if !(defined(ARCH_AARCH64) || defined(__riscv))
for (uint64_t offset = 0; offset < SK_VM_STACK_PAGES * PMM_PAGE_SIZE; offset += PMM_PAGE_SIZE) {
vmm_unmap_page(stack->guard_vaddr + offset);
}
#endif
pmm_free_contiguous(stack->paddr, SK_VM_STACK_PAGES);
}
void sk_vm_arena_assert_guards(const char *tag) {
if (!vm_arena_initialized) {
return;
}
if (!guard_check(vm_arena_guard_vaddr) ||
!guard_check(vm_arena_guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_MEMORY_SIZE)) {
console_puts("[VM][arena] guard corruption at ");
console_println(tag ? tag : "unknown");
/* vm_dictionary_log_last_word(NULL, tag); -- not implemented in minimal kernel */
sk_hal_panic("VM arena guard corrupted");
}
}
#endif /* __STARKERNEL__ */
+5
View File
@@ -0,0 +1,5 @@
# src/starkernel/vm/bootstrap/
- `sk_vm_bootstrap.c` — owns kernel VM initialization and capsule-loading
wiring, called from `kernel_main.c`. Kept deliberately separate from
`kernel_main.c`, which owns only hardware milestones.
+331
View File
@@ -0,0 +1,331 @@
/*
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.
*/
/**
* bootstrap.c - Kernel VM bootstrap + parity emit
*
* Gated by STARFORTH_ENABLE_VM to avoid touching stable hosted VM builds.
* Initializes the VM using kernel host hooks, collects parity, and prints it.
*/
#ifdef __STARKERNEL__
#include "vm.h"
#include "log.h"
#include "block_subsystem.h"
#include "starkernel/vm/parity.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/hal/hal.h"
#include "starkernel/console.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "vm_internal.h"
#include "platform_time.h"
#include "test_runner/include/test_runner.h"
#include "word_source/include/mama_forth_words.h"
#if SK_PARITY_DEBUG
extern char __text_start[];
extern char __text_end[];
extern char __rodata_start[];
extern char __rodata_end[];
extern char __data_start[];
extern char __data_end[];
extern char __bss_start[];
extern char __bss_end[];
/**
* @brief Print a 64-bit value as "0xNNNNNNNNNNNNNNNN" to the kernel console.
*
* Used by @c sk_bootstrap_debug_log_xt() when @c SK_PARITY_DEBUG is enabled
* to dump raw pointer values during XT validation. Formats directly into a
* 19-byte stack buffer; no libc dependency.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param value 64-bit value to format as uppercase-nibble hex.
*/
static void sk_bootstrap_debug_print_hex(uint64_t value) {
char buf[19];
buf[0] = '0';
buf[1] = 'x';
buf[18] = '\0';
for (int i = 0; i < 16; ++i) {
uint8_t nibble = (uint8_t)((value >> ((15 - i) * 4)) & 0xF);
buf[i + 2] = (nibble < 10) ? (char)('0' + nibble) : (char)('a' + nibble - 10);
}
console_puts(buf);
}
/**
* @brief Test whether a 64-bit address is a canonical x86-64 virtual address.
*
* Checks that bits [63:48] are all 0x0000 (user-space range) or all 0xFFFF
* (kernel-space range). A non-canonical address indicates a corrupt function
* pointer and triggers a @c sk_hal_panic() in the calling debug log function.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param addr 64-bit address to test.
* @return Non-zero if canonical, 0 if non-canonical.
*/
static int sk_bootstrap_xt_is_canonical(uint64_t addr) {
uint64_t upper = addr >> 48;
return (upper == 0x0000ULL) || (upper == 0xFFFFULL);
}
/**
* @brief Return a short string naming the memory region containing @p addr.
*
* Classifies @p addr into:
* - @c "null" — zero address
* - @c "text" — within the kernel .text section [@c sk_hal_text_start(),
* @c sk_hal_text_end())
* - @c "directmap" — ≥ @c 0xffff800000000000 (kernel direct-map range)
* - @c "unknown" — anything else
*
* Used by @c sk_bootstrap_debug_log_xt() to label XTs for diagnostic output.
* Rodata/data/bss are not checked because XTs should only reside in .text.
* Uses @c sk_hal_text_start() / @c sk_hal_text_end() to avoid GOT-relative
* accesses under @c -fPIC.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param addr 64-bit address to classify.
* @return Static string literal naming the region.
*/
static const char *sk_bootstrap_region_name(uint64_t addr) {
if (addr == 0) return "null";
/* Use HAL getters to avoid GOT indirection issues with -fPIC */
uint64_t text_start = sk_hal_text_start();
uint64_t text_end = sk_hal_text_end();
if (addr >= text_start && addr < text_end) {
return "text";
}
/* For rodata/data/bss, we rely on the HAL whitelist check instead,
* since those symbols also have the same GOT issue. XTs should be
* in text section anyway. */
if (addr >= 0xffff800000000000ULL) {
return "directmap";
}
return "unknown";
}
/**
* @brief Log a dictionary entry's XT pointer and panic if it is invalid.
*
* Prints a @c "[SK_BOOTDBG] <label> entry=ADDR xt=ADDR region=NAME" line to
* the kernel console. If the XT address is non-canonical or in the "unknown"
* region, prints an additional error line and calls @c sk_hal_panic() —
* never returns in that case.
*
* Marked @c __attribute__((unused)) because call sites are conditionally
* compiled; the function exists for use during active debug sessions.
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param label Short label string (e.g., the word name) for diagnostic output.
* @param entry Dictionary entry whose XT pointer to validate.
*/
static void __attribute__((unused)) sk_bootstrap_debug_log_xt(const char *label, const DictEntry *entry) {
console_puts("[SK_BOOTDBG] ");
console_puts(label ? label : "(word)");
console_puts(" entry=");
sk_bootstrap_debug_print_hex((uint64_t)(uintptr_t)entry);
const void *xt = entry ? (const void *)entry->func : NULL;
console_puts(" xt=");
sk_bootstrap_debug_print_hex((uint64_t)(uintptr_t)xt);
console_puts(" region=");
const char *region = sk_bootstrap_region_name((uint64_t)(uintptr_t)xt);
console_puts(region);
console_println("");
uint64_t addr = (uint64_t)(uintptr_t)xt;
if (!sk_bootstrap_xt_is_canonical(addr) || (region[0] == 'u' && region[1] == 'n')) {
console_puts("[SK_BOOTDBG] invalid XT for ");
console_puts(label ? label : "(word)");
console_puts(" region=");
console_puts(region);
console_println("");
sk_hal_panic("bootstrap XT pointer invalid");
}
}
#endif
#ifndef STARFORTH_ENABLE_VM
#define STARFORTH_ENABLE_VM 0
#endif
#if STARFORTH_ENABLE_VM
static VM sk_mama_vm; /* Mama's VM — persists after bootstrap */
/* 1 MB POST block RAM — mirrors hosted blk_ram in main.c (BSS, no PE file bloat) */
static uint8_t sk_post_blk_ram[BLK_RAM_BLOCKS * BLK_FORTH_SIZE];
/**
* @brief Return a pointer to the Mama VM instance.
*
* The Mama VM (@c sk_mama_vm) is a file-scope static that persists for the
* lifetime of the kernel. This accessor is used by the capsule subsystem and
* other kernel components that need to call back into the VM after bootstrap
* without holding a direct reference to the static variable.
*
* Only compiled when @c STARFORTH_ENABLE_VM is non-zero.
*
* @return Pointer to @c sk_mama_vm cast to @c void*.
*/
void *sk_get_mama_vm(void) {
return (void *)&sk_mama_vm;
}
/**
* @brief Bootstrap the Mama VM, run POST, collect parity, and print it.
*
* This is the top-level M7 entry point called from @c kernel_main() after all
* hardware milestones (M0–M6) are complete. Execution order:
*
* 1. @c sk_hal_init() + @c sf_time_init() — initialise HAL and time shim.
* 2. @c vm_init_with_host() — initialise Mama VM using HAL host services.
* On failure, collects and prints the parity packet and returns -1.
* 3. Capsule subsystem setup: @c capsule_vm_hooks_register(),
* @c capsule_vm_registry_init(), @c capsule_run_log_init().
* 4. @c register_mama_forth_words() — BIRTH, KILL, START, STOP, USE + capsule words.
* 5. @c vm_enable_interpreter() — required for POST @c vm_interpret() calls.
* 6. @c blk_subsys_init() — POST block RAM (mirrors hosted main.c init order).
* 7. @c run_all_tests() — full POST (936+ test cases).
* 8. @c sk_parity_collect() + @c sk_parity_print() — emit M7.1a/M7.1b lines.
* 9. @c sk_hal_freeze_exec_range() — lock the XT whitelist.
*
* The PARITY:OK / PARITY:FAIL verdict and the POST: PASSED / FAILED lines are
* the normative acceptance signals visible in the QEMU serial log.
*
* Only compiled when @c STARFORTH_ENABLE_VM is non-zero.
*
* @param out Optional caller-supplied @c ParityPacket to receive the parity
* data. If @c NULL, a local @c ParityPacket is used (data is
* printed but not returned to the caller).
* @return 0 if bootstrap and POST succeeded, -1 on any failure.
*/
int sk_vm_bootstrap_parity(ParityPacket *out) {
VM *vm = &sk_mama_vm;
ParityPacket local_pkt;
ParityPacket *pkt = out ? out : &local_pkt;
/* Reset parity packet */
pkt->bootstrap_result = SK_BOOTSTRAP_INIT_FAIL;
/* Initialize host services (allocator/time/console) */
sk_hal_init();
sf_time_init();
vm_init_with_host(vm, sk_hal_host_services());
if (vm->error) {
console_println("VM: init failed");
sk_parity_collect(vm, pkt);
sk_parity_print(pkt);
return -1;
}
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): Hera gets her own native
* stack too, same as every baby/WIREBIND VM (capsule_birth_baby()).
* Not fatal on failure, same reasoning as there -- nothing executes on
* this yet (Stage 2). */
{
sk_vm_native_stack_t ns;
if (sk_vm_native_stack_alloc(&ns) == 0) {
vm->native_stack_paddr = ns.paddr;
vm->native_stack_guard_vaddr = ns.guard_vaddr;
vm->native_stack_top = ns.stack_top;
} else {
console_println("VM: Hera native stack allocation failed");
}
}
/* Capsule subsystem: hooks, registry (Hera), run log.
* Called exactly once here for Mama — child VMs go through
* capsule_birth_baby() which never calls vm_init_with_host(). */
capsule_vm_hooks_register();
capsule_vm_registry_init(vm); /* establishes [Hera] console prefix */
vm->stadium_vm_id = vm_uuid_hera(); /* item 4.2 */
vm_physics_init(vm_uuid_hera()); /* Hera: the fleet's root, seeded Q48_ONE */
capsule_run_log_init();
register_mama_forth_words(vm); /* BIRTH KILL START STOP USE + capsule words */
#if SK_PARITY_DEBUG
log_set_level(LOG_DEBUG);
#else
/* Enable LOG_TEST level to see test output and count statistics */
log_set_level(LOG_TEST);
#endif
/* Enable interpreter - required for POST to execute vm_interpret() calls */
vm_enable_interpreter(vm);
/* Initialize block subsystem for POST — mirrors hosted main.c init order.
* Without this, all block word tests fail with vm->error=1 (blk_is_valid→0). */
blk_subsys_init(vm, sk_post_blk_ram, sizeof(sk_post_blk_ram));
/* Run full POST (Power-On Self Test) */
console_println("POST: Running comprehensive test suite...");
run_all_tests(vm);
/* Collect and print parity (includes test results) */
sk_parity_collect(vm, pkt);
sk_parity_print(pkt);
/* Verify POST success: parity OK, no test failures, no test errors */
if (pkt->bootstrap_result == SK_BOOTSTRAP_OK &&
pkt->tests_failed == 0 &&
pkt->tests_errors == 0) {
console_println("POST: PASSED");
} else {
console_println("POST: FAILED");
}
sk_hal_freeze_exec_range();
return (pkt->bootstrap_result == SK_BOOTSTRAP_OK) ? 0 : -1;
}
#endif /* STARFORTH_ENABLE_VM */
#endif /* __STARKERNEL__ */
+5
View File
@@ -0,0 +1,5 @@
# src/starkernel/vm/host/
- `shim.c` — host-service shim bridging the kernel VM subsystem to
`src/starkernel/hal/host_services.c`, narrowing the interface the VM
core needs from the underlying hardware/firmware layer.
File diff suppressed because it is too large Load Diff
+818
View File
@@ -0,0 +1,818 @@
/*
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.c - Kernel-resident Hermes: the heat-coupled allocator,
* its release path, and the four-counter ledger (FABRIC-3.6.md tasks
* 2.2/2.3/2.4, item 28).
*
* CORRECTION (task 2.3, found while starting it): task 2.2's first cut of
* sk_hermes_alloc() pulled reservoir heat but never admitted a real
* Stadium-floor patron -- it just set a local `in_use` flag. That is
* wrong: FABRIC-3.5.md SXXXIII.4 item 1 states plainly that
* "MSG-FREE-NODE returns it via STADIUM-EVICT", which only has something
* to evict if allocation admitted something. More importantly, SXL.4's
* invariant is Sigma(resident patron heat) + reservoir + consumed ==
* Q48_ONE -- if held message heat is not a resident patron, it is
* invisible to every term of that equation while held, which cannot be
* right. Corrected here: sk_hermes_alloc() now calls stadium_admit()
* with the pulled heat, storing the returned cell index in the message's
* own stadium_cell field for release to evict later. Behaviour
* DELIVER (1), matching messaging.4th's own `SB-DELIVER STADIUM-ADMIT`
* at its MSG-ALLOC site.
*/
#ifdef __STARKERNEL__
#include <string.h>
#include "freestanding/stdio.h" /* task 3.9 evidence print -- snprintf() */
#include "starkernel/vm/kernel_hermes.h"
#include "starkernel/vm/stadium.h"
#include "starkernel/q48_16.h"
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
* carry their own copy of. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
static SkHermesMessage sk_hermes_msgs[SK_HERMES_MSG_MAX];
/* Task 2.4 (item 28), FABRIC-3.5.md SXXXVII.3/SXL.4's four counters.
* `consumed` is declared now but touched nowhere yet -- task 2.5 adds
* its one increment site when decay exists to record a delta from. */
static uint64_t sk_hermes_held = 0;
static uint64_t sk_hermes_pulled = 0;
static uint64_t sk_hermes_returned = 0;
static uint64_t sk_hermes_consumed = 0;
void sk_hermes_ledger(uint64_t *held, uint64_t *pulled, uint64_t *returned, uint64_t *consumed) {
if (held) *held = sk_hermes_held;
if (pulled) *pulled = sk_hermes_pulled;
if (returned) *returned = sk_hermes_returned;
if (consumed) *consumed = sk_hermes_consumed;
}
int sk_hermes_audit_values(uint64_t held, uint64_t pulled, uint64_t returned, uint64_t consumed) {
/* Epsilon zero (SXXXVII.3): exact integer equality, no tolerance. */
return held == pulled - returned - consumed;
}
static uint64_t sk_hermes_audit_failures = 0;
int sk_hermes_audit(void) {
int ok = sk_hermes_audit_values(sk_hermes_held, sk_hermes_pulled,
sk_hermes_returned, sk_hermes_consumed);
if (!ok) {
sk_hermes_audit_failures++;
console_println("Kernel-Hermes AUDIT FAILURE: held != pulled - returned - consumed");
}
return ok;
}
uint64_t sk_hermes_audit_failure_count(void) {
return sk_hermes_audit_failures;
}
uint64_t sk_hermes_scan_held(size_t *live_count) {
uint64_t sum = 0;
size_t n = 0;
int i;
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use || sk_hermes_msgs[i].stadium_cell < 0) continue;
sum += stadium_cells()[sk_hermes_msgs[i].stadium_cell].header.heat;
n++;
}
if (live_count) *live_count = n;
return sum;
}
int sk_hermes_scan_check(void) {
return sk_hermes_scan_held((size_t *)0) == sk_hermes_held;
}
static int sk_hermes_find_free_slot(void) {
int i;
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use) return i;
}
return -1;
}
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
int slot;
uint64_t pulled;
StadiumPatronHeader candidate;
size_t cell;
if (!out_msg) return -1;
/* Check affordability before touching the reservoir at all -- this is
* what "roll back on refusal" reduces to when the check happens
* first: there is nothing to roll back. */
if (stadium_reservoir_peek(vm_id) < SK_HERMES_Q_SLOT) return -1;
slot = sk_hermes_find_free_slot();
if (slot < 0) return -1; /* arena full, reservoir untouched */
pulled = stadium_reservoir_pull(vm_id, SK_HERMES_Q_SLOT);
if (pulled < SK_HERMES_Q_SLOT) {
/* Should not happen given the peek check above (nothing else runs
* between the two calls on this single-core, cooperative kernel),
* but roll back explicitly rather than trust that invariant
* silently. */
if (pulled > 0) stadium_reservoir_push(vm_id, pulled);
return -1;
}
memset(&candidate, 0, sizeof(candidate));
/* Matches messaging.4th's own MSG-ALLOC: the slot's own index becomes
* the admitted patron's identity, which stadium_dispatch()'s DELIVER
* case later prints back as "msg_idx=" on release -- an arbitrary
* distinct value would satisfy the conservation math just as well,
* but this keeps the diagnostic meaningful instead of every message
* printing identity 0. */
candidate.identity = (uint64_t)slot;
candidate.heat = pulled; /* the reservoir pull itself, unmodified */
candidate.ttl = 0; /* decay/TTL is task 2.5's scope, not this one's */
candidate.link = 0;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1; /* message body lives in sk_hermes_msgs[], not here */
candidate.flags = 0;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_DELIVER;
cell = stadium_admit(vm_id, &candidate);
if (cell == STADIUM_CELL_NONE) {
/* Stadium floor itself refused (unrelated to heat affordability,
* already confirmed above) -- roll back the pull, same "leave
* everything exactly as found" discipline as every other refusal
* path here. */
stadium_reservoir_push(vm_id, pulled);
return -1;
}
memset(&sk_hermes_msgs[slot], 0, sizeof(SkHermesMessage));
sk_hermes_msgs[slot].in_use = 1;
sk_hermes_msgs[slot].stadium_cell = (int32_t)cell;
sk_hermes_msgs[slot].owner = vm_id;
*out_msg = &sk_hermes_msgs[slot];
/* Task 2.4: the one site where held/pulled increment -- only reached
* once every refusal path above has already returned. */
sk_hermes_held += pulled;
sk_hermes_pulled += pulled;
sk_hermes_audit();
return 0;
}
int sk_hermes_release(SkHermesMessage *msg) {
int rc;
uint64_t remaining;
if (!msg || !msg->in_use) return -1;
if (msg->stadium_cell < 0) {
memset(msg, 0, sizeof(*msg));
return -1;
}
/* Read the patron's current heat BEFORE eviction -- stadium_evict()
* zeroes the header as part of returning the cell to the free list,
* and its own return value is a success code, not the amount
* credited. Today (before task 2.5's decay exists) this always
* equals exactly what was pulled at allocation; once decay exists,
* this is the message's true remaining heat, which is what actually
* flows back to the reservoir -- reading it here rather than
* assuming the original pulled amount is what keeps this correct
* without changes once task 2.5 lands. */
remaining = stadium_cells()[msg->stadium_cell].header.heat;
/* stadium_evict() returns the departing patron's remaining heat to
* its owner's reservoir itself (stadium.c: "the departing patron's
* remaining heat must flow back to its owner's reservoir before the
* cell returns to the free list") -- release does not touch the
* reservoir directly, the eviction path does it, matching
* MSG-FREE-NODE's own shape ("DUP 5 CELLS + @ STADIUM-EVICT DROP"). */
rc = stadium_evict((size_t)msg->stadium_cell);
/* Task 2.4: the one site where held/returned change on release. Only
* on successful eviction -- a refused eviction leaves the patron
* (and its heat) exactly where it was, so the ledger must not move
* either. */
if (rc == 0) {
sk_hermes_held -= remaining;
sk_hermes_returned += remaining;
sk_hermes_audit();
}
memset(msg, 0, sizeof(*msg));
return rc;
}
int sk_hermes_decay(SkHermesMessage *msg) {
uint64_t before, after;
StadiumCell *cell;
if (!msg || !msg->in_use || msg->stadium_cell < 0) return -1;
cell = &stadium_cells()[msg->stadium_cell];
before = cell->header.heat;
after = (uint64_t)q48_mul((q48_16_t)before, (q48_16_t)SK_HERMES_Q_DECAY);
cell->header.heat = after;
/* Task 2.5: the one site where consumed increments (and where held
* pays for it). SXXXVII.3: decay's delta is exact and in hand here. */
sk_hermes_consumed += before - after;
sk_hermes_held -= before - after;
stadium_consumed_record(msg->owner, before - after);
sk_hermes_audit();
return 0;
}
/* Task 3.2 (item 27, B1 / FABRIC-3.5.md SXLV.1): the channel table. See
* kernel_hermes.h's own doc comment on this section for scope and sizing
* reasoning. */
static SkHermesChannel *sk_hermes_channels = (SkHermesChannel *)0;
static int sk_hermes_channel_capacity_val = 0;
int sk_hermes_channels_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
SkHermesChannel *table;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
table = (SkHermesChannel *)kmalloc(max_vm_count * sizeof(SkHermesChannel));
if (!table) return -1;
for (i = 0; i < max_vm_count; i++) {
memset(&table[i], 0, sizeof(SkHermesChannel));
}
/* The common channel exists from boot (SXLV.1), empty -- VMs join it
* at birth (kernel_main.c/capsule_birth.c task 3.2 wiring), not here. */
table[SK_HERMES_CHANNEL_COMMON].in_use = 1;
sk_hermes_channels = table;
sk_hermes_channel_capacity_val = (int)max_vm_count;
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" channel slots");
return 0;
}
int sk_hermes_channel_capacity(void) {
return sk_hermes_channel_capacity_val;
}
static int channel_valid(int channel_id) {
return sk_hermes_channels &&
channel_id >= 0 &&
channel_id < sk_hermes_channel_capacity_val &&
sk_hermes_channels[channel_id].in_use;
}
int sk_hermes_channel_create(void) {
int i;
if (!sk_hermes_channels) return -1;
/* Index 0 is always in_use (the common channel), so this scan never
* returns it -- start at 1 purely as a scan-cost micro-optimisation,
* not for correctness (the in_use check alone would already skip it). */
for (i = 1; i < sk_hermes_channel_capacity_val; i++) {
if (!sk_hermes_channels[i].in_use) {
memset(&sk_hermes_channels[i], 0, sizeof(SkHermesChannel));
sk_hermes_channels[i].in_use = 1;
return i;
}
}
return -1;
}
int sk_hermes_channel_destroy(int channel_id) {
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1; /* permanent, SXLV.1 */
if (!channel_valid(channel_id)) return -1;
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
return 0;
}
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return -1;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return -1; /* already a member */
}
if (m->count >= SK_HERMES_MEMBER_MAX) return -1;
m->members[m->count] = vm_id;
m->count++;
return 0;
}
int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return -1;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) {
size_t j;
for (j = i; j < m->count - 1; j++) {
m->members[j] = m->members[j + 1];
}
m->count--;
return 0;
}
}
return -1; /* not a member */
}
int sk_hermes_channel_is_member(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return 0;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return 1;
}
return 0;
}
int sk_hermes_channel_member_count(int channel_id) {
if (!channel_valid(channel_id)) return -1;
return (int)sk_hermes_channels[channel_id].membership.count;
}
/* Task 3.3: per-subscriber pending queue table. See kernel_hermes.h's own
* doc comments on sk_hermes_queues_boot_init()/SK_HERMES_PENDING_MAX for
* scope and sizing reasoning. A queue is found-or-created lazily by
* vm_id, same shape as stadium.c's quota_slot_for_vm() and session.c's
* session_find() -- not pre-populated at birth like channel membership
* is, since not every VM ever receives a message. */
typedef struct {
VMUuid vm_id;
int in_use;
int slots[SK_HERMES_PENDING_MAX]; /* sk_hermes_msgs[] indices, circular FIFO */
size_t head;
size_t count;
} SkHermesPendingQueue;
static SkHermesPendingQueue *sk_hermes_queues = (SkHermesPendingQueue *)0;
static int sk_hermes_queue_capacity_val = 0;
int sk_hermes_queues_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
SkHermesPendingQueue *table;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
table = (SkHermesPendingQueue *)kmalloc(max_vm_count * sizeof(SkHermesPendingQueue));
if (!table) return -1;
for (i = 0; i < max_vm_count; i++) {
memset(&table[i], 0, sizeof(SkHermesPendingQueue));
}
sk_hermes_queues = table;
sk_hermes_queue_capacity_val = (int)max_vm_count;
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" pending-queue slots");
return 0;
}
static SkHermesPendingQueue *find_queue(VMUuid vm_id) {
int i;
if (!sk_hermes_queues) return (SkHermesPendingQueue *)0;
for (i = 0; i < sk_hermes_queue_capacity_val; i++) {
if (sk_hermes_queues[i].in_use && vm_uuid_equal(sk_hermes_queues[i].vm_id, vm_id)) {
return &sk_hermes_queues[i];
}
}
return (SkHermesPendingQueue *)0;
}
static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
int i, free_slot = -1;
if (!sk_hermes_queues) return (SkHermesPendingQueue *)0;
for (i = 0; i < sk_hermes_queue_capacity_val; i++) {
if (sk_hermes_queues[i].in_use && vm_uuid_equal(sk_hermes_queues[i].vm_id, vm_id)) {
return &sk_hermes_queues[i];
}
if (!sk_hermes_queues[i].in_use && free_slot < 0) free_slot = i;
}
if (free_slot < 0) return (SkHermesPendingQueue *)0; /* table full */
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
sk_hermes_queues[free_slot].vm_id = vm_id;
sk_hermes_queues[free_slot].in_use = 1;
return &sk_hermes_queues[free_slot];
}
/* Task 3.4: system-wide sum of every queue's count, maintained alongside
* queue_push()/sk_hermes_pending_pop() below -- lets
* sk_hermes_drain_checkpoint() skip its per-VM queue lookup entirely
* (stadium_max_vm_count()-sized linear scan) on every word dispatch when
* nothing is pending anywhere, the overwhelmingly common case. */
static int sk_hermes_pending_total = 0;
static int queue_push(SkHermesPendingQueue *q, int msg_index) {
size_t tail;
if (q->count >= SK_HERMES_PENDING_MAX) return -1;
tail = (q->head + q->count) % SK_HERMES_PENDING_MAX;
q->slots[tail] = msg_index;
q->count++;
sk_hermes_pending_total++;
return 0;
}
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len) {
SkHermesMessage *msg;
SkHermesPendingQueue *q;
int idx;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
* kernel_hermes.h's own sizing note on why SK_HERMES_CHUNK_MAX_SLICE
* (not SK_HERMES_CHUNK_MAX_PAYLOAD) is a chunk's own content size. */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1;
if (sk_hermes_alloc(from, &msg) != 0) return -1;
/* Real defect, found 2026-09-22 (FABRIC-3.6.md task 3.8's own
* findings log): this used to store the caller's own payload_addr
* as-is, a pointer this function does not own. Two sends before
* either drains meant both messages pointed at the same
* caller-owned buffer -- whichever send wrote last silently won.
* Copy into this message's own payload_buf instead (kernel_hermes.h,
* sized to SK_HERMES_CHUNK_MAX_PAYLOAD, the bound already enforced
* above) and point payload_addr at that -- every message now owns
* its payload bytes, independent of whatever the caller does with
* its own buffer afterward. payload_len == 0 is a real, valid case
* (an empty payload) -- memcpy of 0 bytes is well-defined even with
* a NULL payload_addr -- guarded anyway (C's memcpy() is technically
* undefined for a NULL argument even at length 0), matching this
* project's own STRICT_PTR discipline elsewhere. */
if (payload_len > 0) memcpy(msg->payload_buf, payload_addr, payload_len);
msg->payload_addr = msg->payload_buf;
msg->type = type;
msg->from = from;
msg->to = to;
msg->payload_len = payload_len;
msg->channel = channel_id;
q = find_or_create_queue(to);
if (!q) {
sk_hermes_release(msg); /* roll back -- no destination queue */
return -1;
}
idx = (int)(msg - sk_hermes_msgs);
if (queue_push(q, idx) != 0) {
sk_hermes_release(msg); /* roll back -- destination queue full */
return -1;
}
return 0;
}
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len) {
SkHermesMembership *m;
size_t i;
int delivered = 0;
if (!channel_valid(channel_id)) return -1;
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1; /* see sk_hermes_send_one()'s
* own check -- duplicated here
* so publish() itself returns
* -1 (not a silent 0) on an
* oversized payload */
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (sk_hermes_send_one(from, m->members[i], type, (uint32_t)channel_id,
payload_addr, payload_len) == 0) {
delivered++;
}
}
return delivered;
}
int sk_hermes_pending_count(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
return q ? (int)q->count : 0;
}
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return (SkHermesMessage *)0;
return &sk_hermes_msgs[q->slots[q->head]];
}
int sk_hermes_pending_pop(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return -1;
q->head = (q->head + 1) % SK_HERMES_PENDING_MAX;
q->count--;
sk_hermes_pending_total--;
return 0;
}
int sk_hermes_drain_checkpoint(VM *vm) {
SkHermesMessage *msg;
VMUuid self;
char saved_input[INPUT_BUFFER_SIZE];
size_t saved_length, saved_pos;
vm_mode_t saved_mode;
int saved_error, saved_abort;
int drain_error;
if (!vm) return -1;
if (sk_hermes_pending_total == 0) return 0; /* fast path -- see this
* counter's own doc comment */
self = vm->stadium_vm_id;
msg = sk_hermes_pending_peek(self);
if (!msg) return 0;
/* See kernel_hermes.h's own doc comment on this function for why all
* six of these must be preserved, not just the obvious ones. */
memcpy(saved_input, vm->input_buffer, sizeof(saved_input));
saved_length = vm->input_length;
saved_pos = vm->input_pos;
saved_mode = vm->mode;
saved_error = vm->error;
saved_abort = vm->abort_requested;
/* FABRIC-3.6.md task 3.9 evidence: CONSOLE-CMD-EVENT has no fixed
* FORTH handler word to hang a print on (task 3.8's BLK-ATTACH-ACK
* did) -- its payload is an arbitrary console line, interpreted
* as-is below. So the evidence line lives here instead, gated on
* msg->type, printed before vm_interpret() runs while this
* message's heat is still held (release()/pop() are further down
* in this same function) -- the same mid-hold-instant evidence task
* 3.8 used, same honest limit: evidence for mid-hold, not
* post-release. console_println, not log_message() (confirmed
* invisible in this build's serial capture, task 3.8's own
* finding); fires once per relayed console line, not per word. */
if (msg->type == SK_HERMES_MSG_TYPE_CONSOLE_CMD) {
uint64_t held, pulled, returned, consumed;
char line[160];
int conserved = stadium_conserved(self);
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
snprintf(line, sizeof(line),
"Kernel-Hermes CONSOLE-CMD-EVENT (real, Stage D): ledger held=%llu "
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(self)=%s",
(unsigned long long)held, (unsigned long long)pulled,
(unsigned long long)returned, (unsigned long long)consumed,
conserved ? "true" : "FALSE");
console_println(line);
}
/* FABRIC-3.6.md task 3.10 evidence: ELEVATE-REQUEST also has no fixed
* C-side handler word -- its payload is a FORTH command string
* calling ELEVATE-GRANT (zuse-eligibility.4th), interpreted as-is
* below, same shape as CONSOLE-CMD-EVENT above. Same mid-hold-instant
* evidence, same reasoning; `self` here is always Hera (`vm`'s own
* caller already resolved it that way in KH-ELEVATE-SEND, since
* ELEVATE-GRANT only ever runs on Hera). */
if (msg->type == SK_HERMES_MSG_TYPE_ELEVATE_REQUEST) {
uint64_t held, pulled, returned, consumed;
char line[160];
int conserved = stadium_conserved(self);
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
snprintf(line, sizeof(line),
"Kernel-Hermes ELEVATE-REQUEST (real, Stage D): ledger held=%llu "
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(self)=%s",
(unsigned long long)held, (unsigned long long)pulled,
(unsigned long long)returned, (unsigned long long)consumed,
conserved ? "true" : "FALSE");
console_println(line);
}
vm->mode = MODE_INTERPRET; /* a payload is never compiled into whatever
* the enclosing context was mid-defining */
vm_interpret(vm, (const char *)msg->payload_addr);
drain_error = vm->error;
memcpy(vm->input_buffer, saved_input, sizeof(saved_input));
vm->input_length = saved_length;
vm->input_pos = saved_pos;
vm->mode = saved_mode;
vm->error = saved_error; /* a bad message must not abort the
* enclosing execution */
vm->abort_requested = saved_abort;
sk_hermes_release(msg);
sk_hermes_pending_pop(self);
return drain_error ? -1 : 1;
}
uint32_t sk_hermes_chunk_count(uint32_t len) {
if (len == 0) return 0;
return (len + SK_HERMES_CHUNK_MAX_SLICE - 1) / SK_HERMES_CHUNK_MAX_SLICE;
}
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len) {
int present[SK_HERMES_MSG_MAX];
int i;
uint32_t msg_id;
uint32_t total;
uint32_t last_slice_len = 0;
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
if (n_chunks > SK_HERMES_MSG_MAX) return -1; /* guard before indexing present[] */
for (i = 0; i < n_chunks; i++) present[i] = 0;
if (!chunks[0] || !chunks[0]->payload_addr ||
chunks[0]->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
msg_id = ((SkHermesChunkHeader *)chunks[0]->payload_addr)->msg_id;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h;
uint32_t slice_len;
if (!m || !m->payload_addr || m->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
h = (SkHermesChunkHeader *)m->payload_addr;
slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
if (h->msg_id != msg_id) return -1;
if (h->seq >= (uint32_t)n_chunks) return -1;
if (present[h->seq]) return -1; /* duplicate seq */
present[h->seq] = 1;
if (h->seq == (uint32_t)(n_chunks - 1)) {
if (!h->is_last) return -1;
if (slice_len == 0 || slice_len > SK_HERMES_CHUNK_MAX_SLICE) return -1;
last_slice_len = slice_len;
} else {
if (h->is_last) return -1;
if (slice_len != SK_HERMES_CHUNK_MAX_SLICE) return -1;
}
}
for (i = 0; i < n_chunks; i++) if (!present[i]) return -1; /* gap */
/* Total computed once from validated seq completeness, checked once
* against out_buf_cap, before any memcpy -- never order-dependent on
* which chunk happens to overflow first. */
total = (uint32_t)(n_chunks - 1) * SK_HERMES_CHUNK_MAX_SLICE + last_slice_len;
if (total > out_buf_cap) return -1;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h = (SkHermesChunkHeader *)m->payload_addr;
uint32_t slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
uint32_t offset = h->seq * SK_HERMES_CHUNK_MAX_SLICE;
memcpy(out_buf + offset, (uint8_t *)m->payload_addr + sizeof(SkHermesChunkHeader), slice_len);
}
*out_len = total;
return 0;
}
int sk_hermes_channel_request(VMUuid requester, VMUuid target) {
return sk_hermes_send_one(requester, target, SK_HERMES_MSG_TYPE_CH_REQUEST,
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
}
int sk_hermes_channel_respond(VMUuid target, VMUuid requester, int approved) {
int ch = -1;
if (approved) {
ch = sk_hermes_channel_create();
if (ch >= 0) {
if (sk_hermes_channel_subscribe(ch, requester) != 0 ||
sk_hermes_channel_subscribe(ch, target) != 0) {
sk_hermes_channel_destroy(ch); /* leave no half-open channel */
ch = -1;
}
}
}
if (ch >= 0) {
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_CH_GRANT,
(uint32_t)ch, (void *)0, 0);
/* Ruled ACK cadence (2026-09-21): channel-open + delivery, not
* every message -- this IS that moment. */
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_ACK,
(uint32_t)ch, (void *)0, 0);
} else {
/* SXLV.1: "a deny is a NACK" -- no separate CH_DENY type. */
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_NACK,
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
}
return ch;
}
int sk_hermes_channel_close(VMUuid closer, int channel_id) {
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1;
if (!sk_hermes_channel_is_member(channel_id, closer)) return -1;
return sk_hermes_channel_destroy(channel_id);
}
int sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester) {
static const char word_name[] = "HERMES-CHANNEL-OPEN?";
DictEntry *entry;
DictEntry *saved_entry;
cell_t result;
if (!target_vm) return 0;
entry = vm_find_word(target_vm, word_name, sizeof(word_name) - 1);
if (!entry || !entry->func) return 0; /* fail closed: no policy, no open */
/* Colon words dispatch through execute_colon_word(), which reads its
* own body address from vm->current_executing_entry and no-ops
* silently if it is NULL (vm_core.c:730) -- vm_interpret_word() always
* sets this immediately before calling entry->func(); matched here
* for the same reason (found live: without it, every call silently
* did nothing, leaving the pushed requester untouched on the stack
* rather than erroring, which is why this needed a debug session to
* catch rather than showing up as an obvious crash). Saved and
* restored, not just set, in case target_vm is ever called into
* mid-dispatch (not exercised by this task's own self-test, but no
* reason to assume it away). */
saved_entry = target_vm->current_executing_entry;
target_vm->current_executing_entry = entry;
vm_push(target_vm, (cell_t)requester.hi);
vm_push(target_vm, (cell_t)requester.lo);
entry->func(target_vm);
target_vm->current_executing_entry = saved_entry;
if (target_vm->error || target_vm->dsp < 0) {
target_vm->error = 0; /* a broken policy word must not leak into
* whatever else target_vm is doing */
return 0;
}
result = vm_pop(target_vm);
return result != 0;
}
#endif /* __STARKERNEL__ */
+926
View File
@@ -0,0 +1,926 @@
/*
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.c - Parity packet collection and canonical hash
*
* Implements M7 parity validation for hosted vs kernel comparison.
*
* M7 Normative Rules enforced here:
* - Rule 1: word_id is monotonic creation index
* - Rule 2: Colon bodies hashed as word_id sequence
* - Rule 3: Dictionary traversal in creation order
*/
#include "starkernel/vm/parity.h"
#include "vm.h"
#ifdef __STARKERNEL__
#include "console.h"
#include "starkernel/hal/hal.h"
#else
#include <stdio.h>
#include <stdlib.h>
#endif
#include <stdbool.h>
#include <string.h>
#ifndef SK_PARITY_DEBUG
#define SK_PARITY_DEBUG 0
#endif
static void print_u32(uint32_t val);
static void print_hex64(uint64_t val);
static void print_str(const char *s);
static void print_nl(void);
#if SK_PARITY_DEBUG
#define SK_PARITY_DEBUG_PREFIX "SKPD:"
enum sk_parity_ptr_region {
SK_PTR_REGION_NULL = 0,
SK_PTR_REGION_VM_ARENA,
SK_PTR_REGION_TEXT,
SK_PTR_REGION_RODATA,
SK_PTR_REGION_DATA,
SK_PTR_REGION_BSS,
SK_PTR_REGION_DIRECTMAP,
SK_PTR_REGION_UNKNOWN
};
static void sk_parity_debug_log_msg(const char *msg);
static bool sk_parity_debug_is_canonical(uint64_t addr);
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr);
static const char *sk_parity_region_name(enum sk_parity_ptr_region region);
static void sk_parity_debug_print_word_name(const DictEntry *entry);
static void sk_parity_debug_panic(struct VM *vm,
const char *reason,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *bad_ptr,
enum sk_parity_ptr_region region,
bool canonical);
static void sk_parity_debug_log_ptr(const char *label,
const void *ptr,
enum sk_parity_ptr_region region,
bool canonical);
static void sk_parity_debug_check_ptr(struct VM *vm,
const char *label,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *ptr);
#else
#define sk_parity_debug_log_msg(msg) do { (void)(msg); } while (0)
#define sk_parity_debug_check_ptr(vm,label,entry,idx,hptr,xt,ptr) \
do { (void)(vm); (void)(label); (void)(entry); (void)(idx); (void)(hptr); (void)(xt); (void)(ptr); } while (0)
#endif
/* Maximum dictionary entries for traversal array */
#define MAX_DICT_ENTRIES 2048
#if SK_PARITY_DEBUG
#define SK_PARITY_CANONICAL_MASK 0xffff800000000000ULL
#define SK_PARITY_DIRECTMAP_BASE 0xffff800000000000ULL
/* Note: For __STARKERNEL__ builds, we use sk_hal_text_start/end() from hal.h
* to avoid GOT indirection issues with -fPIC. The rodata/data/bss section
* checks are skipped since XT pointers should only be in text section. */
/**
* @brief Emit a @c SK_PARITY_DEBUG_PREFIX-prefixed diagnostic message line.
*
* Prints @c "SKPD:<msg>\n" to the kernel console or stdout. Used throughout
* parity traversal to trace the hash walk when @c SK_PARITY_DEBUG=1.
* Prints @c "<null>" if @p msg is @c NULL.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero; all call sites are
* macro-eliminated otherwise.
*
* @param msg Null-terminated diagnostic message string.
*/
static void sk_parity_debug_log_msg(const char *msg)
{
print_str(SK_PARITY_DEBUG_PREFIX);
print_str(msg ? msg : "<null>");
print_nl();
}
/**
* @brief Test whether a 64-bit address is a canonical x86-64 virtual address.
*
* On x86-64, bits [63:48] must all equal bit 47 (sign-extend). This function
* checks that invariant: extracts bit 47, then verifies that the upper 16 bits
* (@c SK_PARITY_CANONICAL_MASK) are either all-zero (user space) or all-one
* (kernel space).
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param addr 64-bit virtual address to test.
* @return @c true if @p addr is canonical, @c false otherwise.
*/
static bool sk_parity_debug_is_canonical(uint64_t addr)
{
uint64_t sign = (addr >> 47) & 1ULL;
uint64_t mask = SK_PARITY_CANONICAL_MASK;
return sign ? ((addr & mask) == mask) : ((addr & mask) == 0);
}
/**
* @brief Classify a pointer into a known memory region for debug diagnostics.
*
* Checks @p ptr against known address ranges in the following priority order:
* 1. @c NULL → @c SK_PTR_REGION_NULL
* 2. VM arena [@c vm->memory, @c vm->memory + VM_MEMORY_SIZE) →
* @c SK_PTR_REGION_VM_ARENA
* 3. Kernel .text section [sk_hal_text_start(), sk_hal_text_end()) →
* @c SK_PTR_REGION_TEXT (kernel build only; rodata/data/bss skipped)
* 4. Direct-map region (≥ @c SK_PARITY_DIRECTMAP_BASE) →
* @c SK_PTR_REGION_DIRECTMAP (kernel build only)
* 5. Anything else → @c SK_PTR_REGION_UNKNOWN
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param vm Active VM (used for arena base/size); may be @c NULL.
* @param ptr Pointer to classify.
* @return One of the @c sk_parity_ptr_region enum values.
*/
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr)
{
if (!ptr) {
return SK_PTR_REGION_NULL;
}
uintptr_t addr = (uintptr_t)ptr;
if (vm && vm->memory) {
uintptr_t arena_start = (uintptr_t)vm->memory;
uintptr_t arena_end = arena_start + VM_MEMORY_SIZE;
if (addr >= arena_start && addr < arena_end) {
return SK_PTR_REGION_VM_ARENA;
}
}
#ifdef __STARKERNEL__
/* Use HAL getters to avoid GOT indirection issues with -fPIC */
uintptr_t text_start = (uintptr_t)sk_hal_text_start();
uintptr_t text_end = (uintptr_t)sk_hal_text_end();
if (addr >= text_start && addr < text_end) {
return SK_PTR_REGION_TEXT;
}
/* Note: rodata/data/bss section checks skipped for kernel builds.
* Function pointers (XTs) should only be in the text section anyway. */
if (addr >= SK_PARITY_DIRECTMAP_BASE) {
return SK_PTR_REGION_DIRECTMAP;
}
#endif
return SK_PTR_REGION_UNKNOWN;
}
/**
* @brief Return a human-readable name for a @c sk_parity_ptr_region value.
*
* Used by debug print helpers to label pointer regions in diagnostic output.
* Returns @c "unknown" for any value not listed in the enum.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param region Enum value from @c sk_parity_classify_region().
* @return Pointer to a static string literal naming the region.
*/
static const char *sk_parity_region_name(enum sk_parity_ptr_region region)
{
switch (region) {
case SK_PTR_REGION_NULL: return "null";
case SK_PTR_REGION_VM_ARENA: return "vm_arena";
case SK_PTR_REGION_TEXT: return "text";
case SK_PTR_REGION_RODATA: return "rodata";
case SK_PTR_REGION_DATA: return "data";
case SK_PTR_REGION_BSS: return "bss";
case SK_PTR_REGION_DIRECTMAP: return "directmap";
default: return "unknown";
}
}
/**
* @brief Print the name of a dictionary entry to the console for debug output.
*
* Copies @p entry->name into a local null-terminated buffer (bounded by
* @c WORD_NAME_MAX) and emits it via @c print_str(). Prints @c "<none>" if
* @p entry is @c NULL. Used by @c sk_parity_debug_panic() to identify the
* word being hashed when a pointer violation is detected.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param entry Dictionary entry whose name to print; may be @c NULL.
*/
static void sk_parity_debug_print_word_name(const DictEntry *entry)
{
if (!entry) {
print_str("<none>");
return;
}
uint8_t len = entry->name_len;
if (len == 0 || len > WORD_NAME_MAX) {
len = (len > WORD_NAME_MAX) ? WORD_NAME_MAX : len;
}
char buf[WORD_NAME_MAX + 1];
if (len > 0) {
memcpy(buf, entry->name, len);
}
buf[len] = '\0';
print_str(buf);
}
/**
* @brief Log a pointer's address, canonicality, and region to the console.
*
* Emits a single debug line in the format:
* @c "SKPD:<label>=0xADDR canon=Y/N region=<region_name>\n".
* Used by @c sk_parity_debug_check_ptr() to trace each pointer inspected
* during the canonical hash walk.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param label Short string label for the pointer (e.g., "colon_entry").
* @param ptr Pointer whose address is to be printed.
* @param region Region classification from @c sk_parity_classify_region().
* @param canonical Whether @p ptr is a canonical x86-64 address.
*/
static void sk_parity_debug_log_ptr(const char *label,
const void *ptr,
enum sk_parity_ptr_region region,
bool canonical)
{
print_str(SK_PARITY_DEBUG_PREFIX);
print_str(label ? label : "ptr");
print_str("=");
print_hex64((uint64_t)(uintptr_t)ptr);
print_str(" canon=");
print_str(canonical ? "Y" : "N");
print_str(" region=");
print_str(sk_parity_region_name(region));
print_nl();
}
/**
* @brief Emit a parity pointer-violation diagnostic and halt.
*
* Called when @c sk_parity_debug_check_ptr() finds a pointer that is either
* non-canonical or in an unexpected memory region during the hash walk.
* Prints a multi-line @c "SK_PARITY_PANIC:" report containing:
* - the violation reason string
* - the word's creation-order index and name
* - the entry's header and XT pointers
* - the bad pointer, its canonicality flag, and its region
* - the VM's current @c HERE and @c LATEST values
*
* Then calls @c sk_hal_panic() in kernel builds or @c abort() in hosted
* builds; never returns.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param vm Active VM (for HERE/LATEST diagnostics); may be @c NULL.
* @param reason Short string describing the violation (e.g., "colon_entry").
* @param entry Dictionary entry being processed when the violation occurred.
* @param word_index Creation-order ordinal of @p entry.
* @param header_ptr Pointer to the dictionary entry header.
* @param xt_ptr Pointer to the word's execution token (func pointer).
* @param bad_ptr The offending pointer that failed the check.
* @param region Region classification of @p bad_ptr.
* @param canonical Whether @p bad_ptr is a canonical x86-64 address.
*/
static void sk_parity_debug_panic(struct VM *vm,
const char *reason,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *bad_ptr,
enum sk_parity_ptr_region region,
bool canonical)
{
print_str("SK_PARITY_PANIC: ");
print_str(reason ? reason : "unknown");
print_nl();
print_str(" word_idx=");
print_u32(word_index);
print_str(" name=");
sk_parity_debug_print_word_name(entry);
print_nl();
print_str(" header_ptr=");
print_hex64((uint64_t)(uintptr_t)header_ptr);
print_str(" xt_ptr=");
print_hex64((uint64_t)(uintptr_t)xt_ptr);
print_nl();
print_str(" bad_ptr=");
print_hex64((uint64_t)(uintptr_t)bad_ptr);
print_str(" canon=");
print_str(canonical ? "Y" : "N");
print_str(" region=");
print_str(sk_parity_region_name(region));
print_nl();
if (vm) {
print_str(" HERE=");
print_hex64((uint64_t)vm->here);
print_str(" LATEST=");
print_hex64((uint64_t)(uintptr_t)vm->latest);
print_nl();
}
#ifdef __STARKERNEL__
sk_hal_panic("parity pointer violation");
#else
fprintf(stderr, "Parity pointer violation\n");
abort();
#endif
}
/**
* @brief Validate a pointer and panic if it is non-canonical or out-of-region.
*
* Classifies @p ptr with @c sk_parity_classify_region() and tests it for
* canonicality with @c sk_parity_debug_is_canonical(). Logs the result via
* @c sk_parity_debug_log_ptr(). If the pointer is non-canonical or in the
* @c SK_PTR_REGION_UNKNOWN region, calls @c sk_parity_debug_panic() which
* never returns.
*
* This is the central guard point called at every pointer dereference in
* the hash walk to catch dictionary corruption or bad function pointers
* before they cause silent incorrect results.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero; call sites are
* macro-eliminated in release builds.
*
* @param vm Active VM (passed to @c sk_parity_classify_region()).
* @param label Short label string identifying which pointer is being checked.
* @param entry Current dictionary entry being hashed.
* @param word_index Creation-order ordinal of @p entry.
* @param header_ptr Pointer to the entry header (for panic diagnostics).
* @param xt_ptr Pointer to the entry's XT (for panic diagnostics).
* @param ptr The pointer to validate.
*/
static void sk_parity_debug_check_ptr(struct VM *vm,
const char *label,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *ptr)
{
enum sk_parity_ptr_region region = sk_parity_classify_region(vm, ptr);
bool canonical = sk_parity_debug_is_canonical((uint64_t)(uintptr_t)ptr);
sk_parity_debug_log_ptr(label, ptr, region, canonical);
if (!canonical || region == SK_PTR_REGION_UNKNOWN) {
sk_parity_debug_panic(vm, label, entry, word_index, header_ptr, xt_ptr, ptr, region, canonical);
}
}
#endif /* SK_PARITY_DEBUG */
/**
* @brief FNV-1a 64-bit incremental hash over a byte buffer.
*
* Updates a running FNV-1a hash by folding @p len bytes from @p data into
* @p hash. The caller supplies the current hash state so that multiple
* buffers can be chained: pass @c FNV1A_64_OFFSET_BASIS for the first call
* and the returned value as @p hash for subsequent calls.
*
* Algorithm: for each byte @c b, @c hash ^= b then @c hash *= FNV1A_64_PRIME.
*
* @param data Pointer to the byte buffer to hash.
* @param len Number of bytes to consume.
* @param hash Running hash state; seed with @c FNV1A_64_OFFSET_BASIS.
* @return Updated hash state after processing all @p len bytes.
*/
uint64_t fnv1a_64(const uint8_t *data, size_t len, uint64_t hash) {
for (size_t i = 0; i < len; i++) {
hash ^= data[i];
hash *= FNV1A_64_PRIME;
}
return hash;
}
/**
* @brief FNV-1a 64-bit incremental hash over a single byte.
*
* Convenience wrapper for folding one byte into a running FNV-1a hash state.
* Used by @c fnv1a_64_u32() to hash multi-byte values byte-by-byte in a
* defined endian order.
*
* @param byte Single byte to fold into the hash.
* @param hash Running hash state.
* @return Updated hash state.
*/
static uint64_t fnv1a_64_u8(uint8_t byte, uint64_t hash) {
hash ^= byte;
hash *= FNV1A_64_PRIME;
return hash;
}
/**
* @brief FNV-1a 64-bit incremental hash over a 32-bit value (little-endian).
*
* Folds all four bytes of @p val into @p hash in little-endian order
* (least-significant byte first) via four @c fnv1a_64_u8() calls. Used to
* hash @c word_id fields — which are @c uint32_t — into the canonical
* dictionary hash in a byte-order-stable way.
*
* @param val 32-bit value to hash.
* @param hash Running FNV-1a hash state.
* @return Updated hash state after consuming all four bytes.
*/
static uint64_t fnv1a_64_u32(uint32_t val, uint64_t hash) {
hash = fnv1a_64_u8((uint8_t)(val & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 8) & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 16) & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 24) & 0xFF), hash);
return hash;
}
/**
* @brief Count the number of entries in the VM dictionary.
*
* Walks the linked list starting at @c vm->latest, following each entry's
* @c link pointer until @c NULL. Stops early and returns @c MAX_DICT_ENTRIES
* if the list exceeds that limit, preventing runaway traversal on a corrupt
* dictionary.
*
* @param vm Active VM whose dictionary will be counted; returns 0 if @c NULL.
* @return Number of dictionary entries found, capped at @c MAX_DICT_ENTRIES.
*/
uint32_t sk_dict_word_count(struct VM *vm) {
if (!vm) return 0;
uint32_t count = 0;
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
count++;
if (count >= MAX_DICT_ENTRIES) break;
}
return count;
}
/**
* @brief Hash the compiled body of a colon definition as a sequence of word IDs.
*
* Implements M7 Rule 2: colon bodies are hashed as their @c word_id sequence,
* not as raw code pointers, ensuring the hash is stable across address-space
* layout changes (ASLR, relocation, different build addresses).
*
* Traversal reads each cell from the threaded body in order:
* - A non-NULL @c DictEntry pointer → its @c word_id is hashed.
* - An @c EXIT entry → its @c word_id is hashed and traversal stops.
* - A @c LIT entry → its @c word_id is hashed and the following cell (the
* literal value) is hashed as eight individual bytes (little-endian).
* - A @c NULL cell → traversal stops (end-of-body sentinel).
*
* Returns @p hash unchanged if @p entry lacks the @c WORD_COMPILED flag or
* has no valid data-field. Capped at 1024 cells per body as a safety limit.
*
* @param vm Active VM instance.
* @param entry Dictionary entry whose body will be hashed; must have
* @c WORD_COMPILED set.
* @param word_index Creation-order ordinal of @p entry (used by debug mode).
* @param hash Running FNV-1a hash state from the caller.
* @return Updated hash state after processing the colon body.
*/
static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_index, uint64_t hash) {
if (!vm || !entry || !(entry->flags & WORD_COMPILED)) {
return hash;
}
#if !SK_PARITY_DEBUG
(void)word_index;
#endif
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_colon_body:entry");
sk_parity_debug_check_ptr(vm, "colon_entry", entry, word_index, entry, entry->func, entry);
#endif
/* Get body address from data field */
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "data_field", entry, word_index, entry, entry->func, df);
#endif
vaddr_t body_addr = (vaddr_t)(uint64_t)(*df);
if (body_addr == 0 || body_addr >= VM_MEMORY_SIZE) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "body_addr_ptr", entry, word_index, entry, entry->func,
(vm->memory && body_addr < VM_MEMORY_SIZE) ? (vm->memory + body_addr) : NULL);
#endif
/* Traverse threaded code until we hit EXIT or end */
/* Each cell is a DictEntry* (word to call) */
/* LIT is followed by a literal value */
DictEntry *lit_entry = vm_find_word(vm, "LIT", 3);
DictEntry *exit_entry = vm_find_word(vm, "EXIT", 4);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "lit_entry", lit_entry, word_index, entry, lit_entry ? lit_entry->func : NULL, lit_entry);
sk_parity_debug_check_ptr(vm, "exit_entry", exit_entry, word_index, entry, exit_entry ? exit_entry->func : NULL, exit_entry);
#endif
cell_t *ip = (cell_t *)vm_ptr(vm, body_addr);
if (!ip) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "colon_body_ip", entry, word_index, entry, entry->func, ip);
#endif
/* Safety limit on body length */
size_t max_cells = (VM_MEMORY_SIZE - body_addr) / sizeof(cell_t);
if (max_cells > 1024) max_cells = 1024;
for (size_t i = 0; i < max_cells; i++) {
DictEntry *w = (DictEntry *)(uintptr_t)(ip[i]);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "body_xt", w, word_index, entry, w ? w->func : NULL, w);
#endif
if (w == NULL) {
break; /* End of body */
}
if (w == exit_entry) {
/* Hash EXIT's word_id and stop */
hash = fnv1a_64_u32(w->word_id, hash);
break;
}
/* Hash the called word's word_id */
hash = fnv1a_64_u32(w->word_id, hash);
if (w == lit_entry) {
/* Next cell is literal value - hash it */
i++;
if (i < max_cells) {
cell_t lit_val = ip[i];
/* Hash as 64-bit value */
for (int b = 0; b < 8; b++) {
hash = fnv1a_64_u8((uint8_t)(lit_val & 0xFF), hash);
lit_val >>= 8;
}
}
}
}
return hash;
}
/**
* @brief Compute the canonical FNV-1a 64-bit hash of the VM dictionary.
*
* Implements M7 Rule 3: dictionary traversal is in creation order (oldest
* word first). Because @c vm->latest is the newest word, the function first
* builds a reverse array of entries and then hashes them in reverse-array
* order (oldest → newest).
*
* For each entry the following structural fields are folded into the hash:
* 1. @c flags (1 byte)
* 2. @c name_len (1 byte)
* 3. @c name (name_len bytes)
* 4. @c acl_default (1 byte)
* 5. @c word_id (4 bytes, little-endian)
* 6. Colon body (via @c hash_colon_body()) if @c WORD_COMPILED is set
*
* Pointer-valued fields (@c func, @c link) are intentionally excluded so
* the hash is stable across different build addresses and address-space
* layouts.
*
* Traversal is capped at @c MAX_DICT_ENTRIES (2048) entries to guard
* against corruption.
*
* @param vm Active VM whose dictionary will be hashed; returns 0 if @c NULL.
* @return 64-bit FNV-1a canonical hash of the dictionary structure.
*/
uint64_t sk_dict_canonical_hash(struct VM *vm) {
if (!vm) return 0;
/* Build array of entries in reverse order (latest to oldest) */
DictEntry *entries[MAX_DICT_ENTRIES];
uint32_t count = 0;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_dict_canonical_hash:walk_latest");
sk_parity_debug_check_ptr(vm, "latest_head", vm->latest, 0, vm->latest, vm->latest ? vm->latest->func : NULL, vm->latest);
#endif
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "dict_entry", e, count, e, e->func, e);
#endif
if (count < MAX_DICT_ENTRIES) {
entries[count++] = e;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "dict_link", e->link, count, e, e->func, e->link);
#endif
}
/* Hash in creation order (oldest to newest = reverse of array) */
uint64_t hash = FNV1A_64_OFFSET_BASIS;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_dict_canonical_hash:hash_order");
#endif
for (int i = (int)count - 1; i >= 0; i--) {
DictEntry *e = entries[i];
uint32_t ordinal = (uint32_t)((count - 1) - i);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "hash_entry", e, ordinal, e, e ? e->func : NULL, e);
sk_parity_debug_check_ptr(vm, "hash_xt", e, ordinal, e, e ? e->func : NULL, e ? e->func : NULL);
#endif
/* Hash structural fields only */
/* 1. flags (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:flags");
#endif
hash = fnv1a_64_u8(e->flags, hash);
/* 2. name_len (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:name_len");
#endif
hash = fnv1a_64_u8(e->name_len, hash);
/* 3. name (name_len bytes) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:name_bytes");
#endif
hash = fnv1a_64((const uint8_t *)e->name, e->name_len, hash);
/* 4. acl_default (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:acl");
#endif
hash = fnv1a_64_u8(e->acl_default, hash);
/* 5. word_id (4 bytes) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:word_id");
#endif
hash = fnv1a_64_u32(e->word_id, hash);
/* 6. For colon definitions: hash body as word_id sequence */
if (e->flags & WORD_COMPILED) {
hash = hash_colon_body(vm, e, ordinal, hash);
}
}
return hash;
}
/**
* @brief Collect M7 parity data from the VM into a @c ParityPacket.
*
* Populates @p out with a snapshot of the VM's structural state for offline
* determinism verification. The packet contains:
*
* - M7.1a fields: @c word_count, @c here_offset, @c latest_word_id, and the
* canonical dictionary hash (@c header_hash64) computed by
* @c sk_dict_canonical_hash().
* - M7.1b fields: test result counters (@c tests_total / @c tests_passed /
* @c tests_failed / @c tests_skipped / @c tests_errors) populated from
* @c global_test_stats when @c STARFORTH_ENABLE_TESTS is defined.
*
* If @p vm is @c NULL, sets @c out->bootstrap_result to
* @c SK_BOOTSTRAP_INIT_FAIL and returns with all other fields zeroed. If
* @p out is @c NULL the function is a no-op.
*
* @param vm Active VM to collect parity data from; may be @c NULL.
* @param out Output @c ParityPacket to fill; caller must allocate.
*/
void sk_parity_collect(struct VM *vm, ParityPacket *out) {
if (!out) return;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_parity_collect:enter");
#endif
/* Clear packet */
out->word_count = 0;
out->here_offset = 0;
out->latest_word_id = 0;
out->header_hash64 = 0;
out->tests_total = 0;
out->tests_passed = 0;
out->tests_failed = 0;
out->tests_skipped = 0;
out->tests_errors = 0;
out->window_hash64 = 0;
out->bootstrap_result = SK_BOOTSTRAP_OK;
if (!vm) {
out->bootstrap_result = SK_BOOTSTRAP_INIT_FAIL;
return;
}
/* M7.1a fields */
out->word_count = sk_dict_word_count(vm);
out->here_offset = (uint32_t)vm->here;
out->latest_word_id = vm->latest ? vm->latest->word_id : 0;
out->header_hash64 = sk_dict_canonical_hash(vm);
/* M7.1b fields - from global test stats (if tests were run) */
#ifdef STARFORTH_ENABLE_TESTS
extern TestStats global_test_stats;
out->tests_total = (uint32_t)global_test_stats.total_tests;
out->tests_passed = (uint32_t)global_test_stats.total_pass;
out->tests_failed = (uint32_t)global_test_stats.total_fail;
out->tests_skipped = (uint32_t)global_test_stats.total_skip;
out->tests_errors = (uint32_t)global_test_stats.total_error;
#endif
}
/**
* @brief Print a @c uint32_t value as decimal to the kernel console or stdout.
*
* Converts @p val to a decimal ASCII string using a local buffer (no libc
* required) and emits it via @c console_puts() in kernel builds or
* @c printf() in hosted builds. Used by @c sk_parity_print() to format
* count and ID fields in parity output lines.
*
* @param val 32-bit unsigned value to print.
*/
static void print_u32(uint32_t val) {
char buf[16];
int i = 15;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
#ifdef __STARKERNEL__
console_puts(&buf[i + 1]);
#else
printf("%s", &buf[i + 1]);
#endif
}
/**
* @brief Print a @c uint64_t value as a 16-digit hex string with "0x" prefix.
*
* Formats @p val as "0xNNNNNNNNNNNNNNNN" using a fixed-size local buffer
* (no libc required) and emits it via @c console_puts() in kernel builds or
* @c printf() in hosted builds. Used by @c sk_parity_print() to format hash
* and address fields in parity output lines.
*
* @param val 64-bit value to print as lowercase hex.
*/
static void print_hex64(uint64_t val) {
char buf[19];
buf[0] = '0';
buf[1] = 'x';
for (int i = 17; i >= 2; i--) {
int d = val & 0xF;
buf[i] = (d < 10) ? ('0' + d) : ('a' + d - 10);
val >>= 4;
}
buf[18] = '\0';
#ifdef __STARKERNEL__
console_puts(buf);
#else
printf("%s", buf);
#endif
}
/**
* @brief Print a null-terminated string to the kernel console or stdout.
*
* Emits @p s via @c console_puts() in kernel builds or @c printf() in hosted
* builds. Serves as a single dispatch point so parity output compiles cleanly
* in both environments without scattering @c ifdef guards through the
* formatting code.
*
* @param s Null-terminated string to print.
*/
static void print_str(const char *s) {
#ifdef __STARKERNEL__
console_puts(s);
#else
printf("%s", s);
#endif
}
/**
* @brief Print a newline to the kernel console or stdout.
*
* Emits a line terminator via @c console_println("") in kernel builds or
* @c printf("\n") in hosted builds. Paired with @c print_str() to avoid
* scattering @c ifdef guards through parity formatting code.
*/
static void print_nl(void) {
#ifdef __STARKERNEL__
console_println("");
#else
printf("\n");
#endif
}
/**
* @brief Print a @c ParityPacket to the kernel console or stdout.
*
* Emits two or three structured lines that can be captured in the QEMU serial
* log for offline determinism verification:
*
* - @c "PARITY:M7.1a word_count=N here=0xH latest_id=N hash=0xH" — always
* emitted; contains the structural dictionary fingerprint.
* - @c "PARITY:M7.1b tests=N pass=N fail=N skip=N err=N" — emitted only
* when @c pkt->tests_total > 0; contains POST result counts.
* - @c "PARITY:OK" or @c "PARITY:FAIL code=N" — result verdict line.
*
* All output goes through @c print_str() / @c print_u32() / @c print_hex64()
* / @c print_nl() so the function compiles cleanly in both kernel and hosted
* builds. No-op if @p pkt is @c NULL.
*
* @param pkt Pointer to the @c ParityPacket to display; may be @c NULL.
*/
void sk_parity_print(const ParityPacket *pkt) {
if (!pkt) return;
/* M7.1a line */
print_str("PARITY:M7.1a word_count=");
print_u32(pkt->word_count);
print_str(" here=");
print_hex64(pkt->here_offset);
print_str(" latest_id=");
print_u32(pkt->latest_word_id);
print_str(" hash=");
print_hex64(pkt->header_hash64);
print_nl();
/* M7.1b line (only if tests were run) */
if (pkt->tests_total > 0) {
print_str("PARITY:M7.1b tests=");
print_u32(pkt->tests_total);
print_str(" pass=");
print_u32(pkt->tests_passed);
print_str(" fail=");
print_u32(pkt->tests_failed);
print_str(" skip=");
print_u32(pkt->tests_skipped);
print_str(" err=");
print_u32(pkt->tests_errors);
print_nl();
}
/* Result line */
if (pkt->bootstrap_result == SK_BOOTSTRAP_OK &&
pkt->tests_failed == 0 && pkt->tests_errors == 0) {
print_str("PARITY:OK");
} else {
print_str("PARITY:FAIL code=");
print_u32(pkt->bootstrap_result);
}
print_nl();
}
+45
View File
@@ -0,0 +1,45 @@
/*
* StarKernel Q48.16 fixed-point stubs
*
* Minimal implementations for kernel build - floating point
* conversion is not needed for core VM operation.
*/
#include "q48_16.h"
/*
* @brief Convert a @c double to Q48.16 fixed-point (kernel stub — always 0).
*
* Floating-point arithmetic is unavailable in the freestanding kernel build,
* so this function is a deliberate no-op stub. It discards @p d and returns
* a zero-valued @c q48_16_t. Shared VM code that calls @c q48_from_double()
* only uses it for logging and diagnostic output (e.g., @c WORD-ENTROPY),
* which are suppressed in the kernel build, so the zero return is safe.
*
* @param d Floating-point value to convert (ignored).
* @return Zero-valued @c q48_16_t.
*/
q48_16_t q48_from_double(double d)
{
(void)d;
q48_16_t zero = {0};
return zero;
}
/*
* @brief Convert a Q48.16 fixed-point value to @c double (kernel stub — always 0.0).
*
* Floating-point arithmetic is unavailable in the freestanding kernel build,
* so this function is a deliberate no-op stub. It discards @p q and returns
* @c 0.0. Shared VM code that calls @c q48_to_double() only uses the result
* for logging and RStudio/CSV export paths that are inactive in the kernel
* build, so the zero return is safe.
*
* @param q Q48.16 value to convert (ignored).
* @return @c 0.0 always.
*/
double q48_to_double(q48_16_t q)
{
(void)q;
return 0.0;
}
+157
View File
@@ -0,0 +1,157 @@
/*
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.
*/
/**
* session.c - Per-VM session bookkeeping (FABRIC-2.md §H.12 steps 2-3)
*
* Slot table sized from stadium_max_vm_count() at session_boot_init() time,
* mirroring stadium.c's own StadiumVMQuota table (kmalloc'd to a
* budget-computed count, not a fixed compile-time array -- population isn't
* knowable in advance, same reasoning stadium.c's own history already
* settled). No callers yet; wiring into the boot sequence and into
* stadium_birth_hera()/capsule_birth.c's admission path are later
* §H.12 steps.
*
* session_set_pinned()/session_is_pinned() (step 3) are the pin-authority
* choke point -- see their own doc comments in session.h for the
* authoritative-session-mirrored-Stadium-bit split.
*/
#ifdef __STARKERNEL__
#include "starkernel/session.h"
#include "starkernel/vm/stadium.h"
#include "starkernel/kmalloc.h"
static Session *g_sessions = (Session *)0;
static size_t g_session_slots = 0; /* capacity, from stadium_max_vm_count() */
static size_t g_session_count = 0; /* slots actually in use, 0..g_session_slots */
int session_boot_init(void)
{
size_t max_vm_count = stadium_max_vm_count();
Session *slots;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
slots = (Session *)kmalloc(max_vm_count * sizeof(Session));
if (!slots) return -1;
for (i = 0; i < max_vm_count; i++) {
uint8_t *raw = (uint8_t *)&slots[i];
size_t j;
for (j = 0; j < sizeof(Session); j++) raw[j] = 0;
slots[i].vm_id = vm_uuid_none();
slots[i].parent = vm_uuid_none();
}
g_sessions = slots;
g_session_slots = max_vm_count;
g_session_count = 0;
return 0;
}
Session *session_find(VMUuid vm_id)
{
size_t i;
if (!g_sessions) return (Session *)0;
for (i = 0; i < g_session_slots; i++) {
if (!vm_uuid_is_none(g_sessions[i].vm_id) &&
vm_uuid_equal(g_sessions[i].vm_id, vm_id)) {
return &g_sessions[i];
}
}
return (Session *)0;
}
Session *session_register(VMUuid vm_id, VMUuid parent, const char *name)
{
size_t i;
Session *slot = (Session *)0;
if (!g_sessions) return (Session *)0;
if (session_find(vm_id)) return (Session *)0; /* already registered */
for (i = 0; i < g_session_slots; i++) {
if (vm_uuid_is_none(g_sessions[i].vm_id)) {
slot = &g_sessions[i];
break;
}
}
if (!slot) return (Session *)0; /* table full */
slot->vm_id = vm_id;
slot->parent = parent;
slot->pinned = 0;
slot->stadium_cell = SESSION_STADIUM_CELL_NONE;
{
size_t j = 0;
if (name) {
for (; j < SESSION_NAME_BUF - 1u && name[j]; j++) slot->name[j] = name[j];
}
slot->name[j] = '\0';
}
{
uint8_t *raw = (uint8_t *)&slot->identity;
size_t j;
for (j = 0; j < sizeof(VMIdentity); j++) raw[j] = 0;
}
g_session_count++;
return slot;
}
void session_set_pinned(VMUuid vm_id, int pinned)
{
Session *s = session_find(vm_id);
StadiumCell *cells;
if (!s) return;
s->pinned = pinned ? 1 : 0;
if (s->stadium_cell == SESSION_STADIUM_CELL_NONE) return;
if (s->stadium_cell >= stadium_cell_count()) return;
cells = stadium_cells();
if (!cells) return;
if (pinned) {
cells[s->stadium_cell].header.flags |= STADIUM_FLAG_PIN;
} else {
cells[s->stadium_cell].header.flags &= (uint8_t)~STADIUM_FLAG_PIN;
}
}
int session_is_pinned(VMUuid vm_id)
{
Session *s = session_find(vm_id);
if (!s) return 0;
return s->pinned;
}
#endif /* __STARKERNEL__ */
+853
View File
@@ -0,0 +1,853 @@
/*
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.c - The Stadium cell definitions (item 3.1) and boot-time
* allocation (item 3.2).
*
* Also the translation unit that makes stadium.h's compile-time assertions
* actually get compiled on all three architectures, not merely included by
* something that never builds.
*/
#include "starkernel/vm/stadium.h"
#ifdef __STARKERNEL__
#include "starkernel/kmalloc.h"
#include "starkernel/pmm.h"
#include "starkernel/console.h"
#include "starkernel/log.h" /* log_message() -- stadium_dispatch()'s own trace, LOG_DEBUG */
#include "starkernel/hal/hal.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- item 4.1's reservoir starts each VM's quota at 1.0 */
#include "vm.h" /* VM_MEMORY_SIZE -- the per-VM footprint stadium_max_vm_count() budgets against */
#include "block_subsystem.h" /* blk_flush() -- STADIUM_BEHAVIOUR_MIGRATE's real write-back action */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 4 */
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
static uint8_t *stadium_bitmap = (uint8_t *)0;
static uint16_t *stadium_owner = (uint16_t *)0; /* widened from uint8_t
2026-08-15: the VM-count bound is no longer capped at
255 (see stadium_max_vm_count_val below), so a byte
could silently wrap. 65535 is ample headroom. */
static size_t stadium_ncells = 0;
static int stadium_initialized = 0;
static size_t stadium_max_vm_count_val = 0; /* computed at boot, see
stadium_boot_init() and stadium_max_vm_count() */
/* Sentinel for the header's `link` field while it is reused as a free-list
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
* uint32_t truncates to the same bit pattern as this constant (safe), but
* casting THIS constant back up to size_t does not sign-extend to
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
* size_to_link() conversions below rather than a raw cast either direction. */
#define STADIUM_LINK_NONE ((uint32_t)-1)
static uint32_t size_to_link(size_t v) {
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
}
static size_t link_to_size(uint32_t v) {
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
}
/*
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC-0.md §22.3).
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
* rather than direct indexing.
*/
typedef struct {
VMUuid vm_id;
int in_use;
size_t free_head;
size_t free_count; /* FABRIC-3.md SXVI donor-floor fix: O(1)-maintained
* length of this VM's own free list -- lets
* stadium_grant_quota()'s caller pick whichever VM
* actually has the most spare capacity to split
* from, instead of always Hera specifically (the
* old policy silently starved new VM births once
* HERA's own list ran thin, even while other VMs
* granted a generous initial half sat on nearly all
* of it unused). Kept in exact lockstep with every
* free_head push/pop below and in stadium_evict()/
* stadium_admit(). */
size_t resident_head; /* FABRIC-3.md aarch64 Stadium/COOL scaling fix:
* head of this VM's own resident-cell list, threaded
* through stadium_resident_next[]/stadium_resident_prev[]
* (global, indexed by cell, below). Lets
* stadium_admit()'s eviction-fallback scan walk only
* this VM's own residents -- what its doc already
* claimed ("scoped to that SAME VM's own resident
* patrons only") -- instead of filtering the entire
* stadium_ncells array by owner, which is what the
* scan actually did before this fix. STADIUM_CELL_NONE
* when empty. */
uint64_t reservoir; /* item 4.1, FABRIC-0.md §17.7 -- Q48.16, heat this VM's
* quota holds but no resident patron has claimed.
* Invariant: Σ(resident patron heat) + reservoir ==
* Q48_ONE, checked the same way vm_physics_conserved()
* checks the fleet sum. */
uint64_t consumed; /* FABRIC-3.5.md §XL.4 -- Q48.16, heat this VM's messages
* have decayed away (recorded by kernel-Hermes,
* FABRIC-3.6.md task 2.7). Full invariant:
* Σ(resident) + reservoir + consumed == Q48_ONE. */
} StadiumVMQuota;
/* kmalloc'd at stadium_boot_init() to stadium_max_vm_count_val entries,
* replacing the old static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT]
* (2026-08-15: the bound is computed from RAM, not a compile-time constant). */
static StadiumVMQuota *stadium_quotas = (StadiumVMQuota *)0;
/* Doubly-linked resident-list threading, one entry per cell, parallel to
* stadium_owner[] -- kmalloc'd at stadium_boot_init() to stadium_ncells
* entries each. Only meaningful for cells currently resident (bitmap bit
* set); STADIUM_LINK_NONE-equivalent (via link_to_size()/size_to_link(),
* defined below) terminates each end. Doubly-linked because stadium_evict()
* removes by cell_index directly (not by list-walk), so O(1) unlink needs a
* prev pointer, not just next. */
static uint32_t *stadium_resident_next = (uint32_t *)0;
static uint32_t *stadium_resident_prev = (uint32_t *)0;
/* resident_list_push - O(1) insert cell_index at the front of slot's own
* resident list. Caller must have already set bitmap/owner for cell_index. */
static void resident_list_push(int slot, size_t cell_index) {
size_t old_head = stadium_quotas[slot].resident_head;
stadium_resident_prev[cell_index] = size_to_link(STADIUM_CELL_NONE);
stadium_resident_next[cell_index] = size_to_link(old_head);
if (old_head != STADIUM_CELL_NONE) {
stadium_resident_prev[old_head] = size_to_link(cell_index);
}
stadium_quotas[slot].resident_head = cell_index;
}
/* resident_list_remove - O(1) splice cell_index out of slot's own resident
* list. Caller must call this BEFORE clearing cell_index's bitmap bit. */
static void resident_list_remove(int slot, size_t cell_index) {
size_t prev = link_to_size(stadium_resident_prev[cell_index]);
size_t next = link_to_size(stadium_resident_next[cell_index]);
if (prev != STADIUM_CELL_NONE) {
stadium_resident_next[prev] = size_to_link(next);
} else {
stadium_quotas[slot].resident_head = next;
}
if (next != STADIUM_CELL_NONE) {
stadium_resident_prev[next] = size_to_link(prev);
}
}
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
static int quota_slot_for_vm(VMUuid vm_id) {
size_t i;
for (i = 0; i < stadium_max_vm_count_val; i++) {
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
}
return -1;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading zeros. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
int stadium_boot_init(void) {
/* Budgeted from kmalloc_get_stats(), not pmm_get_stats() (corrected
* 2026-08-15 -- see this function's doc in stadium.h): kmalloc_init()
* (M6) already carved its own fixed-size heap out of PMM before this
* ever runs, and every allocation below draws from that heap, not raw
* PMM, so PMM's own free-byte figure was the wrong pool to budget
* against. */
kmalloc_stats_t kstats = kmalloc_get_stats();
uint64_t budget_bytes = (kstats.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
size_t ncells = (size_t)(budget_bytes / STADIUM_CELL_BYTES);
size_t bitmap_bytes = (ncells + 7u) / 8u;
if (ncells == 0) {
console_println("Stadium: boot-time allocation skipped (0 cells from memory budget)");
return -1;
}
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
uint16_t *owner = (uint16_t *)kmalloc(ncells * sizeof(uint16_t));
uint32_t *resident_next = (uint32_t *)kmalloc(ncells * sizeof(uint32_t));
uint32_t *resident_prev = (uint32_t *)kmalloc(ncells * sizeof(uint32_t));
if (!cells || !bitmap || !owner || !resident_next || !resident_prev) {
console_println("Stadium: kmalloc failed for boot-time allocation");
if (cells) kfree(cells);
if (bitmap) kfree(bitmap);
if (owner) kfree(owner);
if (resident_next) kfree(resident_next);
if (resident_prev) kfree(resident_prev);
return -1;
}
/* VM population bound (2026-08-15, replaces compile-time
* STADIUM_MAX_VM_COUNT): STADIUM_VM_MEMORY_PERCENT of whatever's left
* in the kmalloc heap AFTER the cell array above, divided by
* VM_MEMORY_SIZE (the real per-VM footprint -- 5 MiB, include/vm.h --
* not the small quota/word-slot bookkeeping tables). Floored to 1 so
* Hera can always boot; no ceiling otherwise (Captain Bob, 2026-08-15:
* the population is not knowable in advance). Clamped to what the
* uint16_t owner array can index, logged if that ever actually bites. */
kmalloc_stats_t kstats_after_cells = kmalloc_get_stats();
uint64_t vm_budget_bytes = (kstats_after_cells.free_bytes * (uint64_t)STADIUM_VM_MEMORY_PERCENT) / 100u;
size_t max_vm_count = (size_t)(vm_budget_bytes / VM_MEMORY_SIZE);
if (max_vm_count < 1u) max_vm_count = 1u;
if (max_vm_count > 65535u) {
console_println("Stadium: computed VM count exceeds owner-index range, clamped to 65535");
max_vm_count = 65535u;
}
StadiumVMQuota *quotas = (StadiumVMQuota *)kmalloc(max_vm_count * sizeof(StadiumVMQuota));
if (!quotas) {
console_println("Stadium: kmalloc failed for VM quota table");
kfree(cells);
kfree(bitmap);
kfree(owner);
kfree(resident_next);
kfree(resident_prev);
return -1;
}
{
uint8_t *raw = (uint8_t *)cells;
size_t n = ncells * STADIUM_CELL_BYTES;
size_t i;
for (i = 0; i < n; i++) raw[i] = 0;
}
{
size_t i;
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
}
/* Item 3.7: chain every cell into one free list, ascending index order
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
* cell's own `link` field as the next-free-cell pointer while
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
{
size_t i;
for (i = 0; i < ncells; i++) {
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
cells[i].header.contains = STADIUM_CONTAINS_NONE;
owner[i] = 0;
/* Not on any resident list yet -- nothing is resident until
* stadium_admit() puts it there. */
resident_next[i] = size_to_link(STADIUM_CELL_NONE);
resident_prev[i] = size_to_link(STADIUM_CELL_NONE);
}
}
{
size_t i;
for (i = 0; i < max_vm_count; i++) {
quotas[i].vm_id = vm_uuid_none();
quotas[i].in_use = 0;
quotas[i].free_head = STADIUM_CELL_NONE;
quotas[i].free_count = 0;
quotas[i].resident_head = STADIUM_CELL_NONE;
quotas[i].reservoir = 0;
quotas[i].consumed = 0;
}
}
quotas[0].vm_id = vm_uuid_hera();
quotas[0].in_use = 1;
quotas[0].free_head = 0;
quotas[0].free_count = ncells;
/* item 4.1, §17.7: at quota-grant time, before any resident patron
* exists, the reservoir holds the VM's entire conserved share -- mirrors
* Hera holding the fleet's whole Q48_ONE before any other VM is born. */
quotas[0].reservoir = Q48_ONE;
stadium_cell_array = cells;
stadium_bitmap = bitmap;
stadium_owner = owner;
stadium_resident_next = resident_next;
stadium_resident_prev = resident_prev;
stadium_ncells = ncells;
stadium_quotas = quotas;
stadium_max_vm_count_val = max_vm_count;
stadium_initialized = 1;
console_puts("Stadium: ");
console_put_u64((uint64_t)ncells);
console_puts(" cells (");
console_put_u64((uint64_t)(ncells * STADIUM_CELL_BYTES) / 1024u);
console_puts(" KB), ");
console_put_u64((uint64_t)max_vm_count);
console_println(" VM slots");
return 0;
}
int stadium_is_initialized(void) {
return stadium_initialized;
}
size_t stadium_cell_count(void) {
return stadium_ncells;
}
size_t stadium_max_vm_count(void) {
return stadium_max_vm_count_val;
}
StadiumCell *stadium_cells(void) {
return stadium_cell_array;
}
uint8_t *stadium_header_bitmap(void) {
return stadium_bitmap;
}
/*
* Exhaustive switch, no default: §13/§18.3 require the behaviour set to be
* closed and fixed at build time. With -Wall -Werror, omitting a case here
* for a tag that exists is a build failure, not a silent gap -- the compiler
* enforces closedness, not just this comment.
*
* MIGRATE (blocks) is real, FABRIC-2.md §B: write-back via blk_flush(),
* called with the departing patron's identity (the LBN) -- stadium_evict()
* calls this BEFORE zeroing the header, so `identity` is still valid here.
*
* DELIVER (Hermes messages) needs no extra payload action here, confirmed
* 2026-08-26 (FABRIC-2.md §B): capsules/hermes/init.4th's MSG-ALLOC already
* admits every message with this behaviour, and MSG-FREE-NODE (called from
* both explicit ack and heat-driven reap) already evicts it -- has been
* real and live on every boot for weeks. The actual delivery action
* (VM-EXEC of the payload, MSG-DELIVER) already runs in FORTH, decoupled
* from and prior to Stadium reap; by the time this dispatches, delivery is
* already done. Was printed as "(stub)" for a long time regardless, which
* was simply wrong, not a real gap -- fixed to print the departing
* message's identity, same shape as MIGRATE's lbn print, instead of
* claiming to be unimplemented.
*
* COOL (words, item 4.1; VMs, FABRIC-2.md §B) needs no extra payload action
* here either, same reasoning as DELIVER: stadium_evict()'s own universal
* reservoir credit is the whole of what "cooling off the floor" means for
* both patron kinds -- real and live for both since item 4.1 and this
* session respectively. Fixed the same way, to print the departing
* patron's identity (word_id for a word, 0 -- the patron-zero convention --
* for a VM) instead of a misleading "(stub)" label.
*
* EXPIRE (ACL) alone remains genuinely unbuilt -- no ACL-Stadium
* integration of any kind exists yet (FABRIC-2.md §B).
*/
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour) {
/* 2026-09-22: was an unconditional console_puts()/console_println()
* sequence -- the only Stadium trace with no level gating at all,
* printing on every single dispatch regardless of log level
* (confirmed live: interleaves visibly with ordinary interactive
* console sessions). Routed through log_message(LOG_DEBUG, ...)
* instead, matching every other diagnostic trace in this codebase --
* silent at the default LOG_WARN runtime level, available with
* --log-level=debug. */
switch (behaviour) {
case STADIUM_BEHAVIOUR_MIGRATE: {
uint32_t lbn = (uint32_t)stadium_cell_array[cell_index].header.identity;
log_message(LOG_DEBUG, "Stadium: dispatch cell=%zu behaviour=MIGRATE lbn=%u",
cell_index, (unsigned)lbn);
blk_flush(lbn);
break;
}
case STADIUM_BEHAVIOUR_DELIVER: {
uint64_t msg_idx = stadium_cell_array[cell_index].header.identity;
log_message(LOG_DEBUG, "Stadium: dispatch cell=%zu behaviour=DELIVER msg_idx=%llu",
cell_index, (unsigned long long)msg_idx);
break;
}
case STADIUM_BEHAVIOUR_EXPIRE:
log_message(LOG_DEBUG, "Stadium: dispatch cell=%zu behaviour=EXPIRE (stub)",
cell_index);
break;
case STADIUM_BEHAVIOUR_COOL: {
uint64_t identity = stadium_cell_array[cell_index].header.identity;
log_message(LOG_DEBUG, "Stadium: dispatch cell=%zu behaviour=COOL identity=%llu",
cell_index, (unsigned long long)identity);
break;
}
}
}
/*
* FABRIC-0.md §19.2/§19.3: density is heat / mass, read on demand from fields
* already in the header -- not a value a scheduler maintains. mass == 0
* (an empty or never-admitted slot; everything is zero-initialized until
* something is actually born into the Stadium, which nothing yet does)
* returns 0 rather than dividing by zero. An out-of-range cell_index also
* returns 0 -- there is no patron there to be dense.
*/
uint64_t stadium_density(size_t cell_index) {
StadiumPatronHeader *header;
if (cell_index >= stadium_ncells) return 0;
header = &stadium_cell_array[cell_index].header;
if (header->mass == 0) return 0;
return header->heat / (uint64_t)header->mass;
}
/* STADIUM_FLAG_PIN moved to stadium.h (FABRIC-2.md §H.12 step 3) -- see its
* doc comment there for why. */
static int bitmap_get(size_t cell_index) {
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
}
static void bitmap_set(size_t cell_index) {
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
}
static void bitmap_clear(size_t cell_index) {
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
}
/*
* FABRIC-0.md §17.2: reap means leaves the floor, not destroyed. Refuses a
* pinned header (§3) or one with a non-none `contains` (item 1.1: a patron
* holding another cannot be reaped). Refuses an out-of-range index or a cell
* whose discriminator bit is not set -- nothing resident there to reap.
*
* §20.5 #3 / item 3.6: panics, does not return, if a resident cell 0 (Hera,
* patron zero) is ever selected -- checked before pin/contains below, on
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
* via the ordinary refusal path instead of surfacing it.
*/
int stadium_evict(size_t cell_index) {
StadiumPatronHeader *header;
uint16_t slot;
if (cell_index >= stadium_ncells) return -1;
if (!bitmap_get(cell_index)) return -1;
if (cell_index == STADIUM_HERA_CELL_INDEX) {
sk_hal_panic("Stadium: eviction selected patron zero (Hera) -- governor invariant broken");
}
header = &stadium_cell_array[cell_index].header;
if (header->flags & STADIUM_FLAG_PIN) return -1;
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
slot = stadium_owner[cell_index];
resident_list_remove((int)slot, cell_index);
bitmap_clear(cell_index);
/* item 4.1, §17.7: the departing patron's remaining heat must flow back
* to its owner's reservoir before the cell returns to the free list, or
* every reap leaks heat and Σ(resident) + reservoir drifts below
* Q48_ONE. Captured BEFORE the zero-fill below, which would otherwise
* destroy it. */
stadium_quotas[slot].reservoir += header->heat;
{
uint8_t *raw = (uint8_t *)header;
size_t i;
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
}
/* Item 3.7: return the freed cell to its owning VM's free list. contains
* must be set to the real "none" sentinel here, not left at the zero
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
* would make this freed cell look permanently "contains Hera" to the
* very next admit() that pops it. */
header->link = size_to_link(stadium_quotas[slot].free_head);
header->contains = STADIUM_CONTAINS_NONE;
stadium_quotas[slot].free_head = cell_index;
stadium_quotas[slot].free_count++;
return 0;
}
/*
* FABRIC-0.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
* rule only governs the full case). Only if that list is empty does this
* fall back to eviction, scoped to that SAME VM's own residents (quota
* isolation), finding the least-dense evictable one (pinned and
* contains-gated residents are skipped, never eviction candidates) and
* evicting it only if the candidate is strictly denser.
*/
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
int slot;
size_t idx;
size_t least_dense_index = STADIUM_CELL_NONE;
uint64_t least_dense_value = 0;
uint64_t candidate_density;
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
/* Multi-cell patrons need a continuation chain, and no header field is
* documented as carrying one's first index -- see this function's doc
* in stadium.h. Refuse rather than admit only the header and leak the
* rest, which would break capacity conservation. */
if (candidate->mass != 1) return STADIUM_CELL_NONE;
/* contains must be a real "none" or a real index -- catches garbage/
* uninitialized values, though not a zero-initialized candidate that
* meant "none": 0 is Hera's valid index, so that case is a caller
* contract issue this function cannot detect (see the header doc). */
if (candidate->contains != STADIUM_CONTAINS_NONE &&
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
slot = quota_slot_for_vm(vm_id);
if (slot < 0) return STADIUM_CELL_NONE;
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_quotas[slot].free_count--;
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
/* Item 4.2 fix (§25.7): record ownership so stadium_evict()'s
* reservoir credit and free-list return land on the VM that actually
* admitted this patron, not whatever owner[idx] held at boot. */
stadium_owner[idx] = (uint16_t)slot;
resident_list_push(slot, idx);
return idx;
}
/* FABRIC-3.md aarch64 Stadium/COOL scaling fix: this used to be a
* for (i = 0; i < stadium_ncells; i++) scan of the ENTIRE global array,
* filtered down to this VM's own owned cells by an `owner[i] != slot`
* check inside the loop -- O(stadium_ncells) on every call that reaches
* here, not O(this VM's own resident count) as the function's own doc
* above already claimed ("scoped to that SAME VM's own residents").
* Root-caused live (2026-09-11/12): word-execution admission
* (stadium_word_dispatch(), stadium_words.c) calls stadium_admit() once
* per distinct word a VM's own capsule executes, and each VM's granted
* quota is a small, halving fraction of Hera's original pool -- once
* that quota's free list empties (a handful of words in), EVERY further
* new word this VM ever executes hit this scan. At stadium_ncells in
* the hundreds of thousands (aarch64's -m 4096 vs. amd64/riscv64's
* -m 1024 --> a 4x larger kmalloc heap --> a 4x larger Stadium, see
* FABRIC-3.md SXV/SXVI), repeated hundred-thousand-cell scans compounded
* into the observed 90+ minute stall. Walking this VM's own
* resident_head list instead bounds the cost by how many cells this VM
* actually owns (<= its granted quota, itself <= a small constant
* multiple of DICTIONARY_SIZE in practice), independent of
* stadium_ncells. */
for (idx = stadium_quotas[slot].resident_head; idx != STADIUM_CELL_NONE;
idx = link_to_size(stadium_resident_next[idx])) {
StadiumPatronHeader *h = &stadium_cell_array[idx].header;
if (h->flags & STADIUM_FLAG_PIN) continue;
if (h->contains != STADIUM_CONTAINS_NONE) continue;
{
uint64_t d = stadium_density(idx);
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
least_dense_index = idx;
least_dense_value = d;
}
}
}
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
* list -- its owner is `slot`, the same quota we scoped the search to.
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
* yet built), so nothing else can have touched the list meanwhile; pop
* it straight back off. */
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_quotas[slot].free_count--;
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
/* Same fix as the free-list-pop path above -- stadium_evict() just wrote
* owner[idx] = slot as part of reaping least_dense_index, so this is
* currently a no-op in practice, but it must not be assumed to stay a
* no-op: this is the correctness statement, not a redundant write. */
stadium_owner[idx] = (uint16_t)slot;
resident_list_push(slot, idx);
return idx;
}
/*
* FABRIC-0.md item 4.1a: one-time initial quota grant, not item 1.3's
* (still-unbuilt) recurring transfer. See stadium.h's doc for the full
* argument. from_slot's free-list length is now read directly from its
* O(1)-maintained free_count (FABRIC-3.md SXVI donor-floor fix) rather than
* walked -- the walk below only detaches the first `half` cells into
* new_vm_id's own list, reassigning owner as it goes.
*/
int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
int from_slot, new_slot, i;
size_t half, idx, last_new, new_head, remainder_head;
if (!stadium_initialized) return -1;
if (quota_slot_for_vm(new_vm_id) >= 0) return -1;
from_slot = quota_slot_for_vm(from_vm_id);
if (from_slot < 0) return -1;
new_slot = -1;
for (i = 0; i < (int)stadium_max_vm_count_val; i++) {
if (!stadium_quotas[i].in_use) { new_slot = i; break; }
}
if (new_slot < 0) return -1;
half = stadium_quotas[from_slot].free_count / 2;
if (half == 0) return -1; /* fewer than 2 free cells -- nothing to split */
new_head = stadium_quotas[from_slot].free_head;
idx = new_head;
last_new = STADIUM_CELL_NONE;
for (i = 0; i < (int)half; i++) {
stadium_owner[idx] = (uint16_t)new_slot;
last_new = idx;
idx = link_to_size(stadium_cell_array[idx].header.link);
}
/* idx now points to the first cell staying with from_slot (or
* STADIUM_CELL_NONE if half == count, i.e. an even list fully moved). */
remainder_head = idx;
stadium_cell_array[last_new].header.link = size_to_link(STADIUM_CELL_NONE);
stadium_quotas[from_slot].free_head = remainder_head;
stadium_quotas[from_slot].free_count -= half;
stadium_quotas[new_slot].vm_id = new_vm_id;
stadium_quotas[new_slot].in_use = 1;
stadium_quotas[new_slot].free_head = new_head;
stadium_quotas[new_slot].free_count = half;
stadium_quotas[new_slot].resident_head = STADIUM_CELL_NONE;
stadium_quotas[new_slot].reservoir = Q48_ONE;
stadium_quotas[new_slot].consumed = 0;
return 0;
}
/*
* stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: which currently
* in-use VM has the most free (unclaimed) cells right now. Callers granting
* a new VM's initial quota (capsule_birth.c) should split from THIS VM, not
* unconditionally from Hera -- the old always-Hera policy meant her own free
* list converged toward empty after a bounded number of grants (each grant
* halves what remains) while every VM she had already granted to typically
* still held almost all of its own share untouched, silently starving every
* later birth even though the Stadium as a whole was nowhere near full.
* O(stadium_max_vm_count_val) -- a linear scan over quota SLOTS (bounded by
* live VM population, not cell count), not the cells themselves.
*
* @return The VM with the largest free_count, or vm_uuid_none() if no VM
* holds a quota yet (Stadium not initialized, or called before
* Hera's own boot-time grant).
*/
VMUuid stadium_best_donor(void) {
size_t i;
size_t best_count = 0;
int found = 0;
VMUuid best = vm_uuid_none();
if (!stadium_initialized) return best;
for (i = 0; i < stadium_max_vm_count_val; i++) {
if (!stadium_quotas[i].in_use) continue;
if (!found || stadium_quotas[i].free_count > best_count) {
best_count = stadium_quotas[i].free_count;
best = stadium_quotas[i].vm_id;
found = 1;
}
}
return best;
}
/* Shared by stadium_cell_heat_get()/_set(): resident AND owned by vm_id's
* own quota slot. Returns the quota slot on success, -1 on any refusal. */
static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return -1;
if (cell_index >= stadium_ncells) return -1;
if (!bitmap_get(cell_index)) return -1;
if (stadium_owner[cell_index] != (uint16_t)slot) return -1;
return slot;
}
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
return stadium_cell_array[cell_index].header.heat;
}
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
int slot = owned_resident_slot(vm_id, cell_index);
uint64_t old_heat, delta, pulled;
if (slot < 0) return -1;
old_heat = stadium_cell_array[cell_index].header.heat;
if (new_heat == old_heat) return 0;
if (new_heat > old_heat) {
delta = new_heat - old_heat;
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
stadium_quotas[slot].reservoir -= pulled;
} else {
delta = old_heat - new_heat;
stadium_quotas[slot].reservoir += delta;
}
stadium_cell_array[cell_index].header.heat = new_heat;
return 0;
}
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
uint64_t pulled;
if (slot < 0) return 0;
pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
stadium_quotas[slot].reservoir -= pulled;
return pulled;
}
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return;
stadium_quotas[slot].reservoir += amount;
}
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
return stadium_quotas[slot].reservoir;
}
int stadium_quota_slot_for_vm(VMUuid vm_id) {
return quota_slot_for_vm(vm_id);
}
uint64_t stadium_resident_sum(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
uint64_t sum = 0;
size_t i;
if (slot < 0) return 0;
for (i = 0; i < stadium_ncells; i++) {
if (!bitmap_get(i)) continue;
if (stadium_owner[i] != (uint16_t)slot) continue;
sum += stadium_cell_array[i].header.heat;
}
return sum;
}
void stadium_consumed_record(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return;
stadium_quotas[slot].consumed += amount;
}
uint64_t stadium_consumed_peek(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
return stadium_quotas[slot].consumed;
}
int stadium_conserved(VMUuid vm_id) {
return (stadium_resident_sum(vm_id) + stadium_reservoir_peek(vm_id) +
stadium_consumed_peek(vm_id)) == (uint64_t)Q48_ONE;
}
/*
* FABRIC-0.md item 3.6 / item 4.1: see stadium.h's doc. Idempotent via the
* item-3.1 discriminator bitmap -- if cell 0 already reads as resident,
* something already birthed her (or, if it isn't actually Hera, something
* else already claimed cell 0 -- either way this must not clobber it).
*
* FABRIC-2.md §H.12 step 4: pinning no longer happens by setting
* STADIUM_FLAG_PIN on the candidate before admission -- session.c's
* session_set_pinned() is now the sole choke point for that bit (§H.10).
* Hera is admitted unpinned, then registered as session zero
* (self-referential parent, matching capsule_run.h's parent_vm_id ==
* vm_id convention for the root) and pinned through the session layer.
* Session registration/pinning is soft-fail (logged, not fatal) -- Hera's
* actual Stadium admission above already succeeded and is what the
* patron-zero invariant is about; session.c not yet being initialized
* must not break that.
*/
int stadium_birth_hera(void) {
StadiumPatronHeader candidate;
size_t idx;
if (!stadium_initialized) return -1;
if (stadium_ncells > 0 && bitmap_get(STADIUM_HERA_CELL_INDEX)) return 0;
{
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
}
candidate.identity = 0;
candidate.heat = 0;
candidate.ttl = 0;
candidate.link = STADIUM_LINK_NONE;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_uuid_hera(), &candidate);
if (idx == STADIUM_CELL_NONE) return -1; /* refused; should not happen (quota is fresh and empty) */
if (idx != STADIUM_HERA_CELL_INDEX) {
sk_hal_panic("Stadium: birth_hera did not land on cell 0 -- patron-zero invariant broken");
}
{
Session *s = session_register(vm_uuid_hera(), vm_uuid_hera(), "Hera");
if (s) {
s->stadium_cell = idx;
session_set_pinned(vm_uuid_hera(), 1);
} else {
console_println("Stadium: session_register failed for Hera (session.c not yet initialized?)");
}
}
return 0;
}
#endif /* __STARKERNEL__ */
+352
View File
@@ -0,0 +1,352 @@
/*
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.c - Block patrons on the Stadium (FABRIC-2.md §B/§D,
* MIGRATE punch-list item). See stadium_blocks.h for the design rationale.
*/
#include "starkernel/vm/stadium_blocks.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium.h"
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE */
#include "starforth_config.h" /* STADIUM_BLOCK_HEAT_QUANTUM, STADIUM_BLOCK_COOL_RATE_Q48,
STADIUM_BLOCK_TRACK_CAP_MULT */
/*
* StadiumBlockSlot - one entry in the fixed-capacity open-addressing hash
* table. Keyed by (quota_slot, lbn) together -- LBN numbering is global
* across VMs (unlike word_id), but two VMs touching the SAME lbn must still
* land in different Stadium cells under different quotas, so both fields
* are part of the key (same lesson stadium_words.c's item 4.2 fix already
* applied for word_id).
*
* `state` is three-valued, not a bool: standard open-addressing deletion
* needs EMPTY vs. TOMBSTONE distinguished from OCCUPIED, or reclaiming a
* stale entry's slot (self-healing, see stadium_word_dispatch()'s identical
* problem for the dense word map) would silently break the probe chain for
* any other key that happened to hash into the same bucket and probed past
* this slot to find its own home.
*/
typedef enum {
BLOCK_SLOT_EMPTY = 0,
BLOCK_SLOT_OCCUPIED,
BLOCK_SLOT_TOMBSTONE
} StadiumBlockSlotState;
typedef struct {
uint8_t state;
int16_t quota_slot;
uint32_t lbn;
size_t cell_index;
uint64_t last_decay_tick;
} StadiumBlockSlot;
static StadiumBlockSlot *block_slots = (StadiumBlockSlot *)0;
static size_t block_slots_cap = 0;
static int blocks_initialized = 0;
/* Per-quota-slot counters, kmalloc'd to stadium_max_vm_count() entries --
* same reasoning as stadium_words.c's stat_promotions/stat_evictions: a
* single global counter can't answer "how is vm_id's own table doing"
* once more than one VM holds a quota. */
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
void stadium_blocks_init(void) {
size_t vm_count;
size_t cell_count;
size_t i;
if (blocks_initialized) return; /* not re-entrant -- see stadium_blocks.h */
cell_count = stadium_cell_count();
vm_count = stadium_max_vm_count();
if (cell_count == 0 || vm_count == 0) {
console_println("Stadium blocks: init skipped (Stadium not initialized)");
return;
}
block_slots_cap = cell_count * (size_t)STADIUM_BLOCK_TRACK_CAP_MULT;
block_slots = (StadiumBlockSlot *)kmalloc(block_slots_cap * sizeof(StadiumBlockSlot));
stat_promotions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
if (!block_slots || !stat_promotions || !stat_evictions) {
console_println("Stadium blocks: kmalloc failed for tracking tables");
if (block_slots) kfree(block_slots);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
block_slots = (StadiumBlockSlot *)0;
block_slots_cap = 0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < block_slots_cap; i++) {
block_slots[i].state = BLOCK_SLOT_EMPTY;
block_slots[i].quota_slot = -1;
block_slots[i].lbn = 0;
block_slots[i].cell_index = STADIUM_CELL_NONE;
block_slots[i].last_decay_tick = 0;
}
for (i = 0; i < vm_count; i++) {
stat_promotions[i] = 0;
stat_evictions[i] = 0;
}
blocks_initialized = 1;
}
static int cell_is_resident(size_t idx) {
const uint8_t *bm = stadium_header_bitmap();
if (!bm) return 0;
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* block_hash - Multiplicative mix of (quota_slot, lbn) into [0, block_slots_cap).
* No libc hash dependency (freestanding kernel); Knuth's multiplicative
* method (constant 2654435761, the standard 32-bit golden-ratio prime) is
* enough to spread a monotonically-touched LBN sequence across buckets --
* this table's job is short probe chains under normal access patterns, not
* cryptographic distribution.
*/
static size_t block_hash(int16_t quota_slot, uint32_t lbn) {
uint32_t key = lbn ^ ((uint32_t)(uint16_t)quota_slot << 16) ^ ((uint32_t)(uint16_t)quota_slot);
key = key * 2654435761u;
return (size_t)(key % (uint32_t)block_slots_cap);
}
/*
* table_probe - Single linear-probe pass serving both lookup and insertion.
* Walks at most block_slots_cap slots starting at the key's hash bucket.
*
* - If an OCCUPIED slot matching (quota_slot, lbn) is found: returns its
* index, *out_found = 1.
* - Else, if any EMPTY or TOMBSTONE slot was seen along the way: returns
* the FIRST such slot's index (so insertion reuses the earliest opening,
* standard open-addressing practice), *out_found = 0.
* - Else (table genuinely full, no opening and no match): returns
* block_slots_cap, *out_found = 0.
*
* Probing must continue past TOMBSTONE slots when searching for a match --
* a tombstone marks "something used to live here," not "the chain ends
* here," or a live entry further down the same chain would become
* unreachable.
*/
static size_t table_probe(int16_t quota_slot, uint32_t lbn, int *out_found) {
size_t start = block_hash(quota_slot, lbn);
size_t first_open = block_slots_cap; /* sentinel: none seen yet */
size_t i;
for (i = 0; i < block_slots_cap; i++) {
size_t idx = (start + i) % block_slots_cap;
StadiumBlockSlot *s = &block_slots[idx];
if (s->state == BLOCK_SLOT_EMPTY) {
if (first_open == block_slots_cap) first_open = idx;
break; /* empty slot ends any possible chain for this key */
}
if (s->state == BLOCK_SLOT_TOMBSTONE) {
if (first_open == block_slots_cap) first_open = idx;
continue;
}
/* OCCUPIED */
if (s->quota_slot == quota_slot && s->lbn == lbn) {
*out_found = 1;
return idx;
}
}
*out_found = 0;
return first_open; /* block_slots_cap if truly full */
}
/*
* block_dispatch_pull - Reservoir pull for block-touch admission, same
* Q48_ONE / 3 floor stadium_word_dispatch()'s word_dispatch_pull() enforces
* and for the identical reason (FABRIC-0.md §25.5/§25.7): block-touch
* admission alone must never be able to drain a VM's reservoir below the
* floor other reservoir consumers (word execution, application-level
* pulls) depend on. Own copy, not shared code across files -- same
* decision stadium_words.c's own version already made.
*/
static uint64_t block_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks) {
int slot;
int found;
size_t idx;
int stale = 0;
if (!blocks_initialized) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
idx = table_probe((int16_t)slot, lbn, &found);
if (found) {
StadiumBlockSlot *bs = &block_slots[idx];
size_t cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) {
stale = 1;
} else if (stadium_cells()[cell].header.identity != (uint64_t)lbn) {
stale = 1;
}
if (!stale) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - bs->last_decay_tick;
if (elapsed > 0) {
uint64_t per_tick = (h->heat * (uint64_t)STADIUM_BLOCK_COOL_RATE_Q48) >> 16;
uint64_t cooled = per_tick * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
bs->last_decay_tick = heartbeat_ticks;
}
h->heat += block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
return;
}
/* Stale: reclaim this exact slot -- idx is still this key's home. */
bs->state = BLOCK_SLOT_TOMBSTONE;
stat_evictions[slot]++;
}
if (idx >= block_slots_cap) return; /* table full: skip tracking this touch */
{
uint64_t pulled = block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
size_t cell_idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)lbn;
candidate.heat = pulled;
candidate.ttl = 0;
candidate.link = 0; /* unused for block patrons, same as words */
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0; /* unpinned */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_MIGRATE;
cell_idx = stadium_admit(vm_id, &candidate);
if (cell_idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
return;
}
block_slots[idx].state = BLOCK_SLOT_OCCUPIED;
block_slots[idx].quota_slot = (int16_t)slot;
block_slots[idx].lbn = lbn;
block_slots[idx].cell_index = cell_idx;
block_slots[idx].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
/* Freestanding: no libc printf. Same small utility stadium_words.c
* duplicates locally (and stadium.c before it) -- prints an unsigned
* decimal, no leading zeros. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) {
int slot = stadium_quota_slot_for_vm(vm_id);
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum = 0;
uint64_t reservoir;
size_t i;
if (!blocks_initialized || slot < 0) {
console_println("Stadium blocks: not initialized or vm_id holds no quota");
return;
}
promotions = stat_promotions[slot];
evictions = stat_evictions[slot];
/* Block-only resident sum: walk this VM's own table entries, same
* self-healing residency check stadium_block_dispatch() uses -- a
* stale entry contributes 0, not garbage. Not exposed as a separate
* API (unlike stadium_words_resident_heat()) since nothing else needs
* it yet; add one if a caller shows up. */
for (i = 0; i < block_slots_cap; i++) {
StadiumBlockSlot *bs = &block_slots[i];
size_t cell;
if (bs->state != BLOCK_SLOT_OCCUPIED || bs->quota_slot != (int16_t)slot) continue;
cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) continue;
if (stadium_cells()[cell].header.identity != (uint64_t)bs->lbn) continue;
resident_sum += stadium_cells()[cell].header.heat;
}
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium blocks: promotions=");
console_put_u64(promotions);
console_puts(" evictions=");
console_put_u64(evictions);
console_println("");
console_puts("Stadium blocks resident_sum=");
console_put_u64(resident_sum);
console_puts(" reservoir=");
console_put_u64(reservoir);
console_println(" (block-only term, not a standalone Q48_ONE check -- reservoir is shared)");
}
#endif /* __STARKERNEL__ */
+342
View File
@@ -0,0 +1,342 @@
/*
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.c - Word patrons on the Stadium (FABRIC-0.md §17.3/§17.7,
* punch list item 4.1). See stadium_words.h for the design rationale.
*/
#include "starkernel/vm/stadium_words.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium.h"
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- diagnostic print only */
#include "vm.h" /* DICTIONARY_SIZE, WORD_ID_INVALID */
/*
* StadiumWordSlot - the kernel-side word_id -> cell_index map (decided
* 2026-08-05: no DictEntry field). `last_decay_tick` is this layer's own
* bookkeeping, separate from DictEntry.physics.last_decay_tick -- that field
* belongs to execution_heat's decay, which item 4.1 does not touch.
*
* item 4.2 fix (FABRIC-0.md §25.5): keyed by [quota slot][word_id], not just
* word_id. word_id is assigned per-VM (vm->next_word_id in
* dictionary_management.c), not globally unique -- a single shared
* word_id -> cell_index map let two VMs' independently-numbered word_ids
* (e.g. both VMs' own "DUP") alias onto the same slot, so one VM's dispatch
* could cool/heat-pump a cell it did not own and credit/debit the wrong
* VM's reservoir. Exposed only because item 4.2 restored a second VM
* (Hermes) with her own dictionary; invisible with Hera alone.
*/
typedef struct {
size_t cell_index; /* STADIUM_CELL_NONE if not resident */
uint64_t last_decay_tick;
} StadiumWordSlot;
/* Row-per-quota-slot, kmalloc'd at stadium_words_init() to stadium_max_vm_
* count() rows of DICTIONARY_SIZE entries each -- replaces the old static
* word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE] (2026-08-15: the VM
* count bound is computed from RAM, not a compile-time constant, so this
* can no longer be a flat static array). */
static StadiumWordSlot **word_slots = (StadiumWordSlot **)0;
static int words_initialized = 0;
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
void stadium_words_init(void) {
size_t slot;
uint32_t i;
size_t count = stadium_max_vm_count();
if (words_initialized) return; /* not re-entrant -- see stadium_words.h */
if (count == 0) {
console_println("Stadium words: init skipped (Stadium not initialized)");
return;
}
word_slots = (StadiumWordSlot **)kmalloc(count * sizeof(StadiumWordSlot *));
stat_promotions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
if (!word_slots || !stat_promotions || !stat_evictions) {
console_println("Stadium words: kmalloc failed for per-VM tables");
if (word_slots) kfree(word_slots);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (slot = 0; slot < count; slot++) {
word_slots[slot] = (StadiumWordSlot *)kmalloc(DICTIONARY_SIZE * sizeof(StadiumWordSlot));
if (!word_slots[slot]) {
console_println("Stadium words: kmalloc failed for a word-slot row");
/* Free everything allocated so far, including this row's
* predecessors, and bail the same all-or-nothing way
* stadium_boot_init() does. */
{
size_t j;
for (j = 0; j < slot; j++) kfree(word_slots[j]);
}
kfree(word_slots);
kfree(stat_promotions);
kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[slot][i].cell_index = STADIUM_CELL_NONE;
word_slots[slot][i].last_decay_tick = 0;
}
stat_promotions[slot] = 0;
stat_evictions[slot] = 0;
}
words_initialized = 1;
}
static int cell_is_resident(size_t idx) {
const uint8_t *bm = stadium_header_bitmap();
if (!bm) return 0;
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* word_dispatch_pull - Reservoir pull for word-execution admission, clamped
* to leave a floor for application-level use (FABRIC-0.md §25.5/§25.7,
* Captain Bob's ruling 2026-08-06). Without this, stadium_word_dispatch()
* pulling STADIUM_WORD_HEAT_QUANTUM on every dispatch -- not just the first
* admission of a given word -- exhausts a VM's entire reservoir within
* roughly 32 total dispatches (65536 / 2048), starving any item-4.2-style
* application economy sharing the same VM's reservoir before it gets a
* chance to pull anything. The floor is Q48_ONE / 3, the same "VM-COUNT=3
* fair share" reasoning capsules/hermes/init.4th's COMMON-CH floor already
* uses -- not a new invented number. Application-level pulls
* (stadium_reservoir_pull() called directly, e.g. via STADIUM-RES-PULL) are
* NOT floored -- only word-execution admission respects this ceiling on
* its own consumption.
*/
static uint64_t word_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
/*
* resolve_resident_cell - Self-healing lookup (advisor-flagged reverse
* coherence gap): the map may claim word_id is resident at a cell that was
* actually reclaimed by someone else's stadium_admit() eviction fallback
* since the map was last written. Detected here, lazily, against ground
* truth already public via stadium_cells()/stadium_header_bitmap() --
* no new coupling from stadium.c into this file. A stale mapping is cleared
* and counted as an eviction on discovery.
*/
static size_t resolve_resident_cell(int slot, uint32_t word_id) {
size_t cell = word_slots[slot][word_id].cell_index;
StadiumCell *cells;
if (cell == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
if (cell >= stadium_cell_count() || !cell_is_resident(cell)) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
cells = stadium_cells();
if (cells[cell].header.identity != (uint64_t)word_id) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
return cell;
}
uint64_t stadium_words_resident_heat(VMUuid vm_id) {
int slot;
uint32_t i;
uint64_t sum = 0;
StadiumCell *cells;
if (!words_initialized) return 0;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
cells = stadium_cells();
for (i = 0; i < DICTIONARY_SIZE; i++) {
size_t cell = resolve_resident_cell(slot, i);
if (cell == STADIUM_CELL_NONE) continue;
sum += cells[cell].header.heat;
}
return sum;
}
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell != STADIUM_CELL_NONE) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - word_slots[slot][word_id].last_decay_tick;
if (elapsed > 0) {
/* Redirected Loop #3 (§17.7): a FRACTION of the cell's own
* current heat per elapsed tick -- unit-safe for a conserved
* share of 1.0, unlike execution_heat's flat per-tick amount.
* STADIUM_WORD_COOL_RATE_Q48 / 65536 is that fraction. */
uint64_t per_tick = (h->heat * (uint64_t)STADIUM_WORD_COOL_RATE_Q48) >> 16;
uint64_t cooled = per_tick * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
}
h->heat += word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
return;
}
/* Not resident: Option B starter-grant admission (§17.7). execution_heat
* plays no role -- density is decided entirely by the pulled quantum. */
{
uint64_t pulled = word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
size_t idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)word_id;
candidate.heat = pulled;
candidate.ttl = 0;
candidate.link = 0; /* unused for word patrons; no continuation/consumer yet */
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0; /* unpinned -- words carry no pin exception (§17.3) */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_id, &candidate);
if (idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
return;
}
word_slots[slot][word_id].cell_index = idx;
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell == STADIUM_CELL_NONE) return;
if (stadium_evict(cell) == 0) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
}
}
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions) {
int slot = stadium_quota_slot_for_vm(vm_id);
if (promotions) *promotions = (slot >= 0) ? stat_promotions[slot] : 0;
if (evictions) *evictions = (slot >= 0) ? stat_evictions[slot] : 0;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small utility stadium.c already duplicates locally. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
void stadium_words_print_boot_diagnostics(VMUuid vm_id) {
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum;
uint64_t reservoir;
stadium_words_stats(vm_id, &promotions, &evictions);
/* item 4.2 fix (FABRIC-0.md §25.5): filtered per-VM -- with two VMs
* holding quotas, summing every resident cell regardless of owner
* (the pre-4.2 behavior) mixed both VMs' conservation totals together. */
resident_sum = stadium_resident_sum(vm_id);
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium words: promotions=");
console_put_u64(promotions);
console_puts(" evictions=");
console_put_u64(evictions);
console_println("");
console_puts("Stadium conservation: resident_sum=");
console_put_u64(resident_sum);
console_puts(" reservoir=");
console_put_u64(reservoir);
console_puts(" sum=");
console_put_u64(resident_sum + reservoir);
console_puts(" (Q48_ONE=");
console_put_u64((uint64_t)Q48_ONE);
console_println(")");
console_puts("Stadium conservation: ");
console_println(stadium_conserved(vm_id) ? "CONSERVED" : "DRIFTED");
}
#endif /* __STARKERNEL__ */
+141
View File
@@ -0,0 +1,141 @@
/*
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.c - Cooperative VM context switch glue (FABRIC-3.md §XXVIII,
* Stage 2, 2026-09-13). See switch.h for the contract.
*/
#ifdef __STARKERNEL__
#include "starkernel/vm/switch.h"
#include "vm.h"
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
* the exact synthetic first-entry frame sk_vm_switch_to() expects -- kept
* in assembly, not duplicated here, so the layout can never drift out of
* sync between the two. */
extern void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
/* Single-writer-mainline global, safe under this stage's cooperative-only,
* one-VM-runs-at-a-time-on-one-hart discipline (same class of bookkeeping
* as vm_core.c's g_log_attrib_vm) -- identifies which VM a freshly-primed
* native stack belongs to. Read exactly once, synchronously, at the very
* top of sk_vm_switch_entry() before any other first-entry could occur.
* Safe to keep as a plain global (unlike the old g_switch_back_to this
* file used to also carry -- see native_switch_back_to's own doc comment
* in vm.h for why that one had to move to per-VM state): a VM's identity
* never changes across the lifetime of its own trampoline instance, only
* who most recently switched into it does. */
static VM *g_switch_entry_vm;
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently running, from the switch mechanism's own point of view --
* independent of vm_log_attributed_vm() (vm_core.c), which is scoped to
* nested vm_interpret() calls for log-source attribution and has no way
* to know a VM is "running" while parked inside this file's own raw C
* trampoline (sk_vm_switch_entry()), never itself calling vm_interpret().
* sk_vm_switch_signal_tick() (capsule_vm_switch_signal.c) consults this
* instead of vm_log_attributed_vm() for exactly that reason -- see its
* own updated comment. Single-writer-mainline, same discipline as
* g_switch_entry_vm above: set unconditionally on every switch, by the
* only code path that can ever change who is physically running.
* Seeded once at boot via sk_vm_switch_set_current() for whichever VM is
* running before any switch has ever happened (Hera, at Stage 3
* registration time -- kernel_main.c). */
static VM *g_switch_current_vm;
VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; }
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
/**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
*
* A freshly-entered VM has no production behavior defined yet -- what a
* switched-to VM should actually DO once running is later-stage work
* (message dispatch, an idle loop, whatever Stage 3+ needs). For now this
* is a minimal, permanent placeholder: immediately yield back to whoever
* most recently switched to it, forever. Reads self->native_switch_back_to
* fresh every iteration (not a value captured once) so a later switch-in
* from a different VM is honored, not silently bounced to whoever entered
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
*
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): an attempt to make
* this attributable by calling vm_interpret(self, "MSG-TICK") here was
* tried and reverted -- it introduced a second, distinct hang (a stall in
* sk_repl_headless_wait()'s WIREBIND-attachment wait, not root-caused) in
* place of the switch-storm it was meant to fix. The switch-storm's real
* cause (sk_vm_switch_signal_tick()'s "who is current" check via
* vm_log_attributed_vm() never seeing a VM that only ever runs this raw
* C trampoline) is instead fixed at the source in switch.c/
* capsule_vm_switch_signal.c: sk_vm_context_switch() now tracks "current"
* itself, independent of vm_interpret()'s own attribution bookkeeping.
* This placeholder's behavior is therefore unchanged from Stage 2.
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
for (;;) {
sk_vm_context_switch(self, self->native_switch_back_to);
}
}
int sk_vm_context_switch(VM *from, VM *to) {
uint64_t new_sp;
if (!from || !to || to->native_stack_top == 0) {
return -1;
}
/* Recorded unconditionally (first-ever entry or a resume) so whoever
* is parked at `to`'s native stack -- most often this placeholder
* trampoline -- always knows who most recently switched into it. */
to->native_switch_back_to = from;
if (to->native_stack_saved_sp == 0) {
/* First-ever entry: synthesize the initial frame. */
g_switch_entry_vm = to;
new_sp = sk_vm_switch_prime(to->native_stack_top, (void *)&sk_vm_switch_entry);
} else {
new_sp = to->native_stack_saved_sp;
to->native_stack_saved_sp = 0; /* about to be running, not parked */
}
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
g_switch_current_vm = to;
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
/* Resumes here only once something later switches back to `from` --
* from `from`'s own point of view, this call simply took a while. */
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE);
g_switch_current_vm = from;
return 0;
}
#endif /* __STARKERNEL__ */
+367
View File
@@ -0,0 +1,367 @@
/*
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.
*/
/*
* vm_bootstrap.c - VM initialization and bootstrap logic
*/
#include "../../../v3/include/vm.h"
#include "../../../v3/include/log.h"
#include "../../../v3/include/word_registry.h"
#include "../../../v3/include/vm_debug.h"
#include "../../../v3/include/physics_hotwords_cache.h"
#include "../../../v3/include/rolling_window_of_truth.h"
#include "../../../v3/include/dictionary_heat_optimization.h"
#include "../../../v3/include/ssm_jacquard.h"
#include "word_source/include/vocabulary_words.h"
#include "vm_internal.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/arena.h"
#include "starkernel/console.h"
#endif
#include <string.h>
#include <errno.h>
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
static void vm_bootstrap_print_hex(uint64_t value)
{
char buf[19];
buf[0] = '0';
buf[1] = 'x';
buf[18] = '\0';
for (int i = 0; i < 16; ++i) {
uint8_t nibble = (uint8_t)((value >> ((15 - i) * 4)) & 0xF);
buf[i + 2] = (nibble < 10) ? (char)('0' + nibble) : (char)('a' + nibble - 10);
}
console_puts(buf);
}
#endif
static void vm_bootstrap_scr(VM *vm)
{
vm_align(vm);
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_bootstrap_scr: SCR allot failed");
return;
}
vm->scr_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_store_cell(vm, vm->scr_addr, 0);
}
void vm_bootstrap_root_vocabulary(VM *vm, const char *name)
{
if (!vm || !name) {
return;
}
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
console_println("[VM_BOOTSTRAP] root vocabulary entry");
#endif
#ifdef __STARKERNEL__
sk_vm_arena_assert_guards("vm_bootstrap_root_vocabulary:entry");
#endif
vocabulary_create_vocabulary_direct(vm, name);
size_t len = strlen(name);
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
console_println("[VM_BOOTSTRAP] before vm_find_word");
#endif
DictEntry *entry = vm_find_word(vm, name, len);
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
console_println("[VM_BOOTSTRAP] after vm_find_word");
#endif
if (!entry || !entry->func) {
vm->error = 1;
log_message(LOG_ERROR, "bootstrap: missing vocabulary '%s'", name);
return;
}
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
console_puts("[VOC] ");
console_puts(name);
console_puts(" entry=");
vm_bootstrap_print_hex((uint64_t)(uintptr_t)entry);
console_puts(" xt=");
vm_bootstrap_print_hex((uint64_t)(uintptr_t)entry->func);
console_println("");
const VMHostServices *host = vm_host(vm);
if (host && host->is_executable_ptr &&
!host->is_executable_ptr((const void *)entry->func)) {
console_puts("[VOC] invalid XT for ");
console_puts(name);
console_puts(" func=");
vm_bootstrap_print_hex((uint64_t)(uintptr_t)entry->func);
console_println("");
if (host->panic) {
host->panic("bootstrap vocabulary XT invalid");
}
}
#endif
entry->func(vm);
vocabulary_word_definitions(vm);
#ifdef __STARKERNEL__
sk_vm_arena_assert_guards("vm_bootstrap_root_vocabulary:exit");
#endif
}
void vm_init(VM* vm)
{
vm_init_with_host(vm, NULL);
}
void vm_init_with_host(VM* vm, const VMHostServices *host)
{
if (!vm) return;
*vm = (VM){0};
vm->host = host ? host : vm_default_host_services();
#ifndef __STARKERNEL__
vm_reset_hosted_fake_ns(vm);
#endif
vm->interpreter_enabled = 0;
vm->next_word_id = 0;
vm->recycled_word_id_count = 0;
if (sf_mutex_init(&vm->dict_lock) != 0)
{
log_message(LOG_ERROR, "vm_init: dict_lock init failed");
vm->error = 1;
return;
}
if (sf_mutex_init(&vm->tuning_lock) != 0)
{
log_message(LOG_ERROR, "vm_init: tuning_lock init failed");
sf_mutex_destroy(&vm->dict_lock);
vm->error = 1;
return;
}
vm->memory = (uint8_t*)vm_host_alloc(vm, VM_MEMORY_SIZE, sizeof(void*));
if (!vm->memory)
{
log_message(LOG_ERROR, "vm_init: out of host memory");
vm->error = 1;
return;
}
vm->dsp = -1;
vm->rsp = -1;
vm->here = 0;
vm->exit_colon = 0;
/* Control-flow bookkeeping sentinels (-1/-999 mean "empty"; the
* `*vm = (VM){0}` above already zeroed the arrays themselves) */
vm->cf.cf_sp = -1;
vm->cf.cf_last_mode = -999;
vm->cf.leave_sp = -1;
vm->cf.leave_mark_sp = -1;
vm->cf.endof_sp = -1;
vm->cf.endof_mark_sp = -1;
vm->abort_requested = 0;
log_message(LOG_DEBUG, "vm_init: memory=%p here=%zu", (void*)vm->memory, vm->here);
vm_bootstrap_scr(vm);
/* STATE (0=interpret, -1=compile) */
{
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: STATE allot failed");
return;
}
vm->state_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_store_cell(vm, vm->state_addr, 0);
vm->state_var = 0;
}
/* BASE (default 10) */
{
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: BASE allot failed");
return;
}
vm->base_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_set_base(vm, 10);
}
/* Pictured-number hold buffer (64 bytes, per-VM so child VMs are independent) */
{
void* p = vm_allot(vm, 64);
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: hold buffer allot failed");
return;
}
vm->hold_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm->hold_pos = 0;
memset(p, 0, 64);
}
vm->mode = MODE_INTERPRET;
vm->compiling_word = NULL;
vm->latest = NULL;
vm->error = 0;
vm->halted = 0;
vm->input_length = 0;
vm->input_pos = 0;
vm->current_executing_entry = NULL;
vm_debug_set_current_vm(vm);
vm_debug_install_signal_handlers();
/* Register Forth-79 wordset */
register_forth79_words(vm);
#ifdef __STARKERNEL__
#if SK_PARITY_DEBUG
console_println("[VM_BOOTSTRAP] register_forth79_words complete");
#endif
#endif
/* Interpreter may be enabled only after parity check passes (in sk_vm_bootstrap.c) */
/* Set FORGET fence to post-boot */
vm->dict_fence_latest = vm->latest;
vm->dict_fence_here = vm->here;
/* Initialize hot-words cache */
vm->hotwords_cache = (HotwordsCache*)vm_host_alloc(vm, sizeof(HotwordsCache), sizeof(void*));
if (!vm->hotwords_cache)
{
log_message(LOG_ERROR, "vm_init: hotwords cache malloc failed");
vm->error = 1;
return;
}
hotwords_cache_init(vm->hotwords_cache);
/* Initialize rolling window */
if (rolling_window_init(&vm->rolling_window) != 0)
{
log_message(LOG_ERROR, "vm_init: rolling window malloc failed");
vm->error = 1;
return;
}
/* Initialize VM heartbeat */
vm->heartbeat.tick_count = 0;
vm->heartbeat.last_inference_tick = 0;
vm->heartbeat.check_counter = 0;
vm->heartbeat.heartbeat_enabled = 1;
vm->heartbeat.tick_target_ns = HEARTBEAT_TICK_NS;
vm->heartbeat.snapshot_index = 0;
vm->heartbeat.worker = NULL;
vm->heartbeat_decay_cursor_id = WORD_ID_INVALID;
vm->pipeline_metrics.prefetch_attempts = 0;
vm->pipeline_metrics.prefetch_hits = 0;
vm->pipeline_metrics.window_tuning_checks = 0;
vm->pipeline_metrics.last_checked_window_size = vm->rolling_window.effective_window_size;
vm->pipeline_metrics.last_checked_accuracy = 0.0;
vm->pipeline_metrics.suggested_next_size = vm->rolling_window.effective_window_size;
heartbeat_publish_snapshot(vm);
#if HEARTBEAT_HAS_THREADS
vm->heartbeat.worker = vm_host_calloc(vm, 1, sizeof(HeartbeatWorker));
if (vm->heartbeat.worker)
{
vm->heartbeat.worker->tick_ns = HEARTBEAT_TICK_NS;
if (pthread_create(&vm->heartbeat.worker->thread, NULL, heartbeat_thread_main, vm) != 0)
{
log_message(LOG_WARN, "heartbeat: pthread_create failed (%d), falling back to inline mode", errno);
vm_host_free(vm, vm->heartbeat.worker);
vm->heartbeat.worker = NULL;
}
}
else
{
log_message(LOG_WARN, "heartbeat: worker allocation failed, using inline heartbeat");
}
#endif
vm->decay_slope_q48 = (1ULL << 16) / 3;
vm->last_decay_check_ns = 0;
vm->total_heat_at_last_check = 0;
vm->stale_word_count_at_check = 0;
vm->decay_slope_direction = 0;
vm->lookup_strategy = 0;
vm->last_bucket_reorg_ns = 0;
dict_update_heat_percentiles(vm);
vm->ssm_l8_state = vm_host_alloc(vm, sizeof(ssm_l8_state_t), sizeof(void*));
if (!vm->ssm_l8_state)
{
log_message(LOG_ERROR, "vm_init: SSM L8 state malloc failed");
vm->error = 1;
return;
}
ssm_l8_init((ssm_l8_state_t*)vm->ssm_l8_state, SSM_MODE_C0);
ssm_l8_init_table((ssm_l8_state_t*)vm->ssm_l8_state);
vm->ssm_config = vm_host_alloc(vm, sizeof(ssm_config_t), sizeof(void*));
if (!vm->ssm_config)
{
log_message(LOG_ERROR, "vm_init: SSM config malloc failed");
vm->error = 1;
return;
}
((ssm_config_t*)vm->ssm_config)->L1_heat_tracking = 0;
((ssm_config_t*)vm->ssm_config)->L2_rolling_window = 0;
((ssm_config_t*)vm->ssm_config)->L3_linear_decay = 0;
((ssm_config_t*)vm->ssm_config)->L4_pipelining = 0;
((ssm_config_t*)vm->ssm_config)->L5_window_inference = 0;
((ssm_config_t*)vm->ssm_config)->L6_decay_inference = 0;
((ssm_config_t*)vm->ssm_config)->L7_adaptive_heartrate = 0;
}
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
/*
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.
*/
#ifndef VM_INTERNAL_H
#define VM_INTERNAL_H
#include "../../../v3/include/vm.h"
#include "../../../v3/include/vm_host.h"
#ifndef PARITY_MODE
#define PARITY_MODE 0
#endif
#ifndef SK_PARITY_DEBUG
#define SK_PARITY_DEBUG 0
#endif
/* HISTORICAL: this condition also excluded L4RE_TARGET (`&&
* !defined(L4RE_TARGET)`) -- L4Re had no pthreads. L4Re support has been
* removed as an active target (it was hosted-only anyway; this kernel
* file never actually saw L4RE_TARGET defined). */
#if HEARTBEAT_THREAD_ENABLED
#include <pthread.h>
#define HEARTBEAT_HAS_THREADS 1
#else
#define HEARTBEAT_HAS_THREADS 0
#endif
#define HEARTBEAT_DECAY_BATCH 64u
typedef struct HeartbeatWorker {
#if HEARTBEAT_HAS_THREADS
pthread_t thread;
#endif
uint64_t tick_ns;
int running;
int stop_requested;
} HeartbeatWorker;
const VMHostServices* vm_default_host_services(void);
const VMHostServices* vm_host(const VM *vm);
void* vm_host_alloc(VM *vm, size_t size, size_t align);
void* vm_host_calloc(VM *vm, size_t n, size_t size);
void vm_host_free(VM *vm, void *ptr);
#ifndef __STARKERNEL__
void vm_reset_hosted_fake_ns(VM *vm);
#endif
uint64_t vm_monotonic_ns(VM *vm);
unsigned vm_get_base(const VM* vm);
void vm_set_base(VM* vm, unsigned b);
uint32_t heartbeat_snapshot_index_load(const volatile uint32_t *ptr);
void heartbeat_snapshot_index_store(volatile uint32_t *ptr, uint32_t value);
void heartbeat_publish_snapshot(VM *vm);
void vm_heartbeat_run_cycle(VM *vm);
#if HEARTBEAT_HAS_THREADS
void* heartbeat_thread_main(void *arg);
#endif
#endif /* VM_INTERNAL_H */
+779
View File
@@ -0,0 +1,779 @@
/*
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.
*/
#include "../../../v3/include/vm.h"
#include "../../../v3/include/log.h"
#include "../../../v3/include/rolling_window_of_truth.h"
#include "../../../v3/include/dictionary_heat_optimization.h"
#include "../../../v3/include/inference_engine.h"
#include "../../../v3/include/physics_metadata.h"
#include "../../../v3/include/physics_hotwords_cache.h"
#include "starkernel/doe_log.h"
#include "../../../v3/include/ssm_jacquard.h"
#include "vm_internal.h"
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/timer.h"
#include <errno.h>
#include <string.h>
#include <time.h>
uint32_t heartbeat_snapshot_index_load(const volatile uint32_t *ptr)
{
#if defined(__GNUC__)
return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
#else
return *ptr;
#endif
}
void heartbeat_snapshot_index_store(volatile uint32_t *ptr, uint32_t value)
{
#if defined(__GNUC__)
__atomic_store_n(ptr, value, __ATOMIC_RELEASE);
#else
*ptr = value;
#endif
}
void heartbeat_publish_snapshot(VM *vm)
{
if (!vm)
return;
uint32_t current = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
uint32_t next = current ^ 1u;
HeartbeatSnapshot *snapshot = &vm->heartbeat.snapshots[next];
snapshot->published_tick = vm->heartbeat.tick_count;
snapshot->published_ns = vm_monotonic_ns(vm);
snapshot->window_width = vm->rolling_window.effective_window_size;
snapshot->decay_slope_q48 = vm->decay_slope_q48;
snapshot->hot_word_count = vm->hot_word_count_at_check;
snapshot->stale_word_count = vm->stale_word_count_at_check;
snapshot->total_heat = vm->total_heat_at_last_check;
heartbeat_snapshot_index_store(&vm->heartbeat.snapshot_index, next);
}
/* ====================== VM Heartbeat (Time-Driven Tuning) ======================= */
/**
* @brief Central heartbeat dispatcher for time-driven tuning operations
*
* Aggregates all periodic optimization tasks (Loop #3 and Loop #5) into one place.
* Designed as plugin architecture - new tuning operations can be added as plugins.
*
* Options for integration:
* - Synchronous (now): Called from main execution loop, every N executions
* - Background thread (future): Runs in separate thread, decoupled from VM execution
*
* @param vm Pointer to VM instance
*/
void vm_tick(VM* vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled)
return;
vm->heartbeat.tick_count++;
/* Unified Inference Engine (Phase 2: Replaces Loops #3 & #5)
* Runs every HEARTBEAT_INFERENCE_FREQUENCY ticks to infer optimal:
* - Window width (via variance inflection detection)
* - Decay slope (via exponential regression on heat trajectory)
*/
if ((vm->heartbeat.tick_count - vm->heartbeat.last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY)
{
vm_tick_inference_engine(vm);
}
/* VM Fleet Physics (see capsule_vm_physics.h): fleet-wide state, one
* instance for the whole Tripod, not per-VM. Called from every VM's
* own heartbeat, not just Hera's -- the readiness signal itself must
* reflect the fleet's aggregate activity (VM-PHYSICS-DYNAMIC-FLEET-
* DESIGN-20260705.md's own stated principle for this mechanism), not
* any one VM's personal tick rate. Gating this on Hera's own
* tick_count (the previous implementation) meant it almost never
* fired, since Hera-as-orchestrator mostly blocks on VM-EXEC/VM-CALL
* dispatch that accrues to the *target* VM's tick count, not hers --
* confirmed by instrumentation showing 61 of Hera's own ticks across
* a full 20-run campaign, nowhere near the 1000 needed
* (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev k). */
vm_physics_heartbeat_tick(vm_monotonic_ns(vm));
/* Plugin 2: System State Monitoring (Future) */
/* vm_tick_system_monitor(vm); */
/* Plugin 3: Formal Verification State Update (Future) */
/* vm_tick_formal_state_sync(vm); */
}
/**
* @brief Loop #5: Context-aware window tuning via binary chop search
*
* Uses prefetch accuracy to guide window size adaptation.
* Binary search converges on optimal effective_window_size for current workload.
*
* @param vm Pointer to VM instance
*/
void vm_tick_window_tuner(VM* vm)
{
if (!vm || !vm->rolling_window.is_warm || !ENABLE_PIPELINING)
return;
RollingWindowOfTruth *window = &vm->rolling_window;
PipelineGlobalMetrics *metrics = &vm->pipeline_metrics;
/* Calculate current prefetch accuracy */
if (metrics->prefetch_attempts == 0)
return; /* Not enough data yet */
double current_accuracy = (double)metrics->prefetch_hits / (double)metrics->prefetch_attempts;
/* Binary chop suggests next window size to try */
uint32_t suggested_size = window->effective_window_size; /* Default: no change */
if (metrics->window_tuning_checks == 0)
{
/* First check: try shrinking by 25% */
suggested_size = (window->effective_window_size * 75) / 100;
}
else
{
/* Compare current accuracy to last check */
double accuracy_delta = current_accuracy - metrics->last_checked_accuracy;
if (accuracy_delta > 0.01) /* Improvement threshold: 1% */
{
/* Accuracy improved! Try shrinking more */
uint32_t smaller = (window->effective_window_size * 75) / 100;
suggested_size = (smaller > ADAPTIVE_MIN_WINDOW_SIZE) ? smaller : ADAPTIVE_MIN_WINDOW_SIZE;
}
else if (accuracy_delta < -0.01)
{
/* Accuracy degraded. Try growing instead */
uint32_t larger = (window->effective_window_size * 133) / 100; /* Grow by ~33% */
suggested_size = (larger < ROLLING_WINDOW_SIZE) ? larger : ROLLING_WINDOW_SIZE;
}
/* else: Plateau, stick with current size */
}
/* Apply if different */
if (suggested_size != window->effective_window_size)
{
log_message(LOG_INFO,
"HEARTBEAT[window]: %u → %u (accuracy %.2f%%, %lu/%lu prefetch hits)",
window->effective_window_size,
suggested_size,
current_accuracy * 100.0,
metrics->prefetch_hits,
metrics->prefetch_attempts);
window->effective_window_size = suggested_size;
}
/* Record for next iteration */
metrics->last_checked_window_size = window->effective_window_size;
metrics->last_checked_accuracy = current_accuracy;
metrics->window_tuning_checks++;
}
/**
* @brief Loop #3: Heat decay slope validation via periodic measurement
*
* Validates that linear decay is actually helping optimize dictionary caching.
* Measures stale word ratio, hot word count, and average heat distribution.
*
* @param vm Pointer to VM instance
*/
void vm_tick_slope_validator(VM* vm)
{
if (!vm)
return;
/* Collect snapshot of current state */
uint64_t hot_word_count = 0;
uint64_t stale_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
/* Scan dictionary and categorize words by heat level */
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
{
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
hot_word_count++;
else if (e->execution_heat > 0 && e->execution_heat < 10)
stale_word_count++;
total_heat += e->execution_heat;
word_count++;
}
sf_mutex_unlock(&vm->dict_lock);
double avg_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0;
double stale_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0;
/* === LOOP #3: INFERENCE ENGINE ===
* Compare current measurements to baseline from last check
* Decide whether decay is too fast, too slow, or optimal
*/
int new_slope_direction = 0; /* -1: decrease slope, 0: stable, +1: increase slope */
sf_mutex_lock(&vm->tuning_lock);
if (vm->word_count_at_check > 0)
{
/* Calculate trend in stale words: absolute count delta indicates accumulation/clearing */
int64_t stale_delta = (int64_t)stale_word_count - (int64_t)vm->stale_word_count_at_check;
/* INFERENCE: If stale words INCREASING, decay is too slow → increase slope */
/* If stale words DECREASING, decay is working (or too fast) → monitor */
if (stale_delta > 5) /* Threshold: 5+ additional stale words signals problem */
{
/* Stale words accumulating: decay is insufficient */
new_slope_direction = 1;
log_message(LOG_INFO,
"HEARTBEAT[slope]: stale_delta=%ld, decay TOO SLOW, increase slope",
(long)stale_delta);
}
else if (stale_delta < -5) /* Threshold: 5+ fewer stale words signals clearing */
{
/* Stale words clearing: decay is aggressive (potentially too fast) */
/* Only decrease slope if avg_heat is dropping below target */
if (avg_heat < 5.0)
{
new_slope_direction = -1;
log_message(LOG_INFO,
"HEARTBEAT[slope]: stale_delta=%ld, avg_heat=%.1f, decay TOO FAST, decrease slope",
(long)stale_delta, avg_heat);
}
else
{
log_message(LOG_INFO,
"HEARTBEAT[slope]: stale_delta=%ld, decay working, hold slope",
(long)stale_delta);
}
}
else
{
log_message(LOG_INFO,
"HEARTBEAT[slope]: stale_delta=%ld (stable), hold slope",
(long)stale_delta);
}
}
else
{
log_message(LOG_INFO,
"HEARTBEAT[slope]: baseline measurement - hot_words=%lu, stale_ratio=%.2f%%, avg_heat=%.1f",
hot_word_count,
stale_ratio * 100.0,
avg_heat);
}
/* === APPLY SLOPE ADJUSTMENT ===
* Only adjust if direction changed (hysteresis to prevent oscillation)
*/
if (new_slope_direction != vm->decay_slope_direction && new_slope_direction != 0)
{
vm->decay_slope_direction = new_slope_direction;
/* Calculate adjustment in Q48.16: 5% change per cycle */
uint64_t adjustment = (vm->decay_slope_q48 * 5) / 100;
if (adjustment < 1) adjustment = 1; /* Minimum increment */
uint64_t old_slope = vm->decay_slope_q48;
if (new_slope_direction > 0)
{
vm->decay_slope_q48 += adjustment;
}
else if (new_slope_direction < 0)
{
vm->decay_slope_q48 = (vm->decay_slope_q48 > adjustment)
? (vm->decay_slope_q48 - adjustment)
: 1; /* Floor at 1 */
}
/* Log the adjustment as human-readable double */
double old_slope_dbl = (double)old_slope / 65536.0;
double new_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
log_message(LOG_INFO,
"HEARTBEAT[slope]: ADJUSTED slope from %.3f to %.3f (direction=%d)",
old_slope_dbl,
new_slope_dbl,
new_slope_direction);
}
/* Store baseline for next comparison */
vm->hot_word_count_at_check = hot_word_count;
vm->total_heat_at_last_check = total_heat;
vm->stale_word_count_at_check = stale_word_count;
vm->word_count_at_check = word_count;
sf_mutex_unlock(&vm->tuning_lock);
}
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns)
{
if (!vm)
return;
sf_mutex_lock(&vm->dict_lock);
DictEntry *cursor = NULL;
if (vm->heartbeat_decay_cursor_id != WORD_ID_INVALID)
cursor = vm_dictionary_lookup_by_word_id(vm, vm->heartbeat_decay_cursor_id);
if (!cursor)
cursor = vm->latest;
size_t processed = 0;
while (cursor && processed < HEARTBEAT_DECAY_BATCH)
{
/* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay().
* now_ns is kept only to refresh last_decay_ns for diagnostics. */
uint64_t elapsed_ticks = vm->heartbeat.tick_count - cursor->physics.last_decay_tick;
physics_metadata_apply_linear_decay(cursor, elapsed_ticks, vm);
cursor->physics.last_decay_tick = vm->heartbeat.tick_count;
cursor->physics.last_decay_ns = now_ns;
cursor = cursor->link;
processed++;
}
vm->heartbeat_decay_cursor_id = (cursor && cursor->word_id != WORD_ID_INVALID)
? cursor->word_id
: WORD_ID_INVALID;
sf_mutex_unlock(&vm->dict_lock);
}
/**
* @brief L8 FINAL INTEGRATION: Jacquard mode selector heartbeat update
*
* Collects metrics from L1-L7 physics layers and feeds them to the L8 Jacquard
* mode selector. L8 then chooses the optimal configuration mode based on workload
* characteristics and applies it to the runtime.
*
* This is the SOLE policy engine - all adaptive decisions flow through L8.
*
* @param vm Pointer to VM instance
*/
static void vm_heartbeat_update_l8(VM *vm)
{
if (!vm || !vm->ssm_l8_state)
return;
ssm_l8_state_t *l8 = (ssm_l8_state_t*)vm->ssm_l8_state;
/* === Collect L1-L7 Metrics and Convert to L8 Format === */
ssm_l8_metrics_t metrics = {0};
/* L2: Rolling window entropy (normalized diversity) */
/* Use unique word count / total executions as entropy proxy */
uint32_t unique_words = (vm->rolling_window.total_executions > 0) ?
(uint32_t)(vm->rolling_window.effective_window_size) : 0;
metrics.entropy = (double)unique_words / (double)(ROLLING_WINDOW_SIZE > 0 ? ROLLING_WINDOW_SIZE : 1);
/* L4: Pipelining metrics → CV (coefficient of variation) */
if (vm->pipeline_metrics.prefetch_attempts > 0) {
double accuracy = (double)vm->pipeline_metrics.prefetch_hits / (double)vm->pipeline_metrics.prefetch_attempts;
metrics.cv = 1.0 - accuracy; /* Invert: high accuracy = low CV (stability) */
} else {
metrics.cv = 0.5; /* Default moderate CV if no data */
}
/* L3: Decay slope → temporal locality signal (Q48.16 → double) */
double decay_slope = (double)vm->decay_slope_q48 / 65536.0;
metrics.temporal_decay = (decay_slope > 0.0) ? (1.0 / decay_slope) : 0.0; /* Higher slope = stronger temporal locality */
if (metrics.temporal_decay > 1.0) metrics.temporal_decay = 1.0; /* Clamp to [0,1] */
/* L5/L6: Inference stability for hysteresis + table ANOVA tracking */
{
int early_exited;
sf_mutex_lock(&vm->tuning_lock);
early_exited = (vm->last_inference_outputs && vm->last_inference_outputs->early_exited);
sf_mutex_unlock(&vm->tuning_lock);
metrics.stability_score = early_exited ? 0.9 : 0.1;
metrics.inference_early_exited = early_exited;
metrics.inference_ran_this_tick =
(vm->heartbeat.tick_count == vm->heartbeat.last_inference_tick) ? 1 : 0;
}
if (l8->table) {
/* === Adaptive table-based selection === */
uint8_t old_config = l8->table->current_config;
CDTuning cd_tuning = cd_tuning_word();
uint32_t recent_ids[CD_MAX_CLASSIFY_DEPTH];
uint32_t recent_count = rolling_window_get_recent_sequence(
&vm->rolling_window, cd_tuning.id_window_depth, recent_ids);
ssm_l8_update_table(l8, &metrics,
(ssm_config_t*)vm->ssm_config,
vm->rolling_window.effective_window_size,
recent_ids, recent_count);
if (l8->table->current_config != old_config) {
ssm_config_t *cfg = (ssm_config_t*)vm->ssm_config;
log_message(LOG_INFO,
"L8[TABLE]: Config %u → %u regime=%u score=%u "
"L1=%d L2=%d L3=%d L4=%d L5=%d L6=%d L7=%d",
(unsigned)old_config,
(unsigned)l8->table->current_config,
(unsigned)l8->table->current_regime,
(unsigned)l8->table->regime_scores
[l8->table->current_regime * l8->table->num_configs
+ l8->table->current_config],
cfg->L1_heat_tracking, cfg->L2_rolling_window,
cfg->L3_linear_decay, cfg->L4_pipelining,
cfg->L5_window_inference, cfg->L6_decay_inference,
cfg->L7_adaptive_heartrate);
}
} else {
/* === Legacy threshold classifier === */
ssm_l8_mode_t old_mode = l8->current_mode;
ssm_l8_update(&metrics, l8);
if (l8->current_mode != old_mode) {
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
ssm_apply_mode(l8, config);
log_message(LOG_INFO,
"L8[JACQUARD]: Mode %s → %s (entropy=%.2f, cv=%.2f, temporal=%.2f)",
ssm_l8_mode_name(old_mode),
ssm_l8_mode_name(l8->current_mode),
metrics.entropy,
metrics.cv,
metrics.temporal_decay);
}
}
/* L1's cache->enabled is the runtime toggle physics_pre_execute's
* hotwords-cache checks actually read (see physics_execution_hooks.c);
* sync it here rather than re-deriving ssm_config on the hot dispatch
* path every word execution. Idempotent -- cheap even when unchanged. */
if (vm->hotwords_cache && vm->ssm_config) {
hotwords_cache_set_enabled(vm->hotwords_cache,
((ssm_config_t*)vm->ssm_config)->L1_heat_tracking);
}
}
void vm_heartbeat_run_cycle(VM *vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled)
return;
/* L8 FINAL INTEGRATION: Core heartbeat operations */
vm_tick(vm);
vm_tick_apply_background_decay(vm, vm_monotonic_ns(vm));
rolling_window_service(&vm->rolling_window);
dict_adaptive_optimization_pass(vm); /* Adaptive dictionary optimization */
/* L8 FINAL INTEGRATION: Jacquard mode selector - the sole policy engine */
vm_heartbeat_update_l8(vm);
heartbeat_publish_snapshot(vm);
/* Phase 2: Real-time heartbeat metrics emission */
HeartbeatTickSnapshot tick_snapshot;
heartbeat_capture_tick_snapshot(vm, &tick_snapshot);
#if defined(HEARTBEAT_CSV_ENABLED) && HEARTBEAT_CSV_ENABLED
heartbeat_emit_tick_row(vm, &tick_snapshot);
#endif
/* Always compiled in; g_doe_log_enabled (default 1) is the runtime
* gate now, toggled live by HB-ON/HB-OFF -- see doe_log.h. */
doe_log_tick_row(vm, &tick_snapshot);
}
#if HEARTBEAT_THREAD_ENABLED
void* heartbeat_thread_main(void *arg)
{
VM *vm = (VM*)arg;
if (!vm || !vm->heartbeat.worker)
return NULL;
HeartbeatWorker *worker = vm->heartbeat.worker;
worker->running = 1;
/* IMPORTANT: No startup delay to allow heartbeat to emit during short DoE runs.
* Original 50ms delay avoided race conditions during word registration, but prevented
* real-time metrics emission in fast-completing tests. */
while (!worker->stop_requested)
{
vm_heartbeat_run_cycle(vm);
uint64_t tick_ns = worker->tick_ns ? worker->tick_ns : HEARTBEAT_TICK_NS;
struct timespec req;
req.tv_sec = (time_t)(tick_ns / 1000000000ULL);
req.tv_nsec = (long)(tick_ns % 1000000000ULL);
while (!worker->stop_requested && nanosleep(&req, &req) == -1 && errno == EINTR)
{
/* Retry with remaining time */
}
}
worker->running = 0;
return NULL;
}
#endif
void vm_snapshot_read(const VM* vm, HeartbeatSnapshot* out_snapshot)
{
if (!vm || !out_snapshot)
return;
uint32_t index = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
*out_snapshot = vm->heartbeat.snapshots[index];
}
/**
* @brief Phase 2: Unified Inference Engine - Adaptive Window & Decay Slope Tuning
*
* Coordinates inference on rolling window of truth to determine:
* - Optimal adaptive window width (via variance inflection detection)
* - Optimal decay slope (via exponential regression on heat trajectory)
*
* Uses ANOVA early-exit to skip full inference when variance is stable (<5% change).
* All math uses Q48.16 fixed-point (integer-only, no floating-point).
*
* Replaces legacy vm_tick_window_tuner() and vm_tick_slope_validator().
*
* @param vm Pointer to VM instance
*/
void vm_tick_inference_engine(VM* vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled || !vm->rolling_window.is_warm)
return;
/* DoE counter: inference engine invocations */
vm->heartbeat.inference_run_count++;
rolling_window_service(&vm->rolling_window);
/* Allocate InferenceOutputs if needed - protect with tuning_lock against race with doe_metrics */
sf_mutex_lock(&vm->tuning_lock);
if (!vm->last_inference_outputs)
{
vm->last_inference_outputs = vm_host_alloc(vm, sizeof(InferenceOutputs), sizeof(void*));
if (!vm->last_inference_outputs)
{
sf_mutex_unlock(&vm->tuning_lock);
log_message(LOG_ERROR, "INFERENCE: Failed to allocate InferenceOutputs");
return;
}
memset(vm->last_inference_outputs, 0, sizeof(InferenceOutputs));
}
sf_mutex_unlock(&vm->tuning_lock);
/* === Collect Current Dictionary Metrics === */
uint64_t hot_word_count = 0;
uint64_t stale_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
{
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
hot_word_count++;
else if (e->execution_heat > 0 && e->execution_heat < 10)
stale_word_count++;
total_heat += e->execution_heat;
word_count++;
}
sf_mutex_unlock(&vm->dict_lock);
/* === Populate InferenceInputs === */
InferenceInputs inference_inputs = {
.window = &vm->rolling_window,
.vm = vm, /* Required for dictionary lookups in extract_heat_trajectory */
.trajectory_length = (vm->rolling_window.window_pos > 0)
? vm->rolling_window.window_pos
: vm->rolling_window.total_executions,
.prefetch_hits = vm->pipeline_metrics.prefetch_hits,
.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts,
.hot_word_count = hot_word_count,
.stale_word_count = stale_word_count,
.total_heat = total_heat,
.word_count = word_count,
.last_total_heat = vm->total_heat_at_last_check,
.last_stale_count = vm->stale_word_count_at_check
};
/* === Run Unified Inference Engine === */
inference_engine_run(&inference_inputs, vm->last_inference_outputs);
/* === Apply Inferred Tuning Parameters === */
if (!vm->last_inference_outputs->early_exited)
{
/* Full inference was executed (not cached by ANOVA early-exit) */
/* 1. Apply adaptive window width */
if (vm->last_inference_outputs->adaptive_window_width > 0 &&
vm->last_inference_outputs->adaptive_window_width != vm->rolling_window.effective_window_size)
{
log_message(LOG_INFO,
"INFERENCE[window]: %u → %u (variance=%.6f Q48.16)",
vm->rolling_window.effective_window_size,
vm->last_inference_outputs->adaptive_window_width,
(double)vm->last_inference_outputs->window_variance_q48 / 65536.0);
vm->rolling_window.effective_window_size = vm->last_inference_outputs->adaptive_window_width;
}
/* 2. Apply adaptive decay slope */
sf_mutex_lock(&vm->tuning_lock);
if (vm->last_inference_outputs->adaptive_decay_slope > 0 &&
vm->last_inference_outputs->adaptive_decay_slope != vm->decay_slope_q48)
{
double old_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
double new_slope_dbl = (double)vm->last_inference_outputs->adaptive_decay_slope / 65536.0;
log_message(LOG_INFO,
"INFERENCE[slope]: %.3f → %.3f (fit_quality=%.6f Q48.16)",
old_slope_dbl,
new_slope_dbl,
(double)vm->last_inference_outputs->slope_fit_quality_q48 / 65536.0);
vm->decay_slope_q48 = vm->last_inference_outputs->adaptive_decay_slope;
}
sf_mutex_unlock(&vm->tuning_lock);
/* 3. Validate outputs */
if (!inference_outputs_validate(vm->last_inference_outputs))
{
/* 2026-09-22: downgraded from LOG_WARN -- fires routinely
* during normal operation and the runtime self-recovers
* every time (the whole point of "ignoring results"), so it
* does not warrant default-visible attention. Still
* available with --log-level=info/debug. */
log_message(LOG_INFO,
"INFERENCE: Output validation failed, ignoring results");
}
vm->heartbeat.last_inference_tick = vm->heartbeat.tick_count;
}
else
{
/* ANOVA early-exit: variance stable, using cached outputs */
vm->heartbeat.early_exit_count++;
log_message(LOG_DEBUG,
"INFERENCE: Early-exit (variance stable <5%%), using cached outputs");
}
/* Store baseline for next inference comparison */
sf_mutex_lock(&vm->tuning_lock);
vm->total_heat_at_last_check = total_heat;
vm->stale_word_count_at_check = stale_word_count;
vm->word_count_at_check = word_count;
sf_mutex_unlock(&vm->tuning_lock);
/* L8 FINAL INTEGRATION: Loop always-on */ /* === Loop #7: Adaptive Heartrate ===
* Adjust tick frequency based on system stability:
* - Variance stable (early_exited) → increase tick interval (less frequent)
* - Variance volatile (full inference) → decrease tick interval (more frequent)
*
* Bounds: [HEARTBEAT_TICK_NS / 4, HEARTBEAT_TICK_NS * 4]
*/
{
uint64_t current_tick_ns = vm->heartbeat.tick_target_ns;
uint64_t min_tick_ns = HEARTBEAT_TICK_NS / 4; /* 4x faster minimum */
uint64_t max_tick_ns = HEARTBEAT_TICK_NS * 4; /* 4x slower maximum */
if (vm->last_inference_outputs && vm->last_inference_outputs->early_exited)
{
/* System stable: slow down heartbeat by 25% */
uint64_t new_tick_ns = (current_tick_ns * 125) / 100;
if (new_tick_ns > max_tick_ns) new_tick_ns = max_tick_ns;
if (new_tick_ns != current_tick_ns)
{
vm->heartbeat.tick_target_ns = new_tick_ns;
if (vm->heartbeat.worker)
vm->heartbeat.worker->tick_ns = new_tick_ns;
log_message(LOG_DEBUG,
"HEARTBEAT[rate]: stable → slower tick %lu → %lu ns",
(unsigned long)current_tick_ns,
(unsigned long)new_tick_ns);
}
}
else
{
/* System volatile: speed up heartbeat by 25% */
uint64_t new_tick_ns = (current_tick_ns * 80) / 100;
if (new_tick_ns < min_tick_ns) new_tick_ns = min_tick_ns;
if (new_tick_ns != current_tick_ns)
{
vm->heartbeat.tick_target_ns = new_tick_ns;
if (vm->heartbeat.worker)
vm->heartbeat.worker->tick_ns = new_tick_ns;
log_message(LOG_DEBUG,
"HEARTBEAT[rate]: volatile → faster tick %lu → %lu ns",
(unsigned long)current_tick_ns,
(unsigned long)new_tick_ns);
}
}
/* Punch-list item 0.8 / FABRIC-0.md §26: drive the physical re-arm
* period from this same execution-derived signal, ratio-preserving
* rescale onto the kernel's 10 ms base rather than the hosted
* HEARTBEAT_TICK_NS (10 µs) base -- see §26.3 for why the literal
* value cannot be used on bare-metal hardware. heartbeat.c clamps
* defensively on the way in, so no clamping is duplicated here.
*
* FABRIC-1.md, Category B "Multi-VM heartbeat ownership", ruled:
* there is exactly one physical timer, so exactly one VM may write
* its period -- never a race between whichever VM's vm_tick() last
* ran. Hera is the fixed point everywhere else in this design
* (patron zero, sole capacity arbiter, sole birther/killer of
* VMs), so she is the sole owner here too. Every other VM's Loop #7
* still adapts vm->heartbeat.tick_target_ns exactly as before --
* that's correct per-VM bookkeeping -- it simply never reaches the
* shared physical re-arm period. */
if (vm_uuid_is_hera(vm->stadium_vm_id))
{
const uint64_t kernel_base_ns = 10000000ULL; /* 10 ms, matches heartbeat.c's HEARTBEAT_BASE_PERIOD_NS */
uint64_t kernel_period_ns =
(vm->heartbeat.tick_target_ns * kernel_base_ns) / HEARTBEAT_TICK_NS;
heartbeat_set_adaptive_period_ns(kernel_period_ns);
}
}
/* End always-on loop */}