Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization: - Move StarForth v3 engine to v3/ (src/, include/, Makefile) - Move kernel to kernel/ (src/, include/, linker/, Makefile) - Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md) - Move FABRIC-0..4.md to docs/fabric/ - Move ONTOLOGY.md and ROADMAP.md to docs/ Board infrastructure: - Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/ - Each board has board.mk (ISA, CPU flags, boot recipe) and README.md - Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET - make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board - ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet) - scripts/mkdiskimage.sh builds disk images for all boards Docs pipeline: - docs/book/ with LaTeX master (main.tex) and Makefile - pandoc converts Markdown to LaTeX at build time - Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks) - make docs builds single PDF (754 pages, 0 missing characters) - make docs TARGET=<board> adds board appendix - build/docs/<book|board>/meta.tex stamps git commit into PDF Bug fixes: - 42 include paths that only worked by accident now use correct relative paths - clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build) - Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved - Doxyfile, .clang-tidy, README.md, Kconfig paths updated Verified: - Hosted v3 build passes 1012 tests, 0 failures - SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE) - Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes - make clean TARGET=<board> removes only that board and its ISA objects - make all builds all boards, hosted v3, and docs in one run Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,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`.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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.
|
||||
@@ -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__ */
|
||||
@@ -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
@@ -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__ */
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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__ */
|
||||
@@ -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__ */
|
||||
@@ -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__ */
|
||||
@@ -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__ */
|
||||
@@ -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
@@ -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 */
|
||||
@@ -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 */}
|
||||
Reference in New Issue
Block a user