Stage S-1b: dictionary word ids grow with the dictionary; a word that can't get an id is refused whole (FABRIC-3.9.0 §8.1)

- word_id_map / recycled_word_ids are heap tables (one shared capacity,
  64 doubling), grown all-or-nothing by allocate-copy-free. Numbering is
  unchanged, so boot parity is unchanged.
- vm_create_word() takes the id before linking; on failure it frees the
  entry, sets vm->error, logs LOG_ERROR and counts
  (vm_dictionary_word_id_refusals()). No word runs with WORD_ID_INVALID.
- Readers of the id space bound by vm->next_word_id, not DICTIONARY_SIZE.
- transition_heat gets its own capacity and grows on record (was sized
  once -- a heap overflow under a dynamic bound); every reader bounds by it.
- Stadium id->cell rows start empty and grow on dispatch; a failed grow is
  logged, counted and not admitted. S-2 (the cell pool) stays open.

Probe (reverted), identical on amd64/aarch64/riscv64: +2000 words get real
ids (capacity 4096); a word at id 2738 lands 200x in the rolling window and
is Stadium-admitted; FORGET recycles across the old boundary; an injected
grow failure refuses the word whole (NULL, error, counter, dictionary
unchanged) and the retry succeeds. 3-ISA acceptance passes; boot parity
(531, 0x572daac83b797332) and the dict_hash set unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 13:59:43 -04:00
co-authored by Claude Opus 5.5
parent ee3db96ea8
commit 1dbd0c74ed
26 changed files with 55442 additions and 83 deletions
+86 -13
View File
@@ -75,6 +75,61 @@ size_t sf_fc_cap[SF_FC_BUCKETS];
static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
/* FABRIC-3.9.0.md §8.1 (Stage S-1b): stable word ids are no longer capped at
* DICTIONARY_SIZE. The id tables grow with the dictionary; a word that can't
* get an id is refused whole (vm_create_word() returns NULL, vm->error set),
* logged at LOG_ERROR and counted -- it is never defined with WORD_ID_INVALID
* to run silently outside the physics. */
#define WORD_ID_TABLE_MIN 64u
static uint32_t g_word_id_refusals;
uint32_t vm_dictionary_word_id_refusals(void) { return g_word_id_refusals; }
/* Grow both id tables to at least `need` entries, all-or-nothing: both new
* tables are allocated before either old one is touched, so a failure leaves
* the VM exactly as it was. Allocate-copy-free, not sf_realloc(): the
* kernel's sf_realloc() does not copy (alloc_kernel.c). */
static int vm_dictionary_grow_word_ids(VM *vm, uint32_t need) {
uint32_t new_cap = vm->word_id_capacity ? vm->word_id_capacity : WORD_ID_TABLE_MIN;
DictEntry **new_map;
uint32_t *new_recycled;
uint32_t i;
while (new_cap < need) {
/* Ids run 0..WORD_ID_INVALID-1, so a table never needs more than
* WORD_ID_INVALID entries. */
new_cap = (new_cap > WORD_ID_INVALID / 2u) ? WORD_ID_INVALID : new_cap * 2u;
}
new_map = (DictEntry **)sf_malloc((size_t)new_cap * sizeof(DictEntry *));
new_recycled = (uint32_t *)sf_malloc((size_t)new_cap * sizeof(uint32_t));
if (!new_map || !new_recycled) {
sf_free(new_map);
sf_free(new_recycled);
return -1;
}
for (i = 0; i < vm->word_id_capacity; i++) {
new_map[i] = vm->word_id_map[i];
}
for (; i < new_cap; i++) {
new_map[i] = NULL;
}
for (i = 0; i < vm->recycled_word_id_count; i++) {
new_recycled[i] = vm->recycled_word_ids[i];
}
sf_free(vm->word_id_map);
sf_free(vm->recycled_word_ids);
vm->word_id_map = new_map;
vm->recycled_word_ids = new_recycled;
vm->word_id_capacity = new_cap;
return 0;
}
/* Numbering is unchanged from the fixed-table version: a recycled id first,
* else the next fresh one. Only the fresh path can need a grow. */
static uint32_t vm_dictionary_acquire_word_id(VM *vm) {
if (!vm) {
return WORD_ID_INVALID;
@@ -84,29 +139,35 @@ static uint32_t vm_dictionary_acquire_word_id(VM *vm) {
return vm->recycled_word_ids[--vm->recycled_word_id_count];
}
if (vm->next_word_id < DICTIONARY_SIZE) {
return vm->next_word_id++;
if (vm->next_word_id == WORD_ID_INVALID) {
return WORD_ID_INVALID; /* every id is issued -- a real limit, refused the same way */
}
return WORD_ID_INVALID;
if (vm->next_word_id >= vm->word_id_capacity &&
vm_dictionary_grow_word_ids(vm, vm->next_word_id + 1u) != 0) {
return WORD_ID_INVALID;
}
return vm->next_word_id++;
}
void vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
int vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
if (!vm || !entry) {
return;
return -1;
}
uint32_t word_id = vm_dictionary_acquire_word_id(vm);
entry->word_id = word_id;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) {
log_message(LOG_WARN,
"vm_dictionary_track_entry: out of stable IDs, disabling metrics for '%.*s'",
if (word_id == WORD_ID_INVALID) {
g_word_id_refusals++;
log_message(LOG_ERROR, "word id table can't grow: '%.*s' refused",
(int)entry->name_len, entry->name);
return;
return -1;
}
vm->word_id_map[word_id] = entry;
return 0;
}
void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
@@ -125,7 +186,7 @@ void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
#endif
uint32_t word_id = entry->word_id;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) {
if (word_id == WORD_ID_INVALID || word_id >= vm->next_word_id) {
return;
}
@@ -142,7 +203,9 @@ void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
vm->word_id_map[word_id] = NULL;
}
if (vm->recycled_word_id_count < DICTIONARY_SIZE) {
/* Always fits: at most next_word_id ids were ever issued, and the tables
* hold word_id_capacity >= next_word_id entries. */
if (vm->recycled_word_id_count < vm->word_id_capacity) {
vm->recycled_word_ids[vm->recycled_word_id_count++] = word_id;
}
@@ -150,7 +213,7 @@ void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
}
DictEntry *vm_dictionary_lookup_by_word_id(VM *vm, uint32_t word_id) {
if (!vm || word_id >= DICTIONARY_SIZE) {
if (!vm || word_id >= vm->next_word_id) {
return NULL;
}
@@ -467,8 +530,18 @@ DictEntry *vm_create_word(VM *vm, const char *name, size_t len, word_func_t func
}
*(cell_t *) (((uint8_t *) entry) + df_off) = 0;
/* S-1b: take the stable id BEFORE linking, so a refusal leaves the
* dictionary exactly as it was -- nothing half-defined. */
if (vm_dictionary_track_entry(vm, entry) != 0) {
vm->error = 1;
if (entry->transition_metrics) {
sf_free(entry->transition_metrics);
}
sf_free(entry);
entry = NULL;
goto create_cleanup;
}
vm->latest = entry;
vm_dictionary_track_entry(vm, entry);
/* Cache coherence: this definition may shadow an older word of the same
* name that the hot-words cache is still serving. Evict the name so the
+1 -1
View File
@@ -184,7 +184,7 @@ static uint64_t* extract_heat_trajectory(
for (uint64_t i = 0; i < span; i++) {
uint32_t word_id = word_ids[start + i];
uint64_t heat = 0;
if (word_id < DICTIONARY_SIZE) {
if (word_id < vm->next_word_id) { /* S-1b: was DICTIONARY_SIZE */
DictEntry *entry = vm_dictionary_lookup_by_word_id(vm, word_id);
if (entry) {
heat = (uint64_t)entry->execution_heat;
+4 -4
View File
@@ -92,7 +92,7 @@ void physics_pre_execute(VM* vm, DictEntry* word, DictEntry* prev)
physics_execution_heat_increment(word);
uint32_t word_id = word->word_id;
if (word_id >= DICTIONARY_SIZE) return;
if (word_id >= vm->next_word_id) return; /* S-1b: ids are bounded by next_word_id, not DICTIONARY_SIZE */
/* Loop #2: Rolling Window of Truth */
rolling_window_record_execution(&vm->rolling_window, word_id);
@@ -116,15 +116,15 @@ void physics_pre_execute(VM* vm, DictEntry* word, DictEntry* prev)
}
/* Record transition from previous word to current word */
transition_metrics_record(prev->transition_metrics, word_id, DICTIONARY_SIZE);
transition_metrics_record(prev->transition_metrics, word_id, vm->next_word_id);
/* Update probability cache to find most likely next word */
transition_metrics_update_cache(prev->transition_metrics, DICTIONARY_SIZE);
transition_metrics_update_cache(prev->transition_metrics, vm->next_word_id);
uint32_t speculated_word_id = prev->transition_metrics->most_likely_next_word_id;
/* Speculative prefetch of most likely next word */
if (speculated_word_id < DICTIONARY_SIZE &&
if (speculated_word_id < vm->next_word_id &&
transition_metrics_should_speculate(prev->transition_metrics, speculated_word_id))
{
DictEntry* spec_entry = vm_dictionary_lookup_by_word_id(vm, speculated_word_id);
+38 -8
View File
@@ -52,6 +52,7 @@
#include "physics_pipelining_metrics.h"
#include "vm.h"
#include "log.h" /* S-1b: a failed transition_heat grow is logged */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -149,6 +150,7 @@ int transition_metrics_init(WordTransitionMetrics *metrics) {
if (!metrics) return -1;
metrics->transition_heat = NULL; /* Lazy allocation on first use */
metrics->transition_heat_cap = 0;
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
metrics->prefetch_hits = 0;
@@ -208,17 +210,41 @@ int transition_metrics_init(WordTransitionMetrics *metrics) {
* @return 0 on success; −1 on NULL @p metrics, out-of-bounds
* @p next_word_id, or allocation failure.
*/
static uint64_t g_transition_grow_failures;
uint64_t transition_metrics_grow_failures(void) { return g_transition_grow_failures; }
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size) {
if (!metrics || next_word_id >= dict_size) {
return -1;
}
/* Lazy allocation: only allocate transition_heat array when first transition occurs */
if (!metrics->transition_heat) {
metrics->transition_heat = (uint64_t *)calloc(dict_size, sizeof(uint64_t));
if (!metrics->transition_heat) {
return -1; /* Memory allocation failure */
/* Lazy allocation on the first transition, and growth when a word id at
* or past the current length arrives (S-1b: ids are no longer capped, so
* an array sized once can be outgrown). Grown to the VM's whole id bound,
* at least doubling, so a steady stream of new words doesn't regrow on
* each one. A failed grow drops this one transition -- the word itself
* already ran; there is nothing to refuse -- and is logged and counted. */
if (next_word_id >= metrics->transition_heat_cap) {
uint32_t new_cap = dict_size;
uint64_t *grown;
if (metrics->transition_heat_cap > new_cap / 2u) {
new_cap = (metrics->transition_heat_cap > UINT32_MAX / 2u)
? UINT32_MAX : metrics->transition_heat_cap * 2u;
}
grown = (uint64_t *)calloc(new_cap, sizeof(uint64_t));
if (!grown) {
g_transition_grow_failures++;
log_message(LOG_ERROR, "transition_heat can't grow: dropped");
return -1;
}
if (metrics->transition_heat) {
memcpy(grown, metrics->transition_heat,
(size_t)metrics->transition_heat_cap * sizeof(uint64_t));
free(metrics->transition_heat);
}
metrics->transition_heat = grown;
metrics->transition_heat_cap = new_cap;
}
/* Record the transition */
@@ -265,7 +291,7 @@ int64_t transition_metrics_get_probability_q48(const WordTransitionMetrics *metr
/* Safety: Bounds check on target_word_id to prevent out-of-bounds read
* Note: transition_heat is allocated with dict_size passed to transition_metrics_record()
* Callers must ensure target_word_id < dict_size used during allocation */
if (!metrics->transition_heat) {
if (!metrics->transition_heat || target_word_id >= metrics->transition_heat_cap) {
return 0;
}
@@ -311,8 +337,10 @@ void transition_metrics_update_cache(WordTransitionMetrics *metrics, uint32_t di
int64_t max_prob_q48 = 0;
uint32_t max_idx = 0;
/* Find word with highest transition count */
for (uint32_t i = 0; i < dict_size; i++) {
/* Find word with highest transition count. Bounded by the array's own
* length (S-1b): it may lag dict_size until the next grow. */
uint32_t limit = (dict_size < metrics->transition_heat_cap) ? dict_size : metrics->transition_heat_cap;
for (uint32_t i = 0; i < limit; i++) {
if (metrics->transition_heat[i] > 0) {
int64_t prob_q48 = transition_metrics_get_probability_q48(metrics, i);
if (prob_q48 > max_prob_q48) {
@@ -511,6 +539,7 @@ void transition_metrics_reset(WordTransitionMetrics *metrics) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
metrics->transition_heat_cap = 0;
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
@@ -559,6 +588,7 @@ void transition_metrics_cleanup(WordTransitionMetrics *metrics) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
metrics->transition_heat_cap = 0;
/* Phase 1 Extension: Clean up context window */
if (metrics->context_window) {
+11 -8
View File
@@ -705,7 +705,7 @@ void rolling_window_cleanup(RollingWindowOfTruth* window)
* that the first interactive command benefits from cache entries derived from
* real POST execution data rather than starting cold. The algorithm:
*
* 1. Allocates a zero-filled frequency array of @c DICTIONARY_SIZE @c uint32_t
* 1. Allocates a zero-filled frequency array of @c vm->next_word_id @c uint32_t
* counters via @c sf_calloc.
* 2. Obtains the current snapshot and tallies occurrences of each word ID
* over all @c ROLLING_WINDOW_SIZE history slots.
@@ -733,8 +733,11 @@ void rolling_window_seed_hotwords_cache(const RollingWindowOfTruth* window,
return; /* Window not ready or no cache */
}
/* Count frequency of each word in rolling window */
uint32_t *freq = (uint32_t *)sf_calloc(DICTIONARY_SIZE, sizeof(uint32_t));
/* Count frequency of each word in rolling window. Sized to the VM's
* word-id bound (S-1b: ids are no longer capped at DICTIONARY_SIZE). */
const uint32_t id_bound = vm->next_word_id;
if (id_bound == 0) return;
uint32_t *freq = (uint32_t *)sf_calloc(id_bound, sizeof(uint32_t));
if (!freq) return;
rolling_window_publish_snapshot_if_needed((RollingWindowOfTruth*)window);
@@ -749,7 +752,7 @@ void rolling_window_seed_hotwords_cache(const RollingWindowOfTruth* window,
for (uint32_t i = 0; i < ROLLING_WINDOW_SIZE; i++)
{
uint32_t word_id = view.history[i];
if (word_id < DICTIONARY_SIZE)
if (word_id < id_bound)
{
freq[word_id]++;
}
@@ -760,7 +763,7 @@ void rolling_window_seed_hotwords_cache(const RollingWindowOfTruth* window,
uint32_t sum_freq = 0;
uint32_t count_nonzero = 0;
for (uint32_t i = 0; i < DICTIONARY_SIZE; i++)
for (uint32_t i = 0; i < id_bound; i++)
{
if (freq[i] > 0)
{
@@ -773,7 +776,7 @@ void rolling_window_seed_hotwords_cache(const RollingWindowOfTruth* window,
uint32_t threshold = (count_nonzero > 0) ? (sum_freq / count_nonzero) : 1;
sf_mutex_lock(&vm->dict_lock);
for (uint32_t i = 0; i < DICTIONARY_SIZE; i++)
for (uint32_t i = 0; i < id_bound; i++)
{
if (freq[i] > threshold)
{
@@ -866,7 +869,7 @@ void rolling_window_seed_pipelining_context(const RollingWindowOfTruth* window,
uint32_t current_id = view.history[i];
uint32_t next_id = view.history[(i + 1) % ROLLING_WINDOW_SIZE];
if (current_id < DICTIONARY_SIZE && next_id < DICTIONARY_SIZE)
if (current_id < vm->next_word_id && next_id < vm->next_word_id) /* S-1b: was DICTIONARY_SIZE */
{
/* Lookup dictionary entry for current word by stable ID */
DictEntry *current_entry = NULL;
@@ -882,7 +885,7 @@ void rolling_window_seed_pipelining_context(const RollingWindowOfTruth* window,
context,
context_window_size,
next_id,
DICTIONARY_SIZE);
vm->next_word_id);
seeded_transitions++;
+61 -31
View File
@@ -34,7 +34,8 @@
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- diagnostic print only */
#include "vm.h" /* DICTIONARY_SIZE, WORD_ID_INVALID */
#include "vm.h" /* WORD_ID_INVALID */
#include "log.h" /* S-1b: a failed row grow is logged at LOG_ERROR */
/*
* StadiumWordSlot - the kernel-side word_id -> cell_index map (decided
@@ -56,19 +57,60 @@ typedef struct {
uint64_t last_decay_tick;
} StadiumWordSlot;
/* Row-per-quota-slot, kmalloc'd at stadium_words_init() to stadium_max_vm_
* count() rows of DICTIONARY_SIZE entries each -- replaces the old static
/* Row-per-quota-slot: stadium_max_vm_count() rows -- replaces the old static
* word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE] (2026-08-15: the VM
* count bound is computed from RAM, not a compile-time constant, so this
* can no longer be a flat static array). */
* can no longer be a flat static array).
*
* FABRIC-3.9.0.md §8.1 (Stage S-1b): each row is no longer DICTIONARY_SIZE
* entries -- word ids aren't capped any more. A row starts empty and grows
* (row_cap[slot]) when a word id at or past its length is dispatched. Grown
* at dispatch, not at definition: a slot can be granted after the VM's
* words exist, and slots are reused across VMs. A failed grow is logged and
* counted and the word is simply not admitted this dispatch -- the same,
* already-conserving outcome as a full Stadium. This is the id -> cell map,
* not the cell pool (S-2 stays open). */
static StadiumWordSlot **word_slots = (StadiumWordSlot **)0;
static uint32_t *row_cap = (uint32_t *)0;
static int words_initialized = 0;
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
static uint64_t row_grow_failures = 0;
uint64_t stadium_words_row_grow_failures(void) { return row_grow_failures; }
/* Grow `slot`'s row to cover word_id, at least doubling. All-or-nothing:
* the old row stays in place if the new one can't be allocated. */
static int grow_row(int slot, uint32_t word_id) {
uint32_t old_cap = row_cap[slot];
uint32_t new_cap = old_cap ? old_cap : 64u;
StadiumWordSlot *row;
uint32_t i;
while (new_cap <= word_id) {
new_cap = (new_cap > UINT32_MAX / 2u) ? UINT32_MAX : new_cap * 2u;
}
row = (StadiumWordSlot *)kmalloc((size_t)new_cap * sizeof(StadiumWordSlot));
if (!row) {
row_grow_failures++;
log_message(LOG_ERROR, "Stadium word row can't grow: not admitted");
return -1;
}
for (i = 0; i < old_cap; i++) {
row[i] = word_slots[slot][i];
}
for (; i < new_cap; i++) {
row[i].cell_index = STADIUM_CELL_NONE;
row[i].last_decay_tick = 0;
}
if (word_slots[slot]) kfree(word_slots[slot]);
word_slots[slot] = row;
row_cap[slot] = new_cap;
return 0;
}
void stadium_words_init(void) {
size_t slot;
uint32_t i;
size_t count = stadium_max_vm_count();
if (words_initialized) return; /* not re-entrant -- see stadium_words.h */
@@ -79,42 +121,26 @@ void stadium_words_init(void) {
}
word_slots = (StadiumWordSlot **)kmalloc(count * sizeof(StadiumWordSlot *));
row_cap = (uint32_t *)kmalloc(count * sizeof(uint32_t));
stat_promotions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(count * sizeof(uint64_t));
if (!word_slots || !stat_promotions || !stat_evictions) {
if (!word_slots || !row_cap || !stat_promotions || !stat_evictions) {
console_println("Stadium words: kmalloc failed for per-VM tables");
if (word_slots) kfree(word_slots);
if (row_cap) kfree(row_cap);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
row_cap = (uint32_t *)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
/* S-1b: rows start empty and grow on dispatch (grow_row()). */
for (slot = 0; slot < count; slot++) {
word_slots[slot] = (StadiumWordSlot *)kmalloc(DICTIONARY_SIZE * sizeof(StadiumWordSlot));
if (!word_slots[slot]) {
console_println("Stadium words: kmalloc failed for a word-slot row");
/* Free everything allocated so far, including this row's
* predecessors, and bail the same all-or-nothing way
* stadium_boot_init() does. */
{
size_t j;
for (j = 0; j < slot; j++) kfree(word_slots[j]);
}
kfree(word_slots);
kfree(stat_promotions);
kfree(stat_evictions);
word_slots = (StadiumWordSlot **)0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[slot][i].cell_index = STADIUM_CELL_NONE;
word_slots[slot][i].last_decay_tick = 0;
}
word_slots[slot] = (StadiumWordSlot *)0;
row_cap[slot] = 0;
stat_promotions[slot] = 0;
stat_evictions[slot] = 0;
}
@@ -189,7 +215,7 @@ uint64_t stadium_words_resident_heat(VMUuid vm_id) {
if (slot < 0) return 0;
cells = stadium_cells();
for (i = 0; i < DICTIONARY_SIZE; i++) {
for (i = 0; i < row_cap[slot]; i++) {
size_t cell = resolve_resident_cell(slot, i);
if (cell == STADIUM_CELL_NONE) continue;
sum += cells[cell].header.heat;
@@ -202,11 +228,13 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
if (word_id == WORD_ID_INVALID) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
if (word_id >= row_cap[slot] && grow_row(slot, word_id) != 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell != STADIUM_CELL_NONE) {
@@ -268,11 +296,13 @@ void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
if (word_id == WORD_ID_INVALID) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
if (word_id >= row_cap[slot]) return; /* never dispatched here -- can't be resident */
cell = resolve_resident_cell(slot, word_id);
if (cell == STADIUM_CELL_NONE) return;
+13 -4
View File
@@ -357,6 +357,15 @@ void vm_cleanup(VM* vm)
vm->latest = NULL;
}
/* S-1b: the stable-id tables are heap-grown now (dictionary_management.c). */
sf_free(vm->word_id_map);
sf_free(vm->recycled_word_ids);
vm->word_id_map = NULL;
vm->recycled_word_ids = NULL;
vm->word_id_capacity = 0;
vm->recycled_word_id_count = 0;
vm->next_word_id = 0;
if (vm->memory)
{
vm_host_free(vm, vm->memory);
@@ -780,7 +789,7 @@ void execute_colon_word(VM* vm)
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)
if (word_id < vm->next_word_id) /* S-1b: was DICTIONARY_SIZE */
{
/* L8 FINAL INTEGRATION: Loop #2 always-on - Rolling Window of Truth */
rolling_window_record_execution(&vm->rolling_window, word_id);
@@ -798,15 +807,15 @@ void execute_colon_word(VM* vm)
}
/* Record transition from previous word to current word */
transition_metrics_record(prev_word->transition_metrics, word_id, DICTIONARY_SIZE);
transition_metrics_record(prev_word->transition_metrics, word_id, vm->next_word_id);
/* Update probability cache to find most likely next word */
transition_metrics_update_cache(prev_word->transition_metrics, DICTIONARY_SIZE);
transition_metrics_update_cache(prev_word->transition_metrics, vm->next_word_id);
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 &&
if (speculated_word_id < vm->next_word_id &&
transition_metrics_should_speculate(prev_word->transition_metrics, speculated_word_id))
{
/* Speculation decision: the most likely next word has high probability
+9
View File
@@ -418,6 +418,15 @@ void vm_cleanup(VM* vm)
vm->latest = NULL;
}
/* S-1b: the stable-id tables are heap-grown now (dictionary_management.c). */
sf_free(vm->word_id_map);
sf_free(vm->recycled_word_ids);
vm->word_id_map = NULL;
vm->recycled_word_ids = NULL;
vm->word_id_capacity = 0;
vm->recycled_word_id_count = 0;
vm->next_word_id = 0;
if (vm->memory)
{
sf_free(vm->memory);
@@ -206,7 +206,7 @@ static void forth_PIPELINING_SHOW_TOP_TRANSITIONS(VM *vm) {
memset(top, 0, sizeof(TransitionPair) * top_count);
/* Scan all words and keep top N */
for (uint32_t i = 0; i < DICTIONARY_SIZE; i++) {
for (uint32_t i = 0; i < m->transition_heat_cap; i++) {
uint64_t count = m->transition_heat[i];
if (count == 0) continue;
@@ -373,9 +373,10 @@ static void forth_PIPELINING_ANALYZE_WORD(VM *vm) {
printf("=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "\n");
/* Calculate entropy / distribution spread */
double avg_count = (double)m->total_transitions / DICTIONARY_SIZE;
/* S-1b: the id space is vm->next_word_id, no longer a fixed DICTIONARY_SIZE. */
double avg_count = (double)m->total_transitions / (vm->next_word_id ? vm->next_word_id : 1u);
double max_count = 0;
for (uint32_t i = 0; i < DICTIONARY_SIZE; i++) {
for (uint32_t i = 0; i < m->transition_heat_cap; i++) {
if (m->transition_heat && m->transition_heat[i] > max_count) {
max_count = (double)m->transition_heat[i];
}