Files
LithosAnanke/src/starkernel/vm/vm_core.c
T
Robert Allan JamesandClaude Sonnet 5 2b1ba031a5 Drain at the outermost checkpoint -- FABRIC-3.6.md task 3.4
sk_hermes_drain_checkpoint() interprets one queued payload per checkpoint
(ruled: one message per checkpoint), reusing sk_vm_at_outermost_interpret()
and placed before the switch-signal block in vm_core.c's existing
cooperative checkpoint (sk_vm_context_switch() doesn't return until
switched back to, so drain must come first or it silently never runs on
a switching checkpoint).

Amends FABRIC-3.5.md SXLIII.5, caught by advisor() before writing the
naive version: "recursive drain is prevented for free" via
g_vm_interpret_depth is true but only for same-message re-drain -- it
doesn't cover the separate same-VM reentrancy hazard FABRIC-3.md SXX
already named for Hera specifically (VMCallState saves rsp/exit_colon/
ecw_nesting only, never input_buffer/input_length/input_pos). Draining
calls vm_interpret() on the same vm whose own vm_interpret() call is
still paused mid-word at the checkpoint; without saving and restoring
the cursor by hand, the enclosing REPL line or LOAD block would be
silently truncated. sk_hermes_drain_checkpoint() snapshots and restores
input_buffer/input_length/input_pos/mode/error/abort_requested around
the call. Not a divergence from the ruling -- cursor preservation is the
implementer's own obligation inside the ruled mechanism.

Gated behind a system-wide pending-total counter so the common
no-message-in-flight case costs one integer read per word dispatch, not
a stadium_max_vm_count()-sized queue scan (also flagged by advisor() as
a real hot-path cost, not deferred).

Verified live on all three architectures: a self-test publishes a real
payload to Hermes, proves the depth gate via VM-EXEC-ing an existing
harmless colon word into Hermes (genuine nested vm_interpret(), depth 2,
must not drain), then drains directly from genuinely-outermost context
and confirms exactly one clean drain. dict_hash unmoved and identical
across architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:27:11 -04:00

1333 lines
47 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 "../include/vm.h"
#include "../include/inference_engine.h"
#include "../include/log.h"
#include "../include/word_registry.h"
#include "../include/vm_debug.h"
#include "../include/profiler.h"
#include "../include/platform_time.h"
#include "../include/physics_metadata.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/physics_pipelining_metrics.h"
#include "../include/rolling_window_of_truth.h"
#include "../include/dictionary_heat_optimization.h"
#include "../include/ssm_jacquard.h"
#include "../include/vm_host.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/arena.h"
#include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */
#include "starkernel/console.h" /* g_sk_fault_word */
#include "platform_alloc.h" /* sf_free for call_stack */
#include "starkernel/vm/stadium_words.h" /* item 4.1: word patrons on the Stadium */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
#include "starkernel/vm/switch.h" /* FABRIC-3.md §XXVIII Stage 2 -- sk_vm_context_switch() */
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.4 -- sk_hermes_drain_checkpoint() */
#include "starkernel/capsule_birth.h" /* capsule_vm_registry_get() */
/* FABRIC-3.md §XXVIII Stage 3 correction (2026-09-13): forward-declared
* here because execute_colon_word() (uses it) appears earlier in this file
* than g_vm_interpret_depth's definition (kept next to g_log_attrib_vm,
* near vm_interpret() -- see that doc comment for the full rationale). */
int sk_vm_at_outermost_interpret(void);
#endif
#include "word_source/include/vocabulary_words.h"
#include "vm_internal.h"
#ifndef __STARKERNEL__
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <stdio.h>
#endif
#include <string.h>
#include <stdint.h>
/* ====================== Host services helpers ======================= */
#ifndef __STARKERNEL__
static uint64_t hosted_fake_ns = 0;
static void* hosted_alloc(size_t size, size_t align)
{
(void)align;
return malloc(size);
}
static void hosted_free(void *ptr)
{
free(ptr);
}
static uint64_t hosted_time_ns(void)
{
#if PARITY_MODE
hosted_fake_ns += 1000; /* Deterministic 1us increments */
return hosted_fake_ns;
#else
return sf_monotonic_ns();
#endif
}
static int hosted_mutex_init(void *mutex) { return sf_mutex_init((sf_mutex_t *)mutex); }
static int hosted_mutex_lock(void *mutex) { sf_mutex_lock((sf_mutex_t *)mutex); return 0; }
static int hosted_mutex_unlock(void *mutex) { sf_mutex_unlock((sf_mutex_t *)mutex); return 0; }
static void hosted_mutex_destroy(void *mutex) { sf_mutex_destroy((sf_mutex_t *)mutex); }
static int hosted_puts(const char *str)
{
if (!str) return -1;
if (fputs(str, stdout) < 0) return -1;
return (int)strlen(str);
}
static int hosted_putc(int c)
{
return (fputc(c, stdout) == EOF) ? -1 : 1;
}
static void hosted_panic(const char *message)
{
if (message) {
fprintf(stderr, "[StarForth panic] %s\n", message);
} else {
fprintf(stderr, "[StarForth panic] unknown\n");
}
fflush(stderr);
abort();
}
static bool hosted_xt_is_executable(const void *ptr)
{
(void)ptr;
return true;
}
static bool hosted_xt_entry_owned(const void *ptr, size_t bytes)
{
(void)ptr;
(void)bytes;
return true;
}
static const VMHostServices hosted_services = {
.alloc = hosted_alloc,
.free = hosted_free,
.monotonic_ns = hosted_time_ns,
.mutex_init = hosted_mutex_init,
.mutex_lock = hosted_mutex_lock,
.mutex_unlock = hosted_mutex_unlock,
.mutex_destroy = hosted_mutex_destroy,
.puts = hosted_puts,
.putc = hosted_putc,
.is_executable_ptr = hosted_xt_is_executable,
.owns_xt_entry = hosted_xt_entry_owned,
.panic = hosted_panic,
.parity_mode = PARITY_MODE,
.verbose = 0
};
#ifndef __STARKERNEL__
void vm_reset_hosted_fake_ns(VM *vm)
{
if (vm && vm->host == &hosted_services && PARITY_MODE) {
hosted_fake_ns = 0;
}
}
#endif
#endif /* !__STARKERNEL__ */
static void vm_assert_interpreter_enabled(VM *vm, const char *caller)
{
if (vm && vm->interpreter_enabled) {
return;
}
if (vm) {
vm->error = 1;
}
log_message(LOG_ERROR, "[vm] interpreter disabled (caller=%s)", caller ? caller : "unknown");
const VMHostServices *host = vm_host(vm);
if (host && host->panic) {
host->panic("interpreter invoked before bootstrap completion");
}
}
void vm_enable_interpreter(VM *vm)
{
if (vm) {
vm->interpreter_enabled = 1;
}
}
const VMHostServices* vm_default_host_services(void)
{
#ifdef __STARKERNEL__
return sk_host_services();
#else
return &hosted_services;
#endif
}
const VMHostServices* vm_host(const VM *vm)
{
const VMHostServices *host = vm ? vm->host : NULL;
return host ? host : vm_default_host_services();
}
void* vm_host_alloc(VM *vm, size_t size, size_t align)
{
const VMHostServices *host = vm_host(vm);
if (!host || !host->alloc || size == 0) {
return NULL;
}
if (align == 0) {
align = sizeof(void*);
}
return host->alloc(size, align);
}
void* vm_host_calloc(VM *vm, size_t n, size_t size)
{
size_t total = n * size;
void *p = vm_host_alloc(vm, total, sizeof(void*));
if (p) {
memset(p, 0, total);
}
return p;
}
void vm_host_free(VM *vm, void *ptr)
{
const VMHostServices *host = vm_host(vm);
if (ptr && host && host->free) {
host->free(ptr);
}
}
uint64_t vm_monotonic_ns(VM *vm)
{
const VMHostServices *host = vm_host(vm);
if (host && host->monotonic_ns) {
return host->monotonic_ns();
}
return 0;
}
unsigned vm_get_base(const VM* vm)
{
if (!vm) return 10u;
/* Prefer VM cell if valid */
vaddr_t a = vm->base_addr;
if ((a % sizeof(cell_t)) == 0 && (size_t)a + sizeof(cell_t) <= VM_MEMORY_SIZE)
{
cell_t v = vm_load_cell((VM*)vm, a); /* cast-away const for accessor */
if (v >= 2 && v <= 36) return (unsigned)v;
}
/* Fallback to host mirror */
if (vm->base >= 2 && vm->base <= 36) return (unsigned)vm->base;
return 10u;
}
void vm_set_base(VM* vm, unsigned b)
{
if (!vm) return;
if (b < 2 || b > 36) b = 10;
vm_store_cell(vm, vm->base_addr, (cell_t)b);
vm->base = (cell_t)b; /* host mirror */
}
/* Kernel-side copy of src/vm.c's vm_zuse_cert_install() -- the kernel
* build excludes src/vm.c entirely (VM_EXCLUDE in Makefile.starkernel;
* see vm_set_base() above for the same duplication pattern already
* established), so any src/vm.c function the kernel needs to call gets
* its own copy here. See src/vm.c's version for the full rationale
* (why this is a VM struct field with no FORTH word, not a CONSTANT). */
int vm_zuse_cert_install(VM* vm, const uint8_t seed[32], const uint8_t pubkey[32])
{
if (!vm) return -1;
if (vm->zuse_cert_installed) return -1;
memcpy(vm->zuse_cert_seed, seed, 32);
memcpy(vm->zuse_cert_pubkey, pubkey, 32);
vm->zuse_cert_installed = 1;
return 0;
}
/* ====================== VM init / teardown ======================= */
void vm_cleanup(VM* vm)
{
if (!vm) return;
#if HEARTBEAT_HAS_THREADS
if (vm->heartbeat.worker)
{
vm->heartbeat.worker->stop_requested = 1;
pthread_join(vm->heartbeat.worker->thread, NULL);
vm_host_free(vm, vm->heartbeat.worker);
vm->heartbeat.worker = NULL;
}
#endif
/* Clean up hot-words cache */
if (vm->hotwords_cache)
{
hotwords_cache_cleanup(vm->hotwords_cache);
vm_host_free(vm, vm->hotwords_cache);
vm->hotwords_cache = NULL;
}
/* Clean up rolling window of truth */
rolling_window_cleanup(&vm->rolling_window);
/* Clean up SSM L8 state */
if (vm->ssm_l8_state)
{
ssm_l8_free_table((ssm_l8_state_t*)vm->ssm_l8_state);
vm_host_free(vm, vm->ssm_l8_state);
vm->ssm_l8_state = NULL;
}
if (vm->ssm_config)
{
vm_host_free(vm, vm->ssm_config);
vm->ssm_config = NULL;
}
/* FABRIC-3.md §VII follow-on, 2026-09-06: every DictEntry is its own
* sf_malloc() (vm_create_word(), dictionary_management.c) -- separate
* from vm->memory entirely, so freeing that arena below never touched
* them. Neither did anything else in this function, or anywhere else
* in the codebase (confirmed by grep before writing this fix) -- every
* word a VM ever defined leaked permanently on kill. Never noticed
* before: the hosted binary normally only calls this once at process
* exit (the OS reclaims everything anyway), and kernel VMs were
* normally born once and kept alive for a whole boot, not repeatedly
* born and killed -- confirmed live as a real PANIC ("malloc failed"
* cascading into "Stadium: eviction... governor invariant broken",
* full halt) during this session's own repeated identity-verification
* workflow (8 consecutive WIREBIND birth/kill cycles in one boot).
* transition_metrics (vm_create_word()'s own second, per-entry
* sf_malloc()) must be freed too, before the entry itself -- freeing
* entry first would leave no way to reach it. */
{
DictEntry *dict_entry = vm->latest;
while (dict_entry) {
DictEntry *next_entry = dict_entry->link;
if (dict_entry->transition_metrics) {
sf_free(dict_entry->transition_metrics);
}
sf_free(dict_entry);
dict_entry = next_entry;
}
vm->latest = NULL;
}
if (vm->memory)
{
vm_host_free(vm, vm->memory);
vm->memory = NULL;
}
vm->here = 0;
sf_mutex_destroy(&vm->tuning_lock);
sf_mutex_destroy(&vm->dict_lock);
#ifdef __STARKERNEL__
sf_free(vm->call_stack);
vm->call_stack = NULL;
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): free the native stack
* allocated at birth (capsule_birth_baby() / sk_vm_bootstrap_parity()).
* Nothing executes on it yet (that's Stage 2), so there is no
* switched-out-state guard to check here yet -- Stage 2 adds that
* guard at the KILL call sites, before this function is ever reached
* for a VM that would need it. */
if (vm->native_stack_paddr != 0) {
sk_vm_native_stack_t ns;
ns.paddr = vm->native_stack_paddr;
ns.guard_vaddr = vm->native_stack_guard_vaddr;
ns.stack_top = vm->native_stack_top;
sk_vm_native_stack_free(&ns);
vm->native_stack_paddr = 0;
vm->native_stack_guard_vaddr = 0;
vm->native_stack_top = 0;
}
#endif
}
/* ====================== Parser / number ======================= */
/*
* @brief Parse next word from input buffer
*
* Skips leading whitespace and extracts next word delimited by whitespace.
*
* @param vm Pointer to VM instance
* @param word Buffer to store parsed word
* @param max_len Maximum length of word buffer
* @return Length of parsed word or 0 if no word found
*/
int vm_parse_word(VM* vm, char* word, size_t max_len)
{
if (!vm || !word || max_len == 0) return 0;
vm_assert_interpreter_enabled(vm, "vm_parse_word");
/* Skip whitespace */
while (vm->input_pos < vm->input_length)
{
char c = vm->input_buffer[vm->input_pos];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') break;
vm->input_pos++;
}
if (vm->input_pos >= vm->input_length) return 0;
size_t len = 0;
while (vm->input_pos < vm->input_length && len < max_len - 1)
{
char c = vm->input_buffer[vm->input_pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') break;
word[len++] = c;
vm->input_pos++;
}
word[len] = '\0';
return (int)len;
}
/*
* @brief Parse string as number in current base
*
* Attempts to parse string as number using VM's current number base.
* Handles optional sign prefix.
*
* @param vm Pointer to VM instance
* @param s String to parse
* @param out Pointer to store parsed value
* @return 1 on success, 0 on parse failure
*/
int vm_parse_number(VM* vm, const char* s, cell_t* out)
{
if (!s || !*s || !out) return 0;
vm_assert_interpreter_enabled(vm, "vm_parse_number");
unsigned base = vm_get_base(vm);
int neg = 0;
if (*s == '+' || *s == '-')
{
neg = (*s == '-');
s++;
if (!*s) return 0;
}
unsigned long long acc = 0;
int any = 0;
for (const char* p = s; *p; ++p)
{
unsigned d;
unsigned char c = (unsigned char)*p;
if (c >= '0' && c <= '9') d = (unsigned)(c - '0');
else if (c >= 'A' && c <= 'Z') d = 10u + (unsigned)(c - 'A');
else if (c >= 'a' && c <= 'z') d = 10u + (unsigned)(c - 'a');
else return 0;
if (d >= base) return 0;
acc = acc * base + d;
any = 1;
}
if (!any) return 0;
cell_t v = (cell_t)acc;
if (neg) v = (cell_t)(-v);
*out = v;
return 1;
}
/* ====================== Compile state ======================= */
void vm_enter_compile_mode(VM* vm, const char* name, size_t len)
{
if (!vm) return;
vm->mode = MODE_COMPILE;
vm->state_var = -1;
vm_store_cell(vm, vm->state_addr, vm->state_var);
if (len > WORD_NAME_MAX) len = WORD_NAME_MAX;
memcpy(vm->current_word_name, name, len);
vm->current_word_name[len] = '\0';
/* Create colon word header with code pointer = execute_colon_word */
DictEntry* de = vm_create_word(vm, name, len, execute_colon_word);
vm->compiling_word = de;
if (!de)
{
log_message(LOG_ERROR, ": vm_create_word failed for '%.*s'", (int)len, name);
vm->error = 1;
return;
}
de->flags |= WORD_SMUDGED;
/* DF (first data cell) will hold the VM-relative address of threaded body */
vm_align(vm);
cell_t* df = vm_dictionary_get_data_field(de);
if (!df)
{
log_message(LOG_ERROR, ": no data field for '%.*s'", (int)len, name);
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)((vaddr_t)vm->here);
log_message(LOG_DEBUG, ": started '%s' at HERE=%zu", vm->current_word_name, vm->here);
}
void vm_compile_word(VM* vm, DictEntry* entry)
{
if (!vm || vm->mode != MODE_COMPILE) return;
if (!entry)
{
log_message(LOG_ERROR, "vm_compile_word: NULL entry");
vm->error = 1;
return;
}
vm_align(vm);
cell_t* slot = (cell_t*)vm_allot(vm, sizeof(cell_t));
if (!slot)
{
log_message(LOG_ERROR, "vm_compile_word: vm_allot failed");
vm->error = 1;
return;
}
*slot = (cell_t)(uintptr_t)
entry; /* threaded code stores DictEntry* as cell */
}
void vm_compile_literal(VM* vm, cell_t value)
{
if (!vm) return;
if (vm->mode != MODE_COMPILE)
{
vm_push(vm, value);
return;
}
DictEntry* LIT = vm_find_word(vm, "LIT", 3);
if (!LIT)
{
vm->error = 1;
log_message(LOG_ERROR, "LIT not found");
return;
}
vm_compile_word(vm, LIT);
vm_align(vm);
cell_t* val = (cell_t*)vm_allot(vm, sizeof(cell_t));
if (!val)
{
log_message(LOG_ERROR, "vm_compile_literal: vm_allot failed");
vm->error = 1;
return;
}
*val = value;
}
void vm_compile_call(VM* vm, word_func_t func)
{
/* FABRIC-3.md §XXXII.6, 2026-09-15: split from the original combined
* `if (!vm || vm->mode != MODE_COMPILE) { vm->error = 1; ... }` guard
* -- that form dereferenced vm->error even on the !vm branch of its
* own condition, a real NULL-pointer-deref risk found while adding
* this diagnostic, not just a missing message. */
if (!vm) return;
if (vm->mode != MODE_COMPILE)
{
log_message(LOG_ERROR, "vm_compile_call: not in compile mode");
vm->error = 1;
return;
}
DictEntry* entry = vm_dictionary_find_by_func(vm, func);
if (!entry)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_compile_call: entry not found");
return;
}
vm_compile_word(vm, entry);
}
void vm_compile_exit(VM* vm)
{
if (!vm || vm->mode != MODE_COMPILE) return;
DictEntry* EXIT = vm_find_word(vm, "EXIT", 4);
if (!EXIT)
{
vm->error = 1;
log_message(LOG_ERROR, "EXIT not found");
return;
}
vm_compile_word(vm, EXIT);
}
void vm_exit_compile_mode(VM* vm)
{
/* Split from the original `if (!vm || !vm->compiling_word)` guard --
* same NULL-deref risk as vm_compile_call() above (§XXXII.6). */
if (!vm) return;
if (!vm->compiling_word)
{
log_message(LOG_ERROR, "vm_exit_compile_mode: no compiling word (unbalanced ';'?)");
vm->error = 1;
return;
}
DictEntry* EXIT = vm_find_word(vm, "EXIT", 4);
if (!EXIT)
{
vm->error = 1;
log_message(LOG_ERROR, "EXIT not found");
return;
}
vm_compile_word(vm, EXIT);
vm->compiling_word->flags &= ~WORD_SMUDGED;
vm->compiling_word->flags |= WORD_COMPILED;
/* Cache coherence: the definition just became visible and now shadows
* any older word of the same name the hot-words cache may still hold. */
hotwords_cache_evict_name(vm->hotwords_cache,
vm->compiling_word->name,
vm->compiling_word->name_len);
/* Inherit creator's ACL from session context:
* zuse-compiled words bypass ACL-RECHECK on first execution (ACL_TTL_OPEN).
* Non-zuse words keep ttl=0, triggering ACL-RECHECK on first execution. */
if (vm->zuse_session) {
vm->compiling_word->acl_ttl = ACL_TTL_OPEN;
vm->compiling_word->acl_allow = 1;
}
cell_t* df = vm_dictionary_get_data_field(vm->compiling_word);
if (df)
{
uint64_t header_bytes = (uint64_t)(((uint8_t*)df + sizeof(cell_t)) - (uint8_t*)vm->compiling_word);
uint64_t body_start = (uint64_t)(vaddr_t)(uint64_t)(*df);
uint64_t here_bytes = (uint64_t)vm->here;
uint64_t body_bytes = (here_bytes >= body_start) ? (here_bytes - body_start) : 0;
uint64_t total = header_bytes + body_bytes;
uint32_t mass = (total > UINT32_MAX) ? UINT32_MAX : (uint32_t)total;
physics_metadata_set_mass(vm->compiling_word, mass);
}
physics_metadata_refresh_state(vm->compiling_word);
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, vm->state_var);
vm->compiling_word = NULL;
log_message(LOG_DEBUG, "; end definition");
}
/* ====================== Inner interpreter ======================= */
/*
Threaded code layout (compiled by vm_compile_word / vm_compile_literal):
DF cell (in DictEntry) holds a VM address (vaddr_t) of the first code cell.
Each code cell is a cell_t that encodes a DictEntry* (for a word to call),
or is a literal payload following a compiled LIT word.
Control-flow runtime words (e.g., (BRANCH), (0BRANCH), (DO), loops) are
responsible for *modifying the IP stored at the top of the return stack*.
The inner interpreter saves the "next ip" on the return stack before
calling the word; after the word returns, we pop the possibly-modified IP
and continue. This matches your runtime branch helpers' contract.
IMPORTANT: EXIT behavior —
Words implement EXIT by setting vm->exit_colon = 1 (one-shot).
We honor that flag here to unwind the *current* colon only,
without disturbing the caller’s R-stack frame.
*/
/* vm.c */
/* Executes the threaded code of a colon definition.
* Uses a return-stack IP: each call saves the next IP on RS and pops it on return.
* When vm->exit_colon is set, it discards the saved IP and returns early.
*/
/* Executes a colon-defined word (direct/threaded).
* Contract: before each call we push the resume IP on RS; after the call we pop
* the (possibly modified) IP. If a word sets vm->exit_colon, we discard the
* saved resume IP and return to the caller (one-shot).
*/
/*
* acl_recheck — cold-path ACL policy call (TTL expired).
* Looks up ACL-RECHECK in the dictionary; if loaded, calls it with
* emergency_console=1 so the check itself is never denied.
* Fails open if ACL.4th is not yet loaded (early boot).
*/
static void acl_recheck(VM *vm, DictEntry *entry)
{
DictEntry *recheck = vm_find_word(vm, "ACL-RECHECK", 11);
if (!recheck || !recheck->func) {
entry->acl_allow = 1;
entry->acl_ttl = ACL_TTL_OPEN;
return;
}
uint8_t saved_ec = vm->emergency_console;
vm->emergency_console = 1;
vm_push(vm, (cell_t)(uintptr_t)entry);
DictEntry *saved_ce = vm->current_executing_entry;
vm->current_executing_entry = recheck;
recheck->func(vm);
vm->current_executing_entry = saved_ce;
vm->emergency_console = saved_ec;
if (vm->error) {
vm->error = 0;
entry->acl_allow = 1;
entry->acl_ttl = ACL_TTL_OPEN;
log_message(LOG_WARN, "acl_recheck: ACL-RECHECK errored for '%.*s'; failing open",
(int)entry->name_len, entry->name);
}
}
void execute_colon_word(VM* vm)
{
if (!vm || !vm->current_executing_entry) return;
vm_assert_interpreter_enabled(vm, "execute_colon_word");
/* Fetch threaded body address from the DictEntry's data field (DF) */
DictEntry* entry = vm->current_executing_entry;
cell_t* df = vm_dictionary_get_data_field(entry);
if (!df)
{
log_message(LOG_ERROR, "execute_colon_word: no data field for '%.*s'",
(int)entry->name_len, entry->name);
vm->error = 1;
return;
}
/* DF holds a VM virtual address (byte offset) of the first code cell */
vaddr_t body_addr = (vaddr_t)(uint64_t)(*df);
cell_t* ip = (cell_t*)vm_ptr(vm, body_addr);
if (!ip)
{
log_message(LOG_ERROR, "execute_colon_word: bad body address for '%.*s'",
(int)entry->name_len, entry->name);
vm->error = 1;
return;
}
/* Phase 1: Track word-to-word transitions for pipelining metrics */
/* L8 FINAL INTEGRATION: Loop always-on */
DictEntry* prev_word = NULL;
vm->ecw_nesting++;
for (;;)
{
/* Each code cell stores a DictEntry* (called word) */
DictEntry* w = (DictEntry*)(uintptr_t)(*ip);
if (w)
{
/* Phase 2: Apply linear decay before accumulating new heat.
* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay(). */
uint64_t now_ns = vm_monotonic_ns(vm);
uint64_t elapsed_ticks = vm->heartbeat.tick_count - w->physics.last_decay_tick;
physics_metadata_apply_linear_decay(w, elapsed_ticks, vm);
w->physics.last_decay_tick = vm->heartbeat.tick_count;
w->physics.last_active_ns = now_ns;
w->physics.last_decay_ns = now_ns;
physics_execution_heat_increment(w);
/* item 4.1, FABRIC-0.md §17.7: feed the Stadium's independent
* conserved heat wire. execution_heat above is untouched by
* this call. item 4.2: dispatching VM's own identity, not the
* item-4.1 hardcoded vm_uuid_hera() -- refused harmlessly by
* stadium_admit() for any VM without a granted quota. */
stadium_word_dispatch(vm->stadium_vm_id, w->word_id, vm->heartbeat.tick_count);
uint32_t word_id = w->word_id;
if (word_id < DICTIONARY_SIZE)
{
/* L8 FINAL INTEGRATION: Loop #2 always-on - Rolling Window of Truth */
rolling_window_record_execution(&vm->rolling_window, word_id);
/* L8 FINAL INTEGRATION: Loop always-on */ /* Loop #4: Pipelining Transition Metrics - WIRED & UTILIZED */
if (prev_word && prev_word->transition_metrics && ENABLE_PIPELINING)
{
/* Check if current word was speculatively promoted by previous word */
uint32_t prev_speculation_target = prev_word->transition_metrics->most_likely_next_word_id;
if (prev_speculation_target == word_id && prev_word->transition_metrics->prefetch_attempts > 0)
{
/* PREFETCH HIT: Current word matches previous word's speculation! */
transition_metrics_record_prefetch_hit(prev_word->transition_metrics, 0);
vm->pipeline_metrics.prefetch_hits++;
}
/* Record transition from previous word to current word */
transition_metrics_record(prev_word->transition_metrics, word_id, DICTIONARY_SIZE);
/* Update probability cache to find most likely next word */
transition_metrics_update_cache(prev_word->transition_metrics, DICTIONARY_SIZE);
uint32_t speculated_word_id = prev_word->transition_metrics->most_likely_next_word_id;
/* Check if we should speculatively prefetch the most likely next word */
if (speculated_word_id < DICTIONARY_SIZE &&
transition_metrics_should_speculate(prev_word->transition_metrics, speculated_word_id))
{
/* Speculation decision: the most likely next word has high probability
* Action: Promote it to hotwords cache now (speculative pre-caching)
* This way when we actually look up that word, it's cache-warm */
DictEntry *spec_entry = vm_dictionary_lookup_by_word_id(vm, speculated_word_id);
/* If we found the entry and have a cache, promote it speculatively */
if (spec_entry && vm->hotwords_cache && ENABLE_HOTWORDS_CACHE)
{
spec_entry->execution_heat = HOTWORDS_EXECUTION_HEAT_THRESHOLD + 1; /* Ensure promotion */
hotwords_cache_promote(vm->hotwords_cache, spec_entry);
/* Record the prefetch attempt (both per-word and global) */
prev_word->transition_metrics->prefetch_attempts++;
vm->pipeline_metrics.prefetch_attempts++;
}
}
}
/* End always-on loop */ }
}
/* Advance IP to next cell and save resume IP on return stack */
ip = ip + 1;
vm_rpush(vm, (cell_t)(uintptr_t)ip);
if (vm->error) { vm->ecw_nesting--; return; }
/* Execute the word */
vm->current_executing_entry = w;
/* Track word execution for profiling */
profiler_word_count(w);
/* ACL two-level check: hot path decrements TTL; cold path calls ACL-RECHECK.
* Bypass only for emergency fault-recovery REPL (bare ok> prompt). */
if (w && !vm->emergency_console) {
if (w->acl_ttl == 0)
acl_recheck(vm, w);
else
w->acl_ttl--;
if (!w->acl_allow) {
log_message(LOG_WARN, "ACL: denied '%.*s'", (int)w->name_len, w->name);
vm->dsp = -1;
vm->rsp = -1;
vm->error = 1;
vm->ecw_nesting--;
return;
}
}
if (w && w->func)
{
#ifdef __STARKERNEL__
g_sk_fault_word = (w->name_len > 0) ? w->name : "?";
log_message(LOG_DEBUG, "ECW: w=%p func=%p '%.*s'",
(void *)(uintptr_t)w,
(void *)(uintptr_t)(w->func),
(int)w->name_len, w->name);
#endif
profiler_word_enter(w);
w->func(vm);
physics_metadata_touch(w, w->execution_heat, vm_monotonic_ns(vm));
profiler_word_exit(w);
vm->heartbeat.words_executed++; /* DoE counter */
}
else
{
const char *pname = (entry && entry->name_len > 0) ? entry->name : "?";
cell_t *bad_ip = ip - 1;
const char *prev_name = (prev_word && prev_word->name_len > 0) ? prev_word->name : "(start)";
if (!w)
log_message(LOG_ERROR,
"execute_colon_word: NULL cell in '%s' after '%s' at %p",
pname, prev_name, (void *)bad_ip);
else
log_message(LOG_ERROR,
"execute_colon_word: null func '%s' in '%s' after '%s' at %p",
w->name, pname, prev_name, (void *)bad_ip);
/* Dump 3 preceding cells — raw values as pointer-sized hex via %p */
log_message(LOG_ERROR, " [ip-3]=%p val=%p",
(void *)(bad_ip - 2), (void *)(uintptr_t)*(bad_ip - 2));
log_message(LOG_ERROR, " [ip-2]=%p val=%p",
(void *)(bad_ip - 1), (void *)(uintptr_t)*(bad_ip - 1));
log_message(LOG_ERROR, " [ip-1]=%p val=%p (BAD)",
(void *)bad_ip, (void *)(uintptr_t)*bad_ip);
vm->error = 1;
}
vm->current_executing_entry = entry; /* restore current colon */
/* L8 FINAL INTEGRATION: Loop #4 always-on - Pipelining */
if (w) prev_word = w; /* always track for diagnostics */
if (ENABLE_PIPELINING && w) { /* transition metrics wired here when enabled */ }
/* Heartbeat: Periodic time-driven tuning (Loop #3 & #5) */
if (!vm->heartbeat.worker && ++vm->heartbeat.check_counter >= HEARTBEAT_CHECK_FREQUENCY)
{
vm_heartbeat_run_cycle(vm);
vm->heartbeat.check_counter = 0;
}
#ifdef __STARKERNEL__
/* FABRIC-3.6.md task 3.4 (SXLIII.3-.5): the target drains its own
* pending queue here, in its own context, at its own outermost
* interpret checkpoint -- same gate, same placement, same
* defer-if-nested discipline the switch-signal checkpoint just
* below already uses (SXLIII.2/.4: this IS that machinery,
* generalized). Placed BEFORE the switch-signal block, not after:
* sk_vm_context_switch() below does not return until something
* later switches back to `vm`, so a drain placed after it would
* silently never run on any checkpoint that switches. */
if (sk_vm_at_outermost_interpret()) {
(void)sk_hermes_drain_checkpoint(vm);
}
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the cooperative
* preemption checkpoint. Deliberately unconditional (every word,
* not throttled like the heartbeat cycle above) -- the check
* itself is a single flag read, cheap regardless of frequency,
* and "a word is a mnemonic" (this project's own eventual FPGA
* framing) is the granularity preemption should approximate here.
* Only ever acts when this word's own execution left the VM in a
* clean, resumable state (past the error/abort/exit_colon checks
* below would be too late -- this runs before them, deliberately,
* so a switch never happens mid-unwind).
*
* Stage 3 correction (2026-09-13): also gated on
* sk_vm_at_outermost_interpret() -- see g_vm_interpret_depth's own
* doc comment above vm_interpret(). If this word is executing
* nested inside a VM-EXEC/VM-CALL dispatch (from another VM's own
* vm_interpret() call, still on the mainline stack), `vm` here does
* not own the physical stack it's currently running on, so a real
* switch must not fire -- it would park that borrowed stack region
* under this VM's saved-context pointer, corrupting the enclosing
* VM's own call chain the moment it resumes. Left pending
* (take_pending() is not called at all in that case) so the next
* checkpoint at the outermost frame picks it up instead of losing
* it.
*
* note_switch_performed() is called BEFORE sk_vm_context_switch(),
* not after (Stage 3 follow-on correction, 2026-09-13): the switch
* call does not return until something later switches back to
* `vm`, which could be an arbitrary number of ticks later --
* calling it after would attribute switch_count_cumulative/
* switch_ticks_since to the wrong (resume) event instead of the
* actual switch-out this counts. It also resets target_id's own
* eligibility state directly (2026-09-14 correction) -- see its
* own doc comment for why that can't be left to ISR polling. */
if (sk_vm_at_outermost_interpret()) {
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
VMRegistryEntry target_entry;
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
* marked pending_reap is a WIREBIND identity whose
* owning device has physically detached while it sat
* parked SWITCHED_OUT -- capsule_vm_kill() already
* refused to free it when that happened (see
* VMRegistryEntry.pending_reap's own doc comment).
* This checkpoint is the safe point the mark-and-
* defer design calls for: reap instead of resuming,
* and skip the switch this tick -- take_pending()
* already consumed the pending flag, so no other
* checkpoint will try this same dead end. */
if (target_entry.pending_reap) {
capsule_vm_force_reap(target_id);
} else {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}
}
}
}
#endif
if (vm->error) { vm->ecw_nesting--; return; }
/* ABORT clears stacks and unwinds to QUIT: leave the flag set so
* every enclosing colon-word frame (this function is recursive --
* every colon word's func is execute_colon_word) also sees it and
* also unwinds, instead of consuming it at the first frame that
* notices. The outermost caller (vm_interpret et al.) is
* responsible for finally clearing it. */
if (vm->abort_requested)
{
vm->ecw_nesting--;
return;
}
/* One-shot early return? (EXIT) */
if (vm->exit_colon)
{
vm->exit_colon = 0;
/* CRITICAL: discard the per-step resume IP */
(void)vm_rpop(vm);
vm->ecw_nesting--;
return;
}
/* Normal path: resume at IP popped from RS (possibly patched by runtime) */
ip = (cell_t*)(uintptr_t)vm_rpop(vm);
if (vm->error) { vm->ecw_nesting--; return; }
}
}
/* ====================== Outer interpreter ======================= */
void vm_interpret_word(VM* vm, const char* word_str, size_t len)
{
if (!vm || !word_str) return;
vm_assert_interpreter_enabled(vm, "vm_interpret_word");
log_message(LOG_DEBUG, "INTERPRET: '%.*s' (mode=%s)",
(int)len, word_str,
vm->mode == MODE_COMPILE ? "COMPILE" : "INTERPRET");
/* Prefer vocabulary-aware lookup; fall back to canonical dictionary */
extern DictEntry*vm_vocabulary_find_word(VM* vm, const char* name, size_t nlen);
DictEntry* entry = vm_vocabulary_find_word(vm, word_str, len);
DictEntry* canon = vm_find_word(vm, word_str, len);
if (!entry) entry = canon;
if (entry)
{
/* Bump usage counters - Thread safety: lock dict for heat modifications */
uint64_t lookup_ns = vm_monotonic_ns(vm);
sf_mutex_lock(&vm->dict_lock);
/* Phase 2: Apply linear decay before accumulating heat.
* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay(). */
uint64_t elapsed_entry_ticks = vm->heartbeat.tick_count - entry->physics.last_decay_tick;
physics_metadata_apply_linear_decay(entry, elapsed_entry_ticks, vm);
entry->physics.last_decay_tick = vm->heartbeat.tick_count;
entry->physics.last_active_ns = lookup_ns;
entry->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(entry);
stadium_word_dispatch(vm->stadium_vm_id, entry->word_id, vm->heartbeat.tick_count);
if (canon && canon != entry)
{
/* Apply decay to canonical entry as well */
uint64_t elapsed_canon_ticks = vm->heartbeat.tick_count - canon->physics.last_decay_tick;
physics_metadata_apply_linear_decay(canon, elapsed_canon_ticks, vm);
canon->physics.last_decay_tick = vm->heartbeat.tick_count;
canon->physics.last_active_ns = lookup_ns;
canon->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(canon);
stadium_word_dispatch(vm->stadium_vm_id, canon->word_id, vm->heartbeat.tick_count);
physics_metadata_touch(canon, canon->execution_heat, lookup_ns);
}
sf_mutex_unlock(&vm->dict_lock);
/* Immediate if either entry or canonical is flagged immediate */
int is_immediate =
((entry && (entry->flags & WORD_IMMEDIATE)) ||
(canon && (canon->flags & WORD_IMMEDIATE)));
if (vm->mode == MODE_COMPILE && !is_immediate)
{
log_message(LOG_DEBUG, "COMPILE: '%.*s'", (int)len, word_str);
vm_compile_word(vm, entry);
return;
}
log_message(LOG_DEBUG, "EXECUTE: '%.*s'", (int)len, word_str);
vm->current_executing_entry = entry;
/* Track word execution for profiling */
profiler_word_count(entry);
/* ACL check on outer-interpreter path — bypass only at bare ok> prompt */
if (!vm->emergency_console) {
if (entry->acl_ttl == 0)
acl_recheck(vm, entry);
else
entry->acl_ttl--;
if (!entry->acl_allow) {
log_message(LOG_WARN, "ACL: denied '%.*s'", (int)entry->name_len, entry->name);
vm->dsp = -1;
vm->rsp = -1;
vm->error = 1;
return;
}
}
if (entry->func)
{
#ifdef __STARKERNEL__
g_sk_fault_word = (entry->name_len > 0) ? entry->name : "?";
#endif
profiler_word_enter(entry);
entry->func(vm);
physics_metadata_touch(entry, entry->execution_heat, vm_monotonic_ns(vm));
profiler_word_exit(entry);
vm->heartbeat.words_executed++; /* DoE counter */
}
else
{
log_message(LOG_ERROR, "NULL func for '%.*s'", (int)len, word_str);
vm->error = 1;
}
vm->current_executing_entry = NULL;
return;
}
/* Not found: try to parse a number in the current BASE */
cell_t value;
if (vm_parse_number(vm, word_str, &value))
{
log_message(LOG_DEBUG, "NUMBER: '%.*s' = %ld", (int)len, word_str, (long)value);
if (vm->mode == MODE_COMPILE)
{
vm_compile_literal(vm, value);
}
else
{
vm_push(vm, value);
}
return;
}
/* Unknown word */
log_message(LOG_ERROR, "UNKNOWN WORD: '%.*s'", (int)len, word_str);
vm->error = 1;
}
/*
* @brief Interpret a string of Forth code
*
* Main interpretation loop that:
* - Loads input into VM buffer
* - Parses words
* - Executes or compiles each word
* - Handles numbers
*
* @param vm Pointer to VM instance
* @param input String containing Forth code to interpret
*/
/* FABRIC-3.md §XXVI follow-on, Step 4 (log_attrib.h): which VM's dictionary
* context is currently executing, for log persistence attribution. Save/
* restore (not a plain set) so a nested vm_interpret() call -- VM-EXEC
* dispatching into a different VM's own dictionary, mid-interpret --
* attributes to whichever VM is actually running when it returns to the
* outer frame, not silently to the inner one. */
static VM *g_log_attrib_vm; /* zero-initialized (BSS) */
VM *vm_log_attributed_vm(void) { return g_log_attrib_vm; }
/* FABRIC-3.md §XXVIII Stage 3 correction (2026-09-13): nesting depth of
* vm_interpret() calls, tracked the same save/restore-adjacent way as
* g_log_attrib_vm just above. A nested call (VM-EXEC/VM-CALL dispatching
* into a different VM's own dictionary mid-interpret) runs on physical
* stack memory it does not own -- the Stage 3 switch checkpoint in
* execute_colon_word() below must never fire a real context switch there,
* only at the outermost vm_interpret() call, or it parks a borrowed stack
* region under the wrong VM's saved-context pointer (silent corruption,
* not just the trampoline livelock this was first found chasing -- see
* FABRIC-3.md for the full writeup). */
static int g_vm_interpret_depth;
int sk_vm_at_outermost_interpret(void) { return g_vm_interpret_depth <= 1; }
void vm_interpret(VM* vm, const char* input)
{
if (!vm || !input) return;
vm_assert_interpreter_enabled(vm, "vm_interpret");
VM *prev_attrib_vm = g_log_attrib_vm;
g_log_attrib_vm = vm;
g_vm_interpret_depth++;
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
while (n < cap)
{
char c = input[n];
vm->input_buffer[n] = c;
if (c == '\0') break;
++n;
}
if (n == cap) vm->input_buffer[n] = '\0';
vm->input_length = n;
vm->input_pos = 0;
char word[64];
size_t wlen;
/* !abort_requested: ABORT unwinds all the way to QUIT -- stop parsing
* further words in this input the moment it's seen. Flag is left set
* on return; caller (REPL, exec_block_with_retry, etc.) consumes it. */
while (!vm->error && !vm->abort_requested &&
(wlen = (size_t)vm_parse_word(vm, word, sizeof(word))) > 0)
{
vm_interpret_word(vm, word, wlen);
/* Heartbeat: Periodic time-driven tuning (Loop #3 & #5) */
if (!vm->heartbeat.worker && ++vm->heartbeat.check_counter >= HEARTBEAT_CHECK_FREQUENCY)
{
vm_heartbeat_run_cycle(vm);
vm->heartbeat.check_counter = 0;
}
}
g_vm_interpret_depth--;
g_log_attrib_vm = prev_attrib_vm;
}
/* ====================== VM memory helpers ======================= */
/*
* @brief Check if memory address range is valid
*
* Validates that an address range falls within VM memory bounds.
*
* @param vm Pointer to VM instance
* @param addr Virtual address to check
* @param len Length of memory range
* @return 1 if range is valid, 0 if invalid
*/
int vm_addr_ok(struct VM* vm, vaddr_t addr, size_t len)
{
if (!vm || !vm->memory) return 0;
if (len > VM_MEMORY_SIZE) return 0;
return addr <= (vaddr_t)(VM_MEMORY_SIZE - len);
}
uint8_t* vm_ptr(struct VM* vm, vaddr_t addr)
{
if (!vm || !vm->memory) return NULL;
if (!vm_addr_ok(vm, addr, 1)) return NULL;
return vm->memory + (size_t)addr;
}
/* FABRIC-3.md §XXXII.6, 2026-09-15: all four of these previously
* dereferenced vm->error after a failed vm_ptr()/vm_addr_ok() call --
* both of those tolerate vm==NULL internally (returning NULL/0), so a
* NULL vm reaching any of these four would have crashed on the error
* report itself, the same NULL-deref shape already found and fixed in
* vm_compile_call()/vm_exit_compile_mode() above. Each now guards `!vm`
* first, and reports the genuinely common case -- a bad/misaligned
* address from an ordinary interactive `@ ! C@ C!` -- with a diagnostic
* these four calls' sheer reach (every FORTH word built on them) makes
* the highest-value fix in this whole audit (§XXXII.3). */
uint8_t vm_load_u8(struct VM* vm, vaddr_t addr)
{
if (!vm) return 0;
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
log_message(LOG_ERROR, "vm_load_u8: bad address %lu", (unsigned long)addr);
vm->error = 1;
return 0;
}
return *p;
}
void vm_store_u8(struct VM* vm, vaddr_t addr, uint8_t v)
{
if (!vm) return;
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
log_message(LOG_ERROR, "vm_store_u8: bad address %lu", (unsigned long)addr);
vm->error = 1;
return;
}
*p = v;
}
cell_t vm_load_cell(struct VM* vm, vaddr_t addr)
{
if (!vm) return 0;
if (!vm_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
log_message(LOG_ERROR, "vm_load_cell: bad/misaligned address %lu", (unsigned long)addr);
vm->error = 1;
return 0;
}
cell_t out = 0;
memcpy(&out, vm->memory + (size_t)addr, sizeof(cell_t));
return out;
}
void vm_store_cell(struct VM* vm, vaddr_t addr, cell_t v)
{
if (!vm) return;
if (!vm_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
log_message(LOG_ERROR, "vm_store_cell: bad/misaligned address %lu", (unsigned long)addr);
vm->error = 1;
return;
}
memcpy(vm->memory + (size_t)addr, &v, sizeof(cell_t));
}
/* Make the most recently created word immediate (FORTH-79) */
void vm_make_immediate(VM* vm)
{
if (!vm) return;
if (!vm->latest)
{
log_message(LOG_ERROR, "vm_make_immediate: no latest word to mark IMMEDIATE");
vm->error = 1;
return;
}
vm->latest->flags |= WORD_IMMEDIATE;
physics_metadata_refresh_state(vm->latest);
log_message(LOG_DEBUG, "IMMEDIATE: '%.*s'",
(int)vm->latest->name_len, vm->latest->name);
}
#ifdef __STARKERNEL__
#define vm_check_arena(tag) sk_vm_arena_assert_guards(tag)
#else
#define vm_check_arena(tag) ((void)0)
#endif