Move vocabulary and control-flow state off file-scope statics onto VM

proof/FINDINGS.md's Isabelle/HOL word-source sweep (§1) found the two
defects severe enough to actively corrupt the live Tripod multi-VM fleet:
file-scope C statics standing in for state that belongs on struct VM.

- vocabulary_words.c (highest severity in the sweep): forth_vocab/
  context_vocab/current_vocab, context_var_addr/current_var_addr, the
  ctx_fc/forth_fc first-char search index, and the `initialized` guard
  were all process-wide statics. Only the first VM to touch any
  vocabulary word ever ran setup; every VM after that silently shared
  VM #1's dictionary-chain pointers and reused VM #1's byte-offset
  addresses as if valid in its own vm->memory. One VM's VOCABULARY/
  DEFINITIONS/FORTH silently changed where every other VM looked up and
  defined words.

- control_words.c: cf_stack/cf_sp/cf_last_mode (IF/THEN/BEGIN/DO/CASE
  compile-time nesting) and the LEAVE/ENDOF patch-site bookkeeping
  (leave_addrs/leave_sp/leave_mark_*, endof_addrs/endof_sp/endof_mark_*)
  were also process-wide statics. Two VMs compiling colon definitions at
  overlapping times would corrupt each other's nesting state.

Both moved onto struct VM, following the existing hold_addr/hold_pos
precedent in include/vm.h ("lives in each VM's own memory... so child
VMs never alias Hera's buffer"):

- New VocabularyState struct (vm->vocab): chain heads, VM-cell addresses,
  first-char index, initialized flag.
- New ControlFlowState struct (vm->cf): cf_stack/cf_sp/cf_last_mode plus
  the LEAVE/ENDOF patch-site stacks. cf_tag_t/cf_item_t/CF_STACK_MAX
  moved from control_words.c into include/vm.h since they're now part of
  the struct VM field's type.
- Sentinel fields (-1/-999, meaning "empty") explicitly initialized in
  both vm_init_with_host() implementations (hosted src/vm_bootstrap.c and
  kernel src/starkernel/vm/vm_bootstrap.c) alongside the existing
  dsp/rsp = -1 initialization, since the preceding zero-init leaves them
  at 0 rather than their empty sentinel.

Every word function in both files already took VM *vm, so no call sites
outside these two files needed to change; cf_push_item/cf_pop_item/
cf_peek_item gained a VM* parameter to reach vm->cf.

Verified: hosted (amd64) and kernel (amd64, __STARKERNEL__) both build
clean with -Wall -Werror after a full clean rebuild (struct VM's layout
changed size, and this Makefile has no header-dependency tracking, so a
stale incremental build would have linked mismatched object layouts).
Hosted POST suite 1012/1012 passing (0 regressions). Manually exercised
VOCABULARY/DEFINITIONS/FORTH/ORDER, and IF/ELSE, DO/LOOP/LEAVE,
BEGIN/WHILE/REPEAT, and CASE/OF/ENDOF/ENDCASE (including nested DO with
I/J) in the REPL -- all correct and unchanged from pre-refactor behavior.

Note: a pre-existing CASE/ENDCASE default-clause bug (the code after the
last OF...ENDOF pair does not correctly become the "default" value once
DROP runs) was found while testing this refactor and confirmed present
on unmodified master too -- not touched here, out of scope for this pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qf6YcnHgaEtEygq3knx19
This commit is contained in:
Claude
2026-09-05 14:07:41 +00:00
parent c36bd99e1e
commit d6895bdd15
6 changed files with 230 additions and 173 deletions
+45 -52
View File
@@ -91,19 +91,13 @@ static size_t vocab_safe_len(const char *text, size_t max_len)
}
/* ---- first-character index for vocab chains (lazy rebuild) ---- */
#define SF_FC_BUCKETS 256
static DictEntry **ctx_fc[SF_FC_BUCKETS]; /* arrays of entry pointers */
static size_t ctx_n[SF_FC_BUCKETS];
static DictEntry *ctx_cached_head = NULL;
static DictEntry **forth_fc[SF_FC_BUCKETS];
static size_t forth_n[SF_FC_BUCKETS];
static DictEntry *forth_cached_head = NULL;
/* ---- first-character index for vocab chains (lazy rebuild) ----
* Bucket arrays themselves live per-VM (VM.vocab.ctx_fc / .forth_fc,
* declared in include/vm.h as VocabularyState); these two helpers just
* operate on whichever VM's arrays are passed in. */
static void fc_free(DictEntry ***lists, size_t *counts) {
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
for (size_t i = 0; i < VOCAB_FC_BUCKETS; ++i) {
free(lists[i]);
lists[i] = NULL;
counts[i] = 0;
@@ -119,14 +113,14 @@ static void fc_rebuild(DictEntry *head, DictEntry ***lists, size_t *counts, Dict
counts[c]++;
}
/* alloc */
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
for (size_t i = 0; i < VOCAB_FC_BUCKETS; ++i) {
if (counts[i]) {
lists[i] = (DictEntry **) malloc(counts[i] * sizeof(DictEntry *));
if (!lists[i]) counts[i] = 0; /* malloc failed: zero count so fill skips */
}
}
/* second pass: fill oldest→newest (we’ll search newest-first by iterating backwards) */
size_t filled[SF_FC_BUCKETS] = {0};
size_t filled[VOCAB_FC_BUCKETS] = {0};
for (DictEntry *e = head; e; e = e->link) {
unsigned c = (unsigned char) e->name[0];
if (lists[c]) lists[c][filled[c]++] = e; /* skip null buckets */
@@ -141,21 +135,12 @@ static void fc_rebuild(DictEntry *head, DictEntry ***lists, size_t *counts, Dict
- No ALSO/ONLY/PREVIOUS here. Period.
*/
/* Host-side state */
static DictEntry *forth_vocab = NULL; /* FORTH vocabulary head (root of system) */
static DictEntry *context_vocab = NULL; /* CONTEXT vocabulary head */
static DictEntry *current_vocab = NULL; /* CURRENT vocabulary head */
/* VM-visible variables (addresses in VM space) */
static vaddr_t context_var_addr = 0; /* cell containing (DictEntry*) CONTEXT */
static vaddr_t current_var_addr = 0; /* cell containing (DictEntry*) CURRENT */
/* Sync host state -> VM cells */
static inline void vocab_sync_vm_vars(VM *vm) {
if (!context_var_addr || !current_var_addr) return;
if (!vm->vocab.context_var_addr || !vm->vocab.current_var_addr) return;
#if defined(__STARKERNEL__) && SK_PARITY_DEBUG
uint64_t cv = (uint64_t)(uintptr_t)context_vocab;
uint64_t rv = (uint64_t)(uintptr_t)current_vocab;
uint64_t cv = (uint64_t)(uintptr_t)vm->vocab.context_vocab;
uint64_t rv = (uint64_t)(uintptr_t)vm->vocab.current_vocab;
uint64_t base = (uintptr_t)vm->memory;
console_puts("[VOC_SYNC] context=");
@@ -163,13 +148,13 @@ static inline void vocab_sync_vm_vars(VM *vm) {
console_puts(" current=");
vocabulary_debug_print_hex(rv);
console_puts(" context_addr=");
vocabulary_debug_print_hex((uint64_t)context_var_addr);
vocabulary_debug_print_hex((uint64_t)vm->vocab.context_var_addr);
console_puts(" current_addr=");
vocabulary_debug_print_hex((uint64_t)current_var_addr);
vocabulary_debug_print_hex((uint64_t)vm->vocab.current_var_addr);
console_puts(" vm_base=");
vocabulary_debug_print_hex(base);
console_puts(" context_vocab=");
vocabulary_debug_print_hex((uint64_t)(uintptr_t)context_vocab);
vocabulary_debug_print_hex((uint64_t)(uintptr_t)vm->vocab.context_vocab);
console_println("");
/* Truncation check */
@@ -179,7 +164,7 @@ static inline void vocab_sync_vm_vars(VM *vm) {
console_println("");
sk_hal_panic("context pointer truncated");
}
/* Canonical check */
if (!sf_is_canonical(cv)) {
console_puts("PANIC: context pointer non-canonical: ");
@@ -191,23 +176,25 @@ static inline void vocab_sync_vm_vars(VM *vm) {
/* NOTE: DictEntry* (vocabulary pointers) live in the kernel heap, not the
* VM arena. An arena-range check here is always wrong — omitted. */
#endif
vm_store_cell(vm, context_var_addr, (cell_t)(uintptr_t)context_vocab);
vm_store_cell(vm, current_var_addr, (cell_t)(uintptr_t)current_vocab);
vm_store_cell(vm, vm->vocab.context_var_addr, (cell_t)(uintptr_t)vm->vocab.context_vocab);
vm_store_cell(vm, vm->vocab.current_var_addr, (cell_t)(uintptr_t)vm->vocab.current_vocab);
}
/**
* @brief Initialize the FORTH vocabulary system
* @details Sets up FORTH as root vocabulary and allocates VM cells for CONTEXT/CURRENT
* @details Sets up FORTH as root vocabulary and allocates VM cells for CONTEXT/CURRENT.
* Per-VM (vm->vocab.initialized): each VM in the fleet seeds its own
* vocabulary roots from its own dictionary, rather than every VM after
* the first silently sharing whichever VM ran this first.
* @param vm Pointer to VM instance
*/
static void init_vocabulary_system(VM *vm) {
static int initialized = 0;
if (initialized) return;
if (vm->vocab.initialized) return;
/* Treat vm->latest as the FORTH vocabulary head */
forth_vocab = vm->latest;
context_vocab = forth_vocab;
current_vocab = forth_vocab;
vm->vocab.forth_vocab = vm->latest;
vm->vocab.context_vocab = vm->vocab.forth_vocab;
vm->vocab.current_vocab = vm->vocab.forth_vocab;
/* Allocate VM cells for CONTEXT and CURRENT */
void *p1 = vm_allot(vm, sizeof(cell_t));
@@ -216,7 +203,7 @@ static void init_vocabulary_system(VM *vm) {
log_message(LOG_ERROR, "VOCAB: failed CONTEXT cell");
return;
}
context_var_addr = (vaddr_t)((uint8_t *) p1 - vm->memory);
vm->vocab.context_var_addr = (vaddr_t)((uint8_t *) p1 - vm->memory);
void *p2 = vm_allot(vm, sizeof(cell_t));
if (!p2) {
@@ -224,10 +211,10 @@ static void init_vocabulary_system(VM *vm) {
log_message(LOG_ERROR, "VOCAB: failed CURRENT cell");
return;
}
current_var_addr = (vaddr_t)((uint8_t *) p2 - vm->memory);
vm->vocab.current_var_addr = (vaddr_t)((uint8_t *) p2 - vm->memory);
vocab_sync_vm_vars(vm);
initialized = 1;
vm->vocab.initialized = 1;
}
/* Finder: search CONTEXT chain first, then FORTH chain; skip hidden/smudged */
@@ -237,16 +224,18 @@ static DictEntry *vocab_find_word(VM *vm, const char *name, size_t len) {
if (!name || len == 0) return NULL;
/* rebuild per-vocab indices if heads changed */
if (ctx_cached_head != context_vocab) fc_rebuild(context_vocab, ctx_fc, ctx_n, &ctx_cached_head);
if (forth_cached_head != forth_vocab) fc_rebuild(forth_vocab, forth_fc, forth_n, &forth_cached_head);
if (vm->vocab.ctx_cached_head != vm->vocab.context_vocab)
fc_rebuild(vm->vocab.context_vocab, vm->vocab.ctx_fc, vm->vocab.ctx_n, &vm->vocab.ctx_cached_head);
if (vm->vocab.forth_cached_head != vm->vocab.forth_vocab)
fc_rebuild(vm->vocab.forth_vocab, vm->vocab.forth_fc, vm->vocab.forth_n, &vm->vocab.forth_cached_head);
const unsigned char first = (unsigned char) name[0];
const unsigned char last = (unsigned char) name[len - 1];
/* search CONTEXT bucket (newest-first) */
{
DictEntry **bucket = ctx_fc[first];
size_t n = ctx_n[first];
DictEntry **bucket = vm->vocab.ctx_fc[first];
size_t n = vm->vocab.ctx_n[first];
if (UNLIKELY(!bucket || n == 0)) goto skip_ctx;
for (size_t i = n; i-- > 0;) {
DictEntry *e = bucket[i];
@@ -266,9 +255,9 @@ static DictEntry *vocab_find_word(VM *vm, const char *name, size_t len) {
skip_ctx:;
/* then FORTH bucket (if different) */
if (context_vocab != forth_vocab) {
DictEntry **bucket = forth_fc[first];
size_t n = forth_n[first];
if (vm->vocab.context_vocab != vm->vocab.forth_vocab) {
DictEntry **bucket = vm->vocab.forth_fc[first];
size_t n = vm->vocab.forth_n[first];
if (UNLIKELY(!bucket || n == 0)) goto skip_forth;
for (size_t i = n; i-- > 0;) {
DictEntry *e = bucket[i];
@@ -299,7 +288,7 @@ static void vocabulary_select_runtime(VM *vm) {
* Fall back to vm->latest only if somehow NULL. */
DictEntry *selected = vm->current_executing_entry;
if (!selected) selected = vm->latest;
context_vocab = selected;
vm->vocab.context_vocab = selected;
vocab_sync_vm_vars(vm);
log_message(LOG_DEBUG, "Vocabulary selected (CONTEXT updated)");
@@ -446,7 +435,7 @@ void vocabulary_create_vocabulary_direct(VM *vm, const char *name)
*/
void vocabulary_word_definitions(VM *vm) {
init_vocabulary_system(vm);
current_vocab = context_vocab;
vm->vocab.current_vocab = vm->vocab.context_vocab;
vocab_sync_vm_vars(vm);
log_message(LOG_DEBUG, "DEFINITIONS: CURRENT := CONTEXT");
}
@@ -459,7 +448,7 @@ void vocabulary_word_definitions(VM *vm) {
*/
void vocabulary_word_context(VM *vm) {
init_vocabulary_system(vm);
vm_push(vm, CELL(context_var_addr));
vm_push(vm, CELL(vm->vocab.context_var_addr));
}
/* CURRENT ( -- addr ) Return VM address of CURRENT cell */
@@ -470,7 +459,7 @@ void vocabulary_word_context(VM *vm) {
*/
void vocabulary_word_current(VM *vm) {
init_vocabulary_system(vm);
vm_push(vm, CELL(current_var_addr));
vm_push(vm, CELL(vm->vocab.current_var_addr));
}
/* FORTH ( -- ) Make FORTH the CONTEXT vocabulary (and nothing else) */
@@ -481,7 +470,7 @@ void vocabulary_word_current(VM *vm) {
*/
void vocabulary_word_forth(VM *vm) {
init_vocabulary_system(vm);
context_vocab = forth_vocab;
vm->vocab.context_vocab = vm->vocab.forth_vocab;
vocab_sync_vm_vars(vm);
log_message(LOG_DEBUG, "FORTH selected (CONTEXT := FORTH)");
}
@@ -537,6 +526,10 @@ void vocabulary_word_paren_find(VM *vm) {
void vocabulary_word_order(VM *vm) {
init_vocabulary_system(vm);
DictEntry *context_vocab = vm->vocab.context_vocab;
DictEntry *forth_vocab = vm->vocab.forth_vocab;
DictEntry *current_vocab = vm->vocab.current_vocab;
printf("Search order: ");
if (context_vocab && context_vocab->name_len > 0) {
fwrite(context_vocab->name, 1, (size_t) context_vocab->name_len, stdout);