Files
StarForth/src/starkernel/vm/vm_core.c
T
Robert Allan JamesandClaude Sonnet 5 986d042aa7 Stage 3: timer-driven preemptive switching, live on all 3 arches (FABRIC-3.md §XXVIII)
Fourth stage of the preemptive context-switching plan, and the biggest.
LithosAnanke now genuinely, continuously preempts between Hera, Hermes,
and Artemis -- timer-driven, running live for the entire remainder of
every boot once the Tripod fleet registers, not a bounded probe.

A real design fork was resolved before writing code: the naive approach
(the timer ISR calling Stage 2's sk_vm_context_switch() directly) is
broken -- Stage 0's trap frame lives on whatever stack was active at
interrupt time, and jumping to a different stack via Stage 2's own
independent swap mid-handler would abandon that trap frame unresumed,
guaranteed corruption on the first tick. Chose the safer of two named
options: the ISR only ever sets a flag and returns completely normally
through its own full epilogue; the actual switch happens moments later,
via Stage 2's already-proven mechanism, at a safe cooperative checkpoint
on the mainline (execute_colon_word()'s per-word dispatch loop, checked
on literally every word, not throttled to the existing 256-word
heartbeat-tuning cadence) -- confirmed with the user that word-level
granularity is fine-grained enough given the eventual Zynq FPGA target
where a word is a mnemonic.

New capsule_vm_switch_signal.c/.h: a purpose-built run-readiness signal,
deliberately separate from capsule_vm_physics.c's execution-heat engine
(that one's own header documents itself as never touched from interrupt
context, by design). Slot table sized with headroom (8) rather than
hardcoded to today's 3 participants, so extending participation later is
another register() call, not a redesign -- per direct request to leave
room for swapping the participant set. Simple linear accumulate-then-
threshold for this first cut; a fancier law can replace it later without
touching the mechanism around it. heartbeat_tick() gains its one
deliberate, documented amendment to this file's own top-half/bottom-half
discipline -- the first time this codebase reaches into VM-scheduling
state from real ISR context.

Registration happens only after all three VMs are fully born, right
before the REPL starts -- no critical-section protection yet against
being switched away mid-birth-setup.

Known, flagged rough edge (not reconciled this pass): MSG-TICK's own
idle-pump and this new mechanism can still independently move control
between the same VMs; not observed to interact badly in verification,
but not fully unified either.

Verified interactively at the console on all 3 architectures with
continuous background preemption running throughout -- amd64 computed
`1 1 + .` -> 2, aarch64 computed `1 1 + dup DUP * . CR` -> 4, both
correct, REPL fully responsive, zero fault indicators over sustained
runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S
2026-09-13 19:32:36 -04:00

1219 lines
40 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/capsule_birth.h" /* capsule_vm_registry_get() */
#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)
{
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)
{
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)
{
vm->error = 1;
return;
}
vm_align(vm);
cell_t* slot = (cell_t*)vm_allot(vm, sizeof(cell_t));
if (!slot)
{
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)
{
vm->error = 1;
return;
}
*val = value;
}
void vm_compile_call(VM* vm, word_func_t func)
{
if (!vm || vm->mode != MODE_COMPILE)
{
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)
{
if (!vm || !vm->compiling_word)
{
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)
{
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)
{
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.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). */
{
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) {
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; }
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;
/* 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_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;
}
uint8_t vm_load_u8(struct VM* vm, vaddr_t addr)
{
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
vm->error = 1;
return 0;
}
return *p;
}
void vm_store_u8(struct VM* vm, vaddr_t addr, uint8_t v)
{
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
vm->error = 1;
return;
}
*p = v;
}
cell_t vm_load_cell(struct VM* vm, vaddr_t addr)
{
if (!vm_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
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_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
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