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
1219 lines
40 KiB
C
1219 lines
40 KiB
C
/*
|
||
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
|