Files
LithosAnanake/src/dictionary_management.c
T
Robert Allan JamesandClaude Sonnet 5 5a28458b21 starkernel: item 4.2 -- Hermes native on the Stadium (complete)
Migrates Hermes's message/channel lifecycle onto the Stadium's unified
heat/capacity economy: MSG-ALLOC/FREE-NODE and CH-ALLOC/FREE-NODE now
route entirely through stadium_admit()/stadium_evict(), replacing the
old local free-list + independent heat-field mechanism. Eight
kernel-only STADIUM-* FORTH primitives (ADMIT, EVICT, RES@, RES-PULL,
RES-PUSH, HEAT@, HEAT!, WORD-HEAT), VM.stadium_vm_id threaded through
all three vm_core.c dispatch sites (replacing item 4.1's hardcoded
vm_uuid_hera()), and the stadium_owner[idx] fix so evict-credit lands
in the VM that actually admitted a patron, not whoever owned cell 0.

This session's own contribution, on top of that pre-existing
implementation: found and fixed two bugs blocking the item's own K≡1.0
conservation self-check (HERMES-K was reading 0, not 65536):

- Q.SLOT admission-heat fix (capsules/hermes/init.4th): MSG-SEND/
  CH-ACCEPT admitted with Q.1 (the entire fleet-wide "1.0" unit) per
  item, a leftover from before the Stadium migration when each
  message/channel had its own unconstrained heat field. Instantly
  drained the shared, finite reservoir.

- Reservoir floor for word-execution admission (stadium_words.c):
  stadium_word_dispatch() (item 4.1) pulls STADIUM_WORD_HEAT_QUANTUM on
  every word dispatch, not just first admission -- exhausts a VM's
  entire reservoir in ~32 dispatches, starving any application-level
  economy sharing that VM's reservoir before it gets a chance to pull
  anything. word_dispatch_pull() now clamps word-execution's own pulls
  to leave a Q48_ONE/3 floor (same fair-share figure COMMON-CH's own
  floor already uses); application-level pulls are unaffected.

- STADIUM-WORD-HEAT primitive + stadium_words_resident_heat(): the
  floor deliberately leaves word-execution residents holding real
  heat, invisible to HERMES-K's original formula (MSG+CH+reservoir,
  no term for word patrons). Adding this term closes K to exactly
  65536 on all three architectures.

Also rules on two open scope questions in FABRIC.md: MBR-ALLOC/
MBR-FREE-NODE stay off the Stadium (membership records have no heat
field, never did -- the acceptance bullet's inclusion of them was a
completeness gesture predating a check of the actual layout), and
records the effort number (12 implementation files, +759/-120 lines).

Verified: all three architectures boot clean, full self-test passes,
Stadium conservation closes exactly (resident_sum + reservoir =
Q48_ONE) at both the C/Stadium level and the FORTH-level HERMES-K
check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:49:23 -04:00

584 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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) 20232025 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 dictionary_management.c — FORTH-79 + ANSI C99
* Optimized: incremental FC index + capacity reuse + newest-first search
*/
#include "../include/vm.h"
#include "../include/io.h"
#include "../include/log.h"
#include "../include/physics_metadata.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/physics_pipelining_metrics.h"
#include "../include/dictionary_heat_optimization.h"
#include "../include/platform_alloc.h"
#include <string.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium_words.h" /* item 4.1: FORGET coherence hook */
#include "starkernel/vm_uuid.h" /* vm_uuid_hera() */
#endif
/* LIKELY/UNLIKELY macros are defined in vm.h */
#ifndef SF_FC_BUCKETS
#define SF_FC_BUCKETS 256
#endif
/* Buckets of entry pointers, sizes, and capacities (we reuse memory). */
/* Note: sf_fc_list, sf_fc_count, and sf_fc_cap are not static to allow access from
* dictionary_heat_optimization.c for heat-aware bucket reorganization with proper
* capacity checks (ASan fix: 2025-12-09). */
DictEntry **sf_fc_list[SF_FC_BUCKETS];
size_t sf_fc_count[SF_FC_BUCKETS];
size_t sf_fc_cap[SF_FC_BUCKETS];
static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
static uint32_t vm_dictionary_acquire_word_id(VM *vm) {
if (!vm) {
return WORD_ID_INVALID;
}
if (vm->recycled_word_id_count > 0) {
return vm->recycled_word_ids[--vm->recycled_word_id_count];
}
if (vm->next_word_id < DICTIONARY_SIZE) {
return vm->next_word_id++;
}
return WORD_ID_INVALID;
}
void vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
if (!vm || !entry) {
return;
}
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'",
(int)entry->name_len, entry->name);
return;
}
vm->word_id_map[word_id] = entry;
}
void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
if (!vm || !entry) {
return;
}
/* Cache coherence: the entry is about to be removed (FORGET frees it) —
* a stale pointer left in the hot-words ring would be a use-after-free,
* and an older same-named word may become visible again.
* §17.3 (item 4.1): retired under __STARKERNEL__ -- the kernel word
* layer stays inert to this mechanism entirely, so there is nothing to
* keep coherent here on the kernel side. Hosted is unaffected. */
#ifndef __STARKERNEL__
hotwords_cache_evict_entry(vm->hotwords_cache, entry);
#endif
uint32_t word_id = entry->word_id;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) {
return;
}
#ifdef __STARKERNEL__
/* item 4.1 coherence hook: if this word_id is resident on the Stadium,
* reclaim its cell (crediting heat back to the reservoir) BEFORE the id
* is recycled below -- otherwise the next word assigned this same id
* would alias onto the forgotten word's stale cell (same failure class
* as the 2026-08-02 block_words.c aliasing bug). */
stadium_word_forget(vm->stadium_vm_id, word_id);
#endif
if (vm->word_id_map[word_id] == entry) {
vm->word_id_map[word_id] = NULL;
}
if (vm->recycled_word_id_count < DICTIONARY_SIZE) {
vm->recycled_word_ids[vm->recycled_word_id_count++] = word_id;
}
entry->word_id = WORD_ID_INVALID;
}
DictEntry *vm_dictionary_lookup_by_word_id(VM *vm, uint32_t word_id) {
if (!vm || word_id >= DICTIONARY_SIZE) {
return NULL;
}
return vm->word_id_map[word_id];
}
/* --- helpers ------------------------------------------------------------- */
static inline void *sf_xrealloc(void *p, size_t nbytes) {
void *r = sf_realloc(p, nbytes);
if (!r) {
sf_free(p);
return NULL;
}
return r;
}
static void sf_fc_clear_all(void) {
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
sf_free(sf_fc_list[i]);
sf_fc_list[i] = NULL;
sf_fc_count[i] = 0;
sf_fc_cap[i] = 0;
}
}
/* Ensure bucket i has at least `need` capacity; grow geometrically. */
static int sf_fc_reserve(size_t i, size_t need) {
if (sf_fc_cap[i] >= need) return 1;
size_t newcap = sf_fc_cap[i] ? sf_fc_cap[i] : 8;
while (newcap < need) newcap <<= 1;
DictEntry **old = sf_fc_list[i];
DictEntry **newv = (DictEntry **) sf_xrealloc(old, newcap * sizeof(*newv));
if (!newv) return 0;
/* Kernel sf_realloc is a bump allocator that does not copy; patch that up.
* On hosted Linux, realloc already copied before freeing old — do NOT memcpy
* from freed memory (UB; glibc overwrites freed block with heap metadata). */
#ifdef __STARKERNEL__
{
size_t old_count = sf_fc_count[i];
if (newv != old && old && old_count > 0)
memcpy(newv, old, old_count * sizeof(*newv));
}
#endif
sf_fc_list[i] = newv;
sf_fc_cap[i] = newcap;
return 1;
}
/* Full rebuild: count → reserve → fill oldest→newest (so backward scan = newest-first). */
static void sf_rebuild_fc_index(VM *vm) {
/* Reset counts, keep capacity to avoid churn. */
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_fc_count[i] = 0;
/* Count first chars. */
for (DictEntry *e = vm->latest; e; e = e->link) {
unsigned fc = (unsigned char) e->name[0];
sf_fc_count[fc]++;
}
/* Reserve memory once per bucket. */
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
if (sf_fc_count[i] && !sf_fc_reserve(i, sf_fc_count[i])) {
/* OOM for this bucket → treat as empty; well just miss the fast path. */
sf_fc_count[i] = 0;
}
}
/* Back-fill so [0]=oldest, [count-1]=newest. */
size_t fill_at[SF_FC_BUCKETS];
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = sf_fc_count[i];
for (DictEntry *e = vm->latest; e; e = e->link) {
unsigned fc = (unsigned char) e->name[0];
if (sf_fc_count[fc] == 0 || !sf_fc_list[fc]) continue;
sf_fc_list[fc][--fill_at[fc]] = e;
}
sf_cached_latest = vm->latest;
}
/* Fast path: if exactly one new word was pushed, append it without rebuilding. */
static void sf_try_fast_append(VM *vm) {
if (!vm || vm->latest == sf_cached_latest) return;
DictEntry *newest = vm->latest;
/* single push if the new head links to the previously cached head */
if (newest && newest->link == sf_cached_latest) {
unsigned fc = (unsigned char) newest->name[0];
size_t n = sf_fc_count[fc];
if (sf_fc_reserve(fc, n + 1)) {
/* Keep oldest→…→newest ordering: append at the end. */
sf_fc_list[fc][n] = newest;
sf_fc_count[fc] = n + 1;
sf_cached_latest = newest;
return;
}
}
/* Otherwise fall back to a full rebuild (multiple changes, deletes, etc.). */
sf_rebuild_fc_index(vm);
}
/* --- API ----------------------------------------------------------------- */
/**
* @brief Return whichever of two entries is the more recent definition.
*
* Walks the definition chain from @c vm->latest; the first of (@p a, @p b)
* encountered is the newer one. The link chain is the only reliable age
* order — word_id values are recycled and bucket positions are shuffled by
* @c dict_reorganize_buckets_by_heat(), so neither can arbitrate age.
* Only invoked when a bucket holds two visible entries with the same name
* (redefinition/shadowing), so the walk cost is off the common path.
*/
static DictEntry *dict_newer_of(VM *vm, DictEntry *a, DictEntry *b) {
for (DictEntry *e = vm->latest; e; e = e->link) {
if (e == a) return a;
if (e == b) return b;
}
return a;
}
DictEntry *vm_dict_resolve_in_bucket(VM *vm, DictEntry **bucket, size_t n,
const char *name, size_t len) {
if (!vm || !bucket || n == 0 || !name || len == 0) return NULL;
const unsigned char last = (len > 1) ? (unsigned char) name[len - 1] : 0;
DictEntry *best = NULL;
/* Full scan: bucket order is heat-shuffled, so no prefix of the bucket
* is authoritative — every same-named candidate must be considered and
* the newest visible definition wins (FORTH-79 shadowing). */
for (size_t i = n; i-- > 0;) {
DictEntry *e = bucket[i];
if (!e) continue;
if ((size_t) e->name_len != len) continue;
#ifdef WORD_HIDDEN
if (UNLIKELY(e->flags & WORD_HIDDEN)) continue;
#endif
#ifdef WORD_SMUDGED
if (UNLIKELY(e->flags & WORD_SMUDGED)) continue;
#endif
if (len > 1 && (unsigned char) e->name[len - 1] != last) continue;
if (memcmp(e->name, name, len) != 0) continue;
best = best ? dict_newer_of(vm, best, e) : e;
}
return best;
}
/* Find by name: newest-first within the first-character bucket. */
/**
* @brief Finds a word in the dictionary by name
*
* Searches for a word in the dictionary using an optimized first-character index.
* The search is performed newest-first within the matching first-character bucket.
*
* @param vm The virtual machine context
* @param name The name to search for
* @param len Length of the name string
* @return DictEntry* Pointer to found dictionary entry or NULL if not found
*/
DictEntry *vm_find_word(VM *vm, const char *name, size_t len) {
if (UNLIKELY(!vm || !name || len == 0)) return NULL;
/* DoE counter: dictionary lookups */
vm->heartbeat.dictionary_lookups++;
/* Thread safety: Acquire dict_lock for read access to dictionary */
sf_mutex_lock(&vm->dict_lock);
/* Keep the index in sync with dictionary head, cheaply if possible. */
if (UNLIKELY(sf_cached_latest != vm->latest)) sf_try_fast_append(vm);
const unsigned char first = (unsigned char) name[0];
DictEntry **bucket = sf_fc_list[first];
size_t n = sf_fc_count[first];
if (UNLIKELY(!bucket || n == 0)) {
sf_mutex_unlock(&vm->dict_lock);
return NULL;
}
/* Use hot-words cache for physics-driven frequency-based acceleration.
* The cache's bucket fallback resolves via vm_dict_resolve_in_bucket(),
* so a non-NULL result already honors newest-first shadowing.
* §17.3 (item 4.1): bypassed under __STARKERNEL__ -- the kernel side
* feeds the Stadium instead (vm_core.c's dispatch sites), and this
* lookup fast path is retired in favor of the ordinary bucket scan
* below. Hosted is unaffected. */
#ifndef __STARKERNEL__
DictEntry *found = hotwords_cache_lookup(vm, vm->hotwords_cache, bucket, n, name, len);
if (found) {
sf_mutex_unlock(&vm->dict_lock);
return found;
}
#endif
/* Phase 2: Choose lookup strategy based on pattern diversity */
if (vm->lookup_strategy == 1) {
/* Heat-aware lookup: search by heat percentiles (hot first) */
DictEntry *result = dict_find_word_heat_aware(vm, name, len);
sf_mutex_unlock(&vm->dict_lock);
return result;
}
/* Fallback/Default: arbitrated scan — buckets may be heat-reordered, so
* first-match-wins is unsound; the resolver picks the newest visible
* definition among all same-named candidates. */
DictEntry *e = vm_dict_resolve_in_bucket(vm, bucket, n, name, len);
sf_mutex_unlock(&vm->dict_lock);
return e;
}
/* Create a new word (unchanged layout); index append happens lazily on next lookup. */
/**
* @brief Creates a new word in the dictionary
*
* Allocates and initializes a new dictionary entry with the given name and function.
* The word is added to the dictionary but index update is deferred until next lookup.
*
* @param vm The virtual machine context
* @param name Name for the new word
* @param len Length of the name string
* @param func Function pointer for the word's behavior
* @return DictEntry* Pointer to new dictionary entry or NULL on failure
*/
DictEntry *vm_create_word(VM *vm, const char *name, size_t len, word_func_t func) {
if (!vm || !name || len == 0 || len > WORD_NAME_MAX) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_create_word: invalid parameters (vm=%p, name=%p, len=%zu, max=%d)",
(void *) vm, (const void *) name, len, WORD_NAME_MAX);
}
return NULL;
}
DictEntry *entry = NULL;
int lock_held = 0;
sf_mutex_lock(&vm->dict_lock);
lock_held = 1;
/* Pin is permanent: no word may shadow a pinned entry — ever, by anyone */
for (DictEntry *scan = vm->latest; scan; scan = scan->link) {
if (scan->acl_pinned && scan->name_len == (uint8_t)len &&
memcmp(scan->name, name, len) == 0) {
vm->error = 1;
log_message(LOG_WARN, "vm_create_word: cannot shadow pinned word '%.*s'",
(int)len, name);
entry = NULL;
goto create_cleanup;
}
}
size_t base = offsetof(DictEntry, name);
size_t name_bytes = len + 1;
size_t align = sizeof(cell_t);
size_t df_off = (base + name_bytes + (align - 1)) & ~(align - 1);
size_t total = df_off + sizeof(cell_t);
entry = (DictEntry *) sf_malloc(total);
if (!entry) {
vm->error = 1;
log_message(LOG_ERROR, "vm_create_word: malloc failed for '%.*s' (%zu bytes)",
(int) len, name, total);
entry = NULL;
goto create_cleanup;
}
entry->link = vm->latest;
entry->func = func;
entry->flags = 0;
entry->name_len = (uint8_t) len;
entry->execution_heat = 0; /* Initialize execution heat counter */
entry->acl_default = ACL_USER_DEFAULT;
entry->acl_ttl = 0; /* first execution always rechecks */
entry->acl_allow = 1; /* permissive until ACL.4th loads */
entry->acl_mode = ACL_MODE_TTL;
entry->acl_pinned = 0;
entry->word_id = WORD_ID_INVALID;
uint32_t header_bytes = (total > UINT32_MAX) ? UINT32_MAX : (uint32_t) total;
physics_metadata_init(entry, header_bytes);
physics_metadata_apply_seed(entry);
/* Initialize word transition metrics for pipelining (Phase 1) */
entry->transition_metrics = (WordTransitionMetrics *)sf_malloc(sizeof(WordTransitionMetrics));
if (entry->transition_metrics) {
transition_metrics_init(entry->transition_metrics);
} else {
log_message(LOG_WARN, "vm_create_word: transition metrics malloc failed for '%.*s'", (int)len, name);
}
memcpy(entry->name, name, len);
entry->name[len] = '\0';
if (df_off > base + name_bytes) {
memset(((uint8_t *) entry) + base + name_bytes, 0, df_off - (base + name_bytes));
}
*(cell_t *) (((uint8_t *) entry) + df_off) = 0;
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
* next lookup re-resolves through the arbitrated bucket scan.
* §17.3 (item 4.1): retired under __STARKERNEL__, same as the other
* hotwords_cache_* call sites in this file. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache, name, len);
#endif
log_message(LOG_DEBUG, "vm_create_word: '%.*s' len=%zu total=%zu @%p",
(int) len, name, len, total, (void *) entry);
create_cleanup:
if (lock_held)
sf_mutex_unlock(&vm->dict_lock);
return entry;
}
/* Linear scan by func (left as-is; usually cold path). */
/**
* @brief Finds a word by its function pointer
*
* Performs a linear scan of the dictionary to find an entry with matching function.
*
* @param vm The virtual machine context
* @param func Function pointer to search for
* @return DictEntry* Pointer to found dictionary entry or NULL if not found
*/
DictEntry *vm_dictionary_find_by_func(VM *vm, word_func_t func) {
for (DictEntry *e = vm ? vm->latest : NULL; e; e = e->link) {
if (e->func == func) return e;
}
return NULL;
}
DictEntry *vm_dictionary_find_latest_by_func(VM *vm, word_func_t func) {
return vm_dictionary_find_by_func(vm, func);
}
/**
* @brief Gets pointer to a word's data field
*
* Calculates and returns pointer to the aligned data field following the name field.
*
* @param entry Dictionary entry to get data field from
* @return cell_t* Pointer to data field or NULL if entry is NULL
*/
cell_t *vm_dictionary_get_data_field(DictEntry *entry) {
if (!entry) return NULL;
size_t base = offsetof(DictEntry, name);
size_t name_bytes = (size_t) entry->name_len + 1;
size_t align = sizeof(cell_t);
size_t df_off = (base + name_bytes + (align - 1)) & ~(align - 1);
return (cell_t *) (((uint8_t *) entry) + df_off);
}
/**
* @brief Marks the latest word as hidden
*
* Sets the WORD_HIDDEN flag on the most recently defined word.
*
* @param vm The virtual machine context
*/
void vm_hide_word(VM *vm) {
if (vm && vm->latest) {
vm->latest->flags |= WORD_HIDDEN;
physics_metadata_refresh_state(vm->latest);
/* Visibility changed: this name may now resolve to an older entry.
* §17.3 (item 4.1): retired under __STARKERNEL__. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache,
vm->latest->name, vm->latest->name_len);
#endif
}
}
void vm_smudge_word(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_smudge_word: invalid vm or no latest word");
}
return;
}
vm->latest->flags ^= WORD_SMUDGED;
physics_metadata_refresh_state(vm->latest);
/* Visibility changed in either direction: force re-resolution.
* §17.3 (item 4.1): retired under __STARKERNEL__. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache,
vm->latest->name, vm->latest->name_len);
#endif
}
void vm_pin_execution_heat(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_pin_execution_heat: invalid vm or no latest word");
}
return;
}
vm->latest->flags |= WORD_PINNED;
physics_metadata_refresh_state(vm->latest);
}
void vm_unpin_execution_heat(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_unpin_execution_heat: invalid vm or no latest word");
}
return;
}
vm->latest->flags &= ~WORD_PINNED;
physics_metadata_refresh_state(vm->latest);
}
/* If you ever need to hard-reset (e.g., switching vocabularies wholesale) */
/**
* @brief Resets the dictionary index
*
* Clears all index data and cached state. Used when switching vocabularies.
*/
void vm_dictionary_index_reset(void) {
sf_cached_latest = NULL;
sf_fc_clear_all();
}