1611 lines
51 KiB
Plaintext
1611 lines
51 KiB
Plaintext
/*
|
||
*** StarForth ***
|
||
|
||
vm.c- FORTH-79 Standard and ANSI C99 ONLY
|
||
Modified by - rajames
|
||
Last modified - 2025-11-09T23:23:06.742-05
|
||
|
||
Copyright (c) 2025 (rajames) Robert A. James - StarshipOS Forth Project.
|
||
|
||
This work is released into the public domain under the Creative Commons Zero v1.0 Universal license.
|
||
To the extent possible under law, the author(s) have dedicated all copyright and related
|
||
and neighboring rights to this software to the public domain worldwide.
|
||
This software is distributed without any warranty.
|
||
|
||
See <http://creativecommons.org/publicdomain/zero/1.0/> for more information.
|
||
|
||
/home/rajames/CLionProjects/StarForth/src/vm.c
|
||
*/
|
||
|
||
#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 <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdint.h>
|
||
#include <errno.h>
|
||
#include <time.h>
|
||
|
||
#if HEARTBEAT_THREAD_ENABLED && !defined(L4RE_TARGET)
|
||
#include <pthread.h>
|
||
#define HEARTBEAT_HAS_THREADS 1
|
||
#else
|
||
#define HEARTBEAT_HAS_THREADS 0
|
||
#endif
|
||
|
||
#define HEARTBEAT_DECAY_BATCH 64u
|
||
|
||
/** @name Forward Declarations
|
||
* @{
|
||
*/
|
||
void execute_colon_word(VM * vm); /* Non-static for SEE decompiler */
|
||
|
||
static void vm_bootstrap_scr(VM * vm);
|
||
|
||
static unsigned vm_get_base(const VM* vm);
|
||
|
||
static void vm_set_base(VM* vm, unsigned b);
|
||
|
||
typedef struct HeartbeatWorker
|
||
{
|
||
#if HEARTBEAT_HAS_THREADS
|
||
pthread_t thread;
|
||
#endif
|
||
uint64_t tick_ns;
|
||
int running;
|
||
int stop_requested;
|
||
} HeartbeatWorker;
|
||
|
||
static void vm_heartbeat_run_cycle(VM *vm);
|
||
static void heartbeat_publish_snapshot(VM *vm);
|
||
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns);
|
||
#if HEARTBEAT_THREAD_ENABLED
|
||
static void* heartbeat_thread_main(void *arg);
|
||
#endif
|
||
|
||
static inline uint32_t heartbeat_snapshot_index_load(const volatile uint32_t *ptr)
|
||
{
|
||
#if defined(__GNUC__)
|
||
return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
|
||
#else
|
||
return *ptr;
|
||
#endif
|
||
}
|
||
|
||
static inline void heartbeat_snapshot_index_store(volatile uint32_t *ptr, uint32_t value)
|
||
{
|
||
#if defined(__GNUC__)
|
||
__atomic_store_n(ptr, value, __ATOMIC_RELEASE);
|
||
#else
|
||
*ptr = value;
|
||
#endif
|
||
}
|
||
|
||
/* ====================== Base helpers ======================= */
|
||
|
||
static 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;
|
||
}
|
||
|
||
static 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 */
|
||
}
|
||
|
||
static void heartbeat_publish_snapshot(VM *vm)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
uint32_t current = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
|
||
uint32_t next = current ^ 1u;
|
||
HeartbeatSnapshot *snapshot = &vm->heartbeat.snapshots[next];
|
||
|
||
snapshot->published_tick = vm->heartbeat.tick_count;
|
||
snapshot->published_ns = sf_monotonic_ns();
|
||
snapshot->window_width = vm->rolling_window.effective_window_size;
|
||
snapshot->decay_slope_q48 = vm->decay_slope_q48;
|
||
snapshot->hot_word_count = vm->hot_word_count_at_check;
|
||
snapshot->stale_word_count = vm->stale_word_count_at_check;
|
||
snapshot->total_heat = vm->total_heat_at_last_check;
|
||
|
||
heartbeat_snapshot_index_store(&vm->heartbeat.snapshot_index, next);
|
||
}
|
||
|
||
/* ====================== VM init / teardown ======================= */
|
||
|
||
/**
|
||
* @brief Initialize a new virtual machine instance
|
||
*
|
||
* Allocates memory and initializes all VM structures including:
|
||
* - Memory array
|
||
* - Data and return stacks
|
||
* - System variables (SCR, STATE, BASE)
|
||
* - Dictionary
|
||
* - Forth-79 wordset
|
||
*
|
||
* @param vm Pointer to VM structure to initialize
|
||
*/
|
||
void vm_init(VM* vm)
|
||
{
|
||
if (!vm) return;
|
||
memset(vm, 0, sizeof(*vm));
|
||
vm->next_word_id = 0;
|
||
vm->recycled_word_id_count = 0;
|
||
|
||
if (sf_mutex_init(&vm->dict_lock) != 0)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: dict_lock init failed");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
if (sf_mutex_init(&vm->tuning_lock) != 0)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: tuning_lock init failed");
|
||
sf_mutex_destroy(&vm->dict_lock);
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
vm->memory = (uint8_t*)malloc(VM_MEMORY_SIZE);
|
||
if (!vm->memory)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: out of host memory");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
vm->dsp = -1;
|
||
vm->rsp = -1;
|
||
vm->here = 0;
|
||
vm->exit_colon = 0;
|
||
vm->abort_requested = 0;
|
||
|
||
vm_align(vm);
|
||
|
||
/* SCR */
|
||
{
|
||
void* p = vm_allot(vm, sizeof(cell_t));
|
||
if (!p)
|
||
{
|
||
vm->error = 1;
|
||
log_message(LOG_ERROR, "vm_init: SCR allot failed");
|
||
return;
|
||
}
|
||
vm->scr_addr = (vaddr_t)((uint8_t*)p - vm->memory);
|
||
vm_store_cell(vm, vm->scr_addr, 0);
|
||
}
|
||
|
||
/* STATE (0=interpret, -1=compile) */
|
||
{
|
||
void* p = vm_allot(vm, sizeof(cell_t));
|
||
if (!p)
|
||
{
|
||
vm->error = 1;
|
||
log_message(LOG_ERROR, "vm_init: STATE allot failed");
|
||
return;
|
||
}
|
||
vm->state_addr = (vaddr_t)((uint8_t*)p - vm->memory);
|
||
vm_store_cell(vm, vm->state_addr, 0);
|
||
vm->state_var = 0;
|
||
}
|
||
|
||
/* BASE (default 10) */
|
||
{
|
||
void* p = vm_allot(vm, sizeof(cell_t));
|
||
if (!p)
|
||
{
|
||
vm->error = 1;
|
||
log_message(LOG_ERROR, "vm_init: BASE allot failed");
|
||
return;
|
||
}
|
||
vm->base_addr = (vaddr_t)((uint8_t*)p - vm->memory);
|
||
vm_set_base(vm, 10);
|
||
}
|
||
|
||
vm_bootstrap_scr(vm);
|
||
|
||
vm->mode = MODE_INTERPRET;
|
||
vm->compiling_word = NULL;
|
||
vm->latest = NULL;
|
||
vm->error = 0;
|
||
vm->halted = 0;
|
||
|
||
vm->input_length = 0;
|
||
vm->input_pos = 0;
|
||
vm->current_executing_entry = NULL;
|
||
|
||
vm_debug_set_current_vm(vm);
|
||
vm_debug_install_signal_handlers();
|
||
|
||
/* Register Forth-79 wordset */
|
||
register_forth79_words(vm);
|
||
|
||
/* Set FORGET fence to post-boot */
|
||
vm->dict_fence_latest = vm->latest;
|
||
vm->dict_fence_here = vm->here;
|
||
|
||
/* Initialize hot-words cache (physics frequency-driven acceleration) */
|
||
vm->hotwords_cache = (HotwordsCache*)malloc(sizeof(HotwordsCache));
|
||
if (!vm->hotwords_cache)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: hotwords cache malloc failed");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
hotwords_cache_init(vm->hotwords_cache);
|
||
|
||
/* Initialize rolling window of truth (deterministic execution history) */
|
||
if (rolling_window_init(&vm->rolling_window) != 0)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: rolling window malloc failed");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
/* Initialize VM heartbeat (centralized time-driven tuning) */
|
||
vm->heartbeat.tick_count = 0;
|
||
vm->heartbeat.last_inference_tick = 0;
|
||
vm->heartbeat.check_counter = 0;
|
||
vm->heartbeat.heartbeat_enabled = 1; /* Enabled by default */
|
||
vm->heartbeat.tick_target_ns = HEARTBEAT_TICK_NS;
|
||
vm->heartbeat.snapshot_index = 0;
|
||
vm->heartbeat.worker = NULL;
|
||
vm->heartbeat_decay_cursor_id = WORD_ID_INVALID;
|
||
|
||
/* Initialize pipelining global metrics (aggregated prefetch tracking) */
|
||
vm->pipeline_metrics.prefetch_attempts = 0;
|
||
vm->pipeline_metrics.prefetch_hits = 0;
|
||
vm->pipeline_metrics.window_tuning_checks = 0;
|
||
vm->pipeline_metrics.last_checked_window_size = vm->rolling_window.effective_window_size;
|
||
vm->pipeline_metrics.last_checked_accuracy = 0.0;
|
||
vm->pipeline_metrics.suggested_next_size = vm->rolling_window.effective_window_size;
|
||
|
||
heartbeat_publish_snapshot(vm);
|
||
|
||
#if HEARTBEAT_HAS_THREADS
|
||
vm->heartbeat.worker = calloc(1, sizeof(HeartbeatWorker));
|
||
if (vm->heartbeat.worker)
|
||
{
|
||
vm->heartbeat.worker->tick_ns = HEARTBEAT_TICK_NS;
|
||
if (pthread_create(&vm->heartbeat.worker->thread, NULL, heartbeat_thread_main, vm) != 0)
|
||
{
|
||
log_message(LOG_WARN, "heartbeat: pthread_create failed (%d), falling back to inline mode", errno);
|
||
free(vm->heartbeat.worker);
|
||
vm->heartbeat.worker = NULL;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_WARN, "heartbeat: worker allocation failed, using inline heartbeat");
|
||
}
|
||
#endif
|
||
|
||
/* Initialize adaptive heat decay tuning (Loop #3) */
|
||
/* Start with 2:1 ratio in Q48.16 format: 2.0 << 16 = 131072 */
|
||
vm->decay_slope_q48 = (1ULL << 16) / 3; /* 1/3 starting slope (Q48.16) */
|
||
vm->last_decay_check_ns = 0;
|
||
vm->total_heat_at_last_check = 0;
|
||
vm->stale_word_count_at_check = 0;
|
||
vm->decay_slope_direction = 0; /* Start neutral */
|
||
|
||
/* Phase 2: Initialize heat-aware dictionary optimization */
|
||
vm->lookup_strategy = 0; /* Start with naive lookup, will adapt based on patterns */
|
||
vm->last_bucket_reorg_ns = 0; /* Force first reorg quickly */
|
||
dict_update_heat_percentiles(vm); /* Calculate initial percentiles */
|
||
|
||
/* SSM L8: Jacquard Mode Selector initialization */
|
||
vm->ssm_l8_state = malloc(sizeof(ssm_l8_state_t));
|
||
if (!vm->ssm_l8_state)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: SSM L8 state malloc failed");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
ssm_l8_init((ssm_l8_state_t*)vm->ssm_l8_state, SSM_MODE_C0);
|
||
|
||
vm->ssm_config = malloc(sizeof(ssm_config_t));
|
||
if (!vm->ssm_config)
|
||
{
|
||
log_message(LOG_ERROR, "vm_init: SSM config malloc failed");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
/* Initialize with C0 (minimal) mode: L2=0, L3=0, L5=0, L6=0 */
|
||
((ssm_config_t*)vm->ssm_config)->L2_rolling_window = 0;
|
||
((ssm_config_t*)vm->ssm_config)->L3_linear_decay = 0;
|
||
((ssm_config_t*)vm->ssm_config)->L5_window_inference = 0;
|
||
((ssm_config_t*)vm->ssm_config)->L6_decay_inference = 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Clean up and free VM resources
|
||
*
|
||
* Frees allocated memory and resets VM state.
|
||
*
|
||
* @param vm Pointer to VM structure to clean up
|
||
*/
|
||
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);
|
||
free(vm->heartbeat.worker);
|
||
vm->heartbeat.worker = NULL;
|
||
}
|
||
#endif
|
||
|
||
/* Clean up hot-words cache */
|
||
if (vm->hotwords_cache)
|
||
{
|
||
hotwords_cache_cleanup(vm->hotwords_cache);
|
||
free(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)
|
||
{
|
||
free(vm->ssm_l8_state);
|
||
vm->ssm_l8_state = NULL;
|
||
}
|
||
if (vm->ssm_config)
|
||
{
|
||
free(vm->ssm_config);
|
||
vm->ssm_config = NULL;
|
||
}
|
||
|
||
if (vm->memory)
|
||
{
|
||
free(vm->memory);
|
||
vm->memory = NULL;
|
||
}
|
||
vm->here = 0;
|
||
|
||
sf_mutex_destroy(&vm->tuning_lock);
|
||
sf_mutex_destroy(&vm->dict_lock);
|
||
}
|
||
|
||
/* ====================== VM Heartbeat (Time-Driven Tuning) ======================= */
|
||
|
||
/**
|
||
* @brief Central heartbeat dispatcher for time-driven tuning operations
|
||
*
|
||
* Aggregates all periodic optimization tasks (Loop #3 and Loop #5) into one place.
|
||
* Designed as plugin architecture - new tuning operations can be added as plugins.
|
||
*
|
||
* Options for integration:
|
||
* - Synchronous (now): Called from main execution loop, every N executions
|
||
* - Background thread (future): Runs in separate thread, decoupled from VM execution
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick(VM* vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled)
|
||
return;
|
||
|
||
vm->heartbeat.tick_count++;
|
||
|
||
/* Unified Inference Engine (Phase 2: Replaces Loops #3 & #5)
|
||
* Runs every HEARTBEAT_INFERENCE_FREQUENCY ticks to infer optimal:
|
||
* - Window width (via variance inflection detection)
|
||
* - Decay slope (via exponential regression on heat trajectory)
|
||
*/
|
||
if ((vm->heartbeat.tick_count - vm->heartbeat.last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY)
|
||
{
|
||
vm_tick_inference_engine(vm);
|
||
}
|
||
|
||
/* Plugin 2: System State Monitoring (Future) */
|
||
/* vm_tick_system_monitor(vm); */
|
||
|
||
/* Plugin 3: Formal Verification State Update (Future) */
|
||
/* vm_tick_formal_state_sync(vm); */
|
||
}
|
||
|
||
/**
|
||
* @brief Loop #5: Context-aware window tuning via binary chop search
|
||
*
|
||
* Uses prefetch accuracy to guide window size adaptation.
|
||
* Binary search converges on optimal effective_window_size for current workload.
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_window_tuner(VM* vm)
|
||
{
|
||
if (!vm || !vm->rolling_window.is_warm || !ENABLE_PIPELINING)
|
||
return;
|
||
|
||
RollingWindowOfTruth *window = &vm->rolling_window;
|
||
PipelineGlobalMetrics *metrics = &vm->pipeline_metrics;
|
||
|
||
/* Calculate current prefetch accuracy */
|
||
if (metrics->prefetch_attempts == 0)
|
||
return; /* Not enough data yet */
|
||
|
||
double current_accuracy = (double)metrics->prefetch_hits / (double)metrics->prefetch_attempts;
|
||
|
||
/* Binary chop suggests next window size to try */
|
||
uint32_t suggested_size = window->effective_window_size; /* Default: no change */
|
||
|
||
if (metrics->window_tuning_checks == 0)
|
||
{
|
||
/* First check: try shrinking by 25% */
|
||
suggested_size = (window->effective_window_size * 75) / 100;
|
||
}
|
||
else
|
||
{
|
||
/* Compare current accuracy to last check */
|
||
double accuracy_delta = current_accuracy - metrics->last_checked_accuracy;
|
||
|
||
if (accuracy_delta > 0.01) /* Improvement threshold: 1% */
|
||
{
|
||
/* Accuracy improved! Try shrinking more */
|
||
uint32_t smaller = (window->effective_window_size * 75) / 100;
|
||
suggested_size = (smaller > ADAPTIVE_MIN_WINDOW_SIZE) ? smaller : ADAPTIVE_MIN_WINDOW_SIZE;
|
||
}
|
||
else if (accuracy_delta < -0.01)
|
||
{
|
||
/* Accuracy degraded. Try growing instead */
|
||
uint32_t larger = (window->effective_window_size * 133) / 100; /* Grow by ~33% */
|
||
suggested_size = (larger < ROLLING_WINDOW_SIZE) ? larger : ROLLING_WINDOW_SIZE;
|
||
}
|
||
/* else: Plateau, stick with current size */
|
||
}
|
||
|
||
/* Apply if different */
|
||
if (suggested_size != window->effective_window_size)
|
||
{
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[window]: %u → %u (accuracy %.2f%%, %lu/%lu prefetch hits)",
|
||
window->effective_window_size,
|
||
suggested_size,
|
||
current_accuracy * 100.0,
|
||
metrics->prefetch_hits,
|
||
metrics->prefetch_attempts);
|
||
|
||
window->effective_window_size = suggested_size;
|
||
}
|
||
|
||
/* Record for next iteration */
|
||
metrics->last_checked_window_size = window->effective_window_size;
|
||
metrics->last_checked_accuracy = current_accuracy;
|
||
metrics->window_tuning_checks++;
|
||
}
|
||
|
||
/**
|
||
* @brief Loop #3: Heat decay slope validation via periodic measurement
|
||
*
|
||
* Validates that linear decay is actually helping optimize dictionary caching.
|
||
* Measures stale word ratio, hot word count, and average heat distribution.
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_slope_validator(VM* vm)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
/* Collect snapshot of current state */
|
||
uint64_t hot_word_count = 0;
|
||
uint64_t stale_word_count = 0;
|
||
uint64_t total_heat = 0;
|
||
uint32_t word_count = 0;
|
||
|
||
/* Scan dictionary and categorize words by heat level */
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
|
||
{
|
||
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
|
||
hot_word_count++;
|
||
else if (e->execution_heat > 0 && e->execution_heat < 10)
|
||
stale_word_count++;
|
||
|
||
total_heat += e->execution_heat;
|
||
word_count++;
|
||
}
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
|
||
double avg_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0;
|
||
double stale_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0;
|
||
|
||
/* === LOOP #3: INFERENCE ENGINE ===
|
||
* Compare current measurements to baseline from last check
|
||
* Decide whether decay is too fast, too slow, or optimal
|
||
*/
|
||
int new_slope_direction = 0; /* -1: decrease slope, 0: stable, +1: increase slope */
|
||
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
|
||
if (vm->word_count_at_check > 0)
|
||
{
|
||
/* Calculate trend in stale words: absolute count delta indicates accumulation/clearing */
|
||
int64_t stale_delta = (int64_t)stale_word_count - (int64_t)vm->stale_word_count_at_check;
|
||
|
||
/* INFERENCE: If stale words INCREASING, decay is too slow → increase slope */
|
||
/* If stale words DECREASING, decay is working (or too fast) → monitor */
|
||
if (stale_delta > 5) /* Threshold: 5+ additional stale words signals problem */
|
||
{
|
||
/* Stale words accumulating: decay is insufficient */
|
||
new_slope_direction = 1;
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, decay TOO SLOW, increase slope",
|
||
(long)stale_delta);
|
||
}
|
||
else if (stale_delta < -5) /* Threshold: 5+ fewer stale words signals clearing */
|
||
{
|
||
/* Stale words clearing: decay is aggressive (potentially too fast) */
|
||
/* Only decrease slope if avg_heat is dropping below target */
|
||
if (avg_heat < 5.0)
|
||
{
|
||
new_slope_direction = -1;
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, avg_heat=%.1f, decay TOO FAST, decrease slope",
|
||
(long)stale_delta, avg_heat);
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: stale_delta=%ld, decay working, hold slope",
|
||
(long)stale_delta);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: stale_delta=%ld (stable), hold slope",
|
||
(long)stale_delta);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: baseline measurement - hot_words=%lu, stale_ratio=%.2f%%, avg_heat=%.1f",
|
||
hot_word_count,
|
||
stale_ratio * 100.0,
|
||
avg_heat);
|
||
}
|
||
|
||
/* === APPLY SLOPE ADJUSTMENT ===
|
||
* Only adjust if direction changed (hysteresis to prevent oscillation)
|
||
*/
|
||
if (new_slope_direction != vm->decay_slope_direction && new_slope_direction != 0)
|
||
{
|
||
vm->decay_slope_direction = new_slope_direction;
|
||
|
||
/* Calculate adjustment in Q48.16: 5% change per cycle */
|
||
uint64_t adjustment = (vm->decay_slope_q48 * 5) / 100;
|
||
if (adjustment < 1) adjustment = 1; /* Minimum increment */
|
||
|
||
uint64_t old_slope = vm->decay_slope_q48;
|
||
if (new_slope_direction > 0)
|
||
{
|
||
vm->decay_slope_q48 += adjustment;
|
||
}
|
||
else if (new_slope_direction < 0)
|
||
{
|
||
vm->decay_slope_q48 = (vm->decay_slope_q48 > adjustment)
|
||
? (vm->decay_slope_q48 - adjustment)
|
||
: 1; /* Floor at 1 */
|
||
}
|
||
|
||
/* Log the adjustment as human-readable double */
|
||
double old_slope_dbl = (double)old_slope / 65536.0;
|
||
double new_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
|
||
log_message(LOG_INFO,
|
||
"HEARTBEAT[slope]: ADJUSTED slope from %.3f to %.3f (direction=%d)",
|
||
old_slope_dbl,
|
||
new_slope_dbl,
|
||
new_slope_direction);
|
||
}
|
||
|
||
/* Store baseline for next comparison */
|
||
vm->hot_word_count_at_check = hot_word_count;
|
||
vm->total_heat_at_last_check = total_heat;
|
||
vm->stale_word_count_at_check = stale_word_count;
|
||
vm->word_count_at_check = word_count;
|
||
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
}
|
||
|
||
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns)
|
||
{
|
||
if (!vm)
|
||
return;
|
||
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
|
||
DictEntry *cursor = NULL;
|
||
if (vm->heartbeat_decay_cursor_id != WORD_ID_INVALID)
|
||
cursor = vm_dictionary_lookup_by_word_id(vm, vm->heartbeat_decay_cursor_id);
|
||
if (!cursor)
|
||
cursor = vm->latest;
|
||
|
||
size_t processed = 0;
|
||
while (cursor && processed < HEARTBEAT_DECAY_BATCH)
|
||
{
|
||
uint64_t last_decay = cursor->physics.last_decay_ns;
|
||
if (last_decay == 0)
|
||
last_decay = cursor->physics.last_active_ns;
|
||
|
||
if (last_decay > 0 && now_ns > last_decay)
|
||
{
|
||
uint64_t elapsed_ns = now_ns - last_decay;
|
||
physics_metadata_apply_linear_decay(cursor, elapsed_ns, vm);
|
||
cursor->physics.last_decay_ns = now_ns;
|
||
}
|
||
|
||
cursor = cursor->link;
|
||
processed++;
|
||
}
|
||
|
||
vm->heartbeat_decay_cursor_id = (cursor && cursor->word_id != WORD_ID_INVALID)
|
||
? cursor->word_id
|
||
: WORD_ID_INVALID;
|
||
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
}
|
||
|
||
static void vm_heartbeat_run_cycle(VM *vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled)
|
||
return;
|
||
|
||
vm_tick(vm);
|
||
vm_tick_apply_background_decay(vm, sf_monotonic_ns());
|
||
rolling_window_service(&vm->rolling_window);
|
||
dict_adaptive_optimization_pass(vm); /* Adaptive dictionary optimization */
|
||
heartbeat_publish_snapshot(vm);
|
||
}
|
||
|
||
#if HEARTBEAT_THREAD_ENABLED
|
||
static void* heartbeat_thread_main(void *arg)
|
||
{
|
||
VM *vm = (VM*)arg;
|
||
if (!vm || !vm->heartbeat.worker)
|
||
return NULL;
|
||
|
||
HeartbeatWorker *worker = vm->heartbeat.worker;
|
||
worker->running = 1;
|
||
|
||
/* Wait for VM initialization to complete before starting heartbeat cycles.
|
||
* This avoids race conditions during word registration and test setup. */
|
||
{
|
||
struct timespec startup_delay = { .tv_sec = 0, .tv_nsec = 50000000 }; /* 50ms */
|
||
nanosleep(&startup_delay, NULL);
|
||
}
|
||
|
||
while (!worker->stop_requested)
|
||
{
|
||
vm_heartbeat_run_cycle(vm);
|
||
|
||
uint64_t tick_ns = worker->tick_ns ? worker->tick_ns : HEARTBEAT_TICK_NS;
|
||
struct timespec req;
|
||
req.tv_sec = (time_t)(tick_ns / 1000000000ULL);
|
||
req.tv_nsec = (long)(tick_ns % 1000000000ULL);
|
||
|
||
while (!worker->stop_requested && nanosleep(&req, &req) == -1 && errno == EINTR)
|
||
{
|
||
/* Retry with remaining time */
|
||
}
|
||
}
|
||
|
||
worker->running = 0;
|
||
return NULL;
|
||
}
|
||
#endif
|
||
|
||
void vm_snapshot_read(const VM* vm, HeartbeatSnapshot* out_snapshot)
|
||
{
|
||
if (!vm || !out_snapshot)
|
||
return;
|
||
|
||
uint32_t index = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
|
||
*out_snapshot = vm->heartbeat.snapshots[index];
|
||
}
|
||
|
||
/**
|
||
* @brief Phase 2: Unified Inference Engine - Adaptive Window & Decay Slope Tuning
|
||
*
|
||
* Coordinates inference on rolling window of truth to determine:
|
||
* - Optimal adaptive window width (via variance inflection detection)
|
||
* - Optimal decay slope (via exponential regression on heat trajectory)
|
||
*
|
||
* Uses ANOVA early-exit to skip full inference when variance is stable (<5% change).
|
||
* All math uses Q48.16 fixed-point (integer-only, no floating-point).
|
||
*
|
||
* Replaces legacy vm_tick_window_tuner() and vm_tick_slope_validator().
|
||
*
|
||
* @param vm Pointer to VM instance
|
||
*/
|
||
void vm_tick_inference_engine(VM* vm)
|
||
{
|
||
if (!vm || !vm->heartbeat.heartbeat_enabled || !vm->rolling_window.is_warm)
|
||
return;
|
||
|
||
/* DoE counter: inference engine invocations */
|
||
vm->heartbeat.inference_run_count++;
|
||
|
||
rolling_window_service(&vm->rolling_window);
|
||
|
||
/* Allocate InferenceOutputs if needed - protect with tuning_lock against race with doe_metrics */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
if (!vm->last_inference_outputs)
|
||
{
|
||
vm->last_inference_outputs = malloc(sizeof(InferenceOutputs));
|
||
if (!vm->last_inference_outputs)
|
||
{
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
log_message(LOG_ERROR, "INFERENCE: Failed to allocate InferenceOutputs");
|
||
return;
|
||
}
|
||
memset(vm->last_inference_outputs, 0, sizeof(InferenceOutputs));
|
||
}
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
/* === Collect Current Dictionary Metrics === */
|
||
uint64_t hot_word_count = 0;
|
||
uint64_t stale_word_count = 0;
|
||
uint64_t total_heat = 0;
|
||
uint32_t word_count = 0;
|
||
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
|
||
{
|
||
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
|
||
hot_word_count++;
|
||
else if (e->execution_heat > 0 && e->execution_heat < 10)
|
||
stale_word_count++;
|
||
|
||
total_heat += e->execution_heat;
|
||
word_count++;
|
||
}
|
||
sf_mutex_unlock(&vm->dict_lock);
|
||
|
||
/* === Populate InferenceInputs === */
|
||
InferenceInputs inference_inputs = {
|
||
.window = &vm->rolling_window,
|
||
.vm = vm, /* Required for dictionary lookups in extract_heat_trajectory */
|
||
.trajectory_length = (vm->rolling_window.window_pos > 0)
|
||
? vm->rolling_window.window_pos
|
||
: vm->rolling_window.total_executions,
|
||
.prefetch_hits = vm->pipeline_metrics.prefetch_hits,
|
||
.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts,
|
||
.hot_word_count = hot_word_count,
|
||
.stale_word_count = stale_word_count,
|
||
.total_heat = total_heat,
|
||
.word_count = word_count,
|
||
.last_total_heat = vm->total_heat_at_last_check,
|
||
.last_stale_count = vm->stale_word_count_at_check
|
||
};
|
||
|
||
/* === Run Unified Inference Engine === */
|
||
inference_engine_run(&inference_inputs, vm->last_inference_outputs);
|
||
|
||
/* === Apply Inferred Tuning Parameters === */
|
||
if (!vm->last_inference_outputs->early_exited)
|
||
{
|
||
/* Full inference was executed (not cached by ANOVA early-exit) */
|
||
|
||
/* 1. Apply adaptive window width */
|
||
if (vm->last_inference_outputs->adaptive_window_width > 0 &&
|
||
vm->last_inference_outputs->adaptive_window_width != vm->rolling_window.effective_window_size)
|
||
{
|
||
log_message(LOG_INFO,
|
||
"INFERENCE[window]: %u → %u (variance=%.6f Q48.16)",
|
||
vm->rolling_window.effective_window_size,
|
||
vm->last_inference_outputs->adaptive_window_width,
|
||
(double)vm->last_inference_outputs->window_variance_q48 / 65536.0);
|
||
vm->rolling_window.effective_window_size = vm->last_inference_outputs->adaptive_window_width;
|
||
}
|
||
|
||
/* 2. Apply adaptive decay slope */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
if (vm->last_inference_outputs->adaptive_decay_slope > 0 &&
|
||
vm->last_inference_outputs->adaptive_decay_slope != vm->decay_slope_q48)
|
||
{
|
||
double old_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
|
||
double new_slope_dbl = (double)vm->last_inference_outputs->adaptive_decay_slope / 65536.0;
|
||
|
||
log_message(LOG_INFO,
|
||
"INFERENCE[slope]: %.3f → %.3f (fit_quality=%.6f Q48.16)",
|
||
old_slope_dbl,
|
||
new_slope_dbl,
|
||
(double)vm->last_inference_outputs->slope_fit_quality_q48 / 65536.0);
|
||
vm->decay_slope_q48 = vm->last_inference_outputs->adaptive_decay_slope;
|
||
}
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
/* 3. Validate outputs */
|
||
if (!inference_outputs_validate(vm->last_inference_outputs))
|
||
{
|
||
log_message(LOG_WARN,
|
||
"INFERENCE: Output validation failed, ignoring results");
|
||
}
|
||
|
||
vm->heartbeat.last_inference_tick = vm->heartbeat.tick_count;
|
||
}
|
||
else
|
||
{
|
||
/* ANOVA early-exit: variance stable, using cached outputs */
|
||
vm->heartbeat.early_exit_count++;
|
||
log_message(LOG_DEBUG,
|
||
"INFERENCE: Early-exit (variance stable <5%%), using cached outputs");
|
||
}
|
||
|
||
/* Store baseline for next inference comparison */
|
||
sf_mutex_lock(&vm->tuning_lock);
|
||
vm->total_heat_at_last_check = total_heat;
|
||
vm->stale_word_count_at_check = stale_word_count;
|
||
vm->word_count_at_check = word_count;
|
||
sf_mutex_unlock(&vm->tuning_lock);
|
||
|
||
#if ENABLE_LOOP_7_ADAPTIVE_HEARTRATE
|
||
/* === Loop #7: Adaptive Heartrate ===
|
||
* Adjust tick frequency based on system stability:
|
||
* - Variance stable (early_exited) → increase tick interval (less frequent)
|
||
* - Variance volatile (full inference) → decrease tick interval (more frequent)
|
||
*
|
||
* Bounds: [HEARTBEAT_TICK_NS / 4, HEARTBEAT_TICK_NS * 4]
|
||
*/
|
||
{
|
||
uint64_t current_tick_ns = vm->heartbeat.tick_target_ns;
|
||
uint64_t min_tick_ns = HEARTBEAT_TICK_NS / 4; /* 4x faster minimum */
|
||
uint64_t max_tick_ns = HEARTBEAT_TICK_NS * 4; /* 4x slower maximum */
|
||
|
||
if (vm->last_inference_outputs && vm->last_inference_outputs->early_exited)
|
||
{
|
||
/* System stable: slow down heartbeat by 25% */
|
||
uint64_t new_tick_ns = (current_tick_ns * 125) / 100;
|
||
if (new_tick_ns > max_tick_ns) new_tick_ns = max_tick_ns;
|
||
|
||
if (new_tick_ns != current_tick_ns)
|
||
{
|
||
vm->heartbeat.tick_target_ns = new_tick_ns;
|
||
if (vm->heartbeat.worker)
|
||
vm->heartbeat.worker->tick_ns = new_tick_ns;
|
||
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[rate]: stable → slower tick %lu → %lu ns",
|
||
(unsigned long)current_tick_ns,
|
||
(unsigned long)new_tick_ns);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* System volatile: speed up heartbeat by 25% */
|
||
uint64_t new_tick_ns = (current_tick_ns * 80) / 100;
|
||
if (new_tick_ns < min_tick_ns) new_tick_ns = min_tick_ns;
|
||
|
||
if (new_tick_ns != current_tick_ns)
|
||
{
|
||
vm->heartbeat.tick_target_ns = new_tick_ns;
|
||
if (vm->heartbeat.worker)
|
||
vm->heartbeat.worker->tick_ns = new_tick_ns;
|
||
|
||
log_message(LOG_DEBUG,
|
||
"HEARTBEAT[rate]: volatile → faster tick %lu → %lu ns",
|
||
(unsigned long)current_tick_ns,
|
||
(unsigned long)new_tick_ns);
|
||
}
|
||
}
|
||
}
|
||
#endif /* ENABLE_LOOP_7_ADAPTIVE_HEARTRATE */
|
||
}
|
||
|
||
/* ====================== 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;
|
||
|
||
/* 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;
|
||
|
||
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;
|
||
|
||
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).
|
||
*/
|
||
void execute_colon_word(VM* vm)
|
||
{
|
||
if (!vm || !vm->current_executing_entry) return;
|
||
|
||
/* 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 */
|
||
#if ENABLE_LOOP_4_PIPELINING_METRICS
|
||
DictEntry* prev_word = NULL;
|
||
#endif
|
||
|
||
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 */
|
||
uint64_t now_ns = sf_monotonic_ns();
|
||
uint64_t elapsed_ns = now_ns - w->physics.last_active_ns;
|
||
physics_metadata_apply_linear_decay(w, elapsed_ns, vm);
|
||
w->physics.last_active_ns = now_ns;
|
||
w->physics.last_decay_ns = now_ns;
|
||
|
||
physics_execution_heat_increment(w);
|
||
|
||
uint32_t word_id = w->word_id;
|
||
if (word_id < DICTIONARY_SIZE)
|
||
{
|
||
#if ENABLE_LOOP_2_ROLLING_WINDOW
|
||
/* Rolling Window of Truth: Record execution for deterministic seeding */
|
||
rolling_window_record_execution(&vm->rolling_window, word_id);
|
||
#endif
|
||
|
||
#if ENABLE_LOOP_4_PIPELINING_METRICS
|
||
/* 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++;
|
||
}
|
||
}
|
||
}
|
||
#endif /* ENABLE_LOOP_4_PIPELINING_METRICS */
|
||
}
|
||
}
|
||
|
||
/* 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) { return; }
|
||
|
||
/* Execute the word */
|
||
vm->current_executing_entry = w;
|
||
|
||
/* Track word execution for profiling */
|
||
profiler_word_count(w);
|
||
|
||
if (w && w->func)
|
||
{
|
||
profiler_word_enter(w);
|
||
w->func(vm);
|
||
physics_metadata_touch(w, w->execution_heat, sf_monotonic_ns());
|
||
profiler_word_exit(w);
|
||
vm->heartbeat.words_executed++; /* DoE counter */
|
||
}
|
||
else
|
||
{
|
||
log_message(LOG_ERROR, "execute_colon_word: null word func");
|
||
vm->error = 1;
|
||
}
|
||
vm->current_executing_entry = entry; /* restore current colon */
|
||
|
||
/* Phase 1 (Pipelining): Update previous word for next transition recording */
|
||
#if ENABLE_LOOP_4_PIPELINING_METRICS
|
||
if (ENABLE_PIPELINING && w)
|
||
{
|
||
prev_word = w;
|
||
}
|
||
#endif
|
||
|
||
/* 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;
|
||
}
|
||
|
||
if (vm->error) { return; }
|
||
|
||
/* Check for ABORT request (clears both stacks, immediate termination) */
|
||
if (vm->abort_requested)
|
||
{
|
||
vm->abort_requested = 0;
|
||
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);
|
||
|
||
return;
|
||
}
|
||
|
||
/* Normal path: resume at IP popped from RS (possibly patched by runtime) */
|
||
ip = (cell_t*)(uintptr_t)vm_rpop(vm);
|
||
if (vm->error) { return; }
|
||
}
|
||
}
|
||
|
||
|
||
/* ====================== Outer interpreter ======================= */
|
||
|
||
void vm_interpret_word(VM* vm, const char* word_str, size_t len)
|
||
{
|
||
if (!vm || !word_str) return;
|
||
|
||
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 = sf_monotonic_ns();
|
||
|
||
sf_mutex_lock(&vm->dict_lock);
|
||
/* Phase 2: Apply linear decay before accumulating heat */
|
||
uint64_t elapsed_entry = lookup_ns - entry->physics.last_active_ns;
|
||
physics_metadata_apply_linear_decay(entry, elapsed_entry, vm);
|
||
entry->physics.last_active_ns = lookup_ns;
|
||
entry->physics.last_decay_ns = lookup_ns;
|
||
|
||
physics_execution_heat_increment(entry);
|
||
if (canon && canon != entry)
|
||
{
|
||
/* Apply decay to canonical entry as well */
|
||
uint64_t elapsed_canon = lookup_ns - canon->physics.last_active_ns;
|
||
physics_metadata_apply_linear_decay(canon, elapsed_canon, vm);
|
||
canon->physics.last_active_ns = lookup_ns;
|
||
canon->physics.last_decay_ns = lookup_ns;
|
||
|
||
physics_execution_heat_increment(canon);
|
||
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);
|
||
|
||
if (entry->func)
|
||
{
|
||
profiler_word_enter(entry);
|
||
entry->func(vm);
|
||
physics_metadata_touch(entry, entry->execution_heat, sf_monotonic_ns());
|
||
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
|
||
*/
|
||
void vm_interpret(VM* vm, const char* input)
|
||
{
|
||
if (!vm || !input) return;
|
||
|
||
/* 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;
|
||
while (!vm->error && (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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ====================== 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));
|
||
}
|
||
|
||
/* ====================== Bootstrap helpers ======================= */
|
||
|
||
static void vm_bootstrap_scr(VM* vm)
|
||
{
|
||
if (!vm) return;
|
||
/* Ensure SCR cell exists and is zero */
|
||
if (!vm_addr_ok(vm, vm->scr_addr, sizeof(cell_t)) ||
|
||
(vm->scr_addr % sizeof(cell_t)) != 0)
|
||
{
|
||
vm_align(vm);
|
||
void* p = vm_allot(vm, sizeof(cell_t));
|
||
if (!p)
|
||
{
|
||
vm->error = 1;
|
||
log_message(LOG_ERROR, "bootstrap SCR allot failed");
|
||
return;
|
||
}
|
||
vm->scr_addr = (vaddr_t)((uint8_t*)p - vm->memory);
|
||
}
|
||
vm_store_cell(vm, vm->scr_addr, 0);
|
||
}
|
||
|
||
/* 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);
|
||
}
|