Files
LithosAnanake/src/word_source/defining_words.c
T
Robert Allan JamesandClaude Sonnet 5 4e7dcdf889 Add FENCE word (SDK v1.9.0 scoping); fix severe pre-existing FORGET use-after-free
FENCE ( -- ) exposes the dict_fence_latest/dict_fence_here state FORGET
already honored internally, letting callers (e.g. a future SDK capsule)
raise the boundary after loading their own content -- no new VM fields,
no policy logic beyond exposing existing state.

Writing a direct test for it surfaced a real, severe, pre-existing bug in
FORGET's relink logic, unrelated to FENCE itself and reproducible with the
original boot-time fence alone:

- Forgetting the single newest word incorrectly destroyed every other word
  back to the fence too, not just the target.
- Forgetting an older word (correctly cascading to remove newer words too,
  per FORTH-79 semantics) crashed with SIGSEGV.

Root cause: the relink code's target_prev pointer was, by construction,
always inside the range the preceding loop had just freed whenever target
wasn't vm->latest -- so writing through it was a use-after-free every time
that branch executed. Fixed by removing the target_prev tracking and both
branches entirely; vm->latest unconditionally becomes target_next (target's
own captured, still-valid link) after the free loop, correct in every case.

Added a FENCE test suite to dictionary_manipulation_words_test.c (Module 14)
including the exact regression case (forgetting the newest word must not
disturb an older one). Verified zero warnings and identical POST/dict_hash
results across all three kernel architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 05:57:18 -04:00

794 lines
25 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.
*/
#include "include/defining_words.h"
#include "../../include/vm.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/profiler.h"
#include "../../include/physics_metadata.h"
#include "../../include/platform_time.h"
#include "../../include/physics_pipelining_metrics.h"
#include <string.h>
#include <stdlib.h> /* malloc/free */
/* ───── Forward declarations so registration sees these symbols ───── */
static void defining_runtime_create(VM * vm);
static void defining_runtime_constant(VM * vm);
static void defining_runtime_variable(VM * vm);
static void defining_runtime_lit(VM * vm);
static void defining_runtime_dodoes(VM * vm);
static void defining_runtime_does_rt(VM * vm);
static void defining_word_colon(VM * vm);
static void defining_word_semicolon(VM * vm);
static void defining_word_create(VM * vm);
static void defining_word_constant(VM * vm);
static void defining_word_variable(VM * vm);
static void defining_word_left_bracket(VM * vm);
static void defining_word_right_bracket(VM * vm);
static void defining_word_state(VM * vm);
static void defining_word_compile(VM * vm);
static void defining_word_bracket_compile(VM * vm);
static void defining_word_literal(VM * vm);
static void defining_word_does(VM * vm);
static void defining_word_immediate(VM * vm);
static void dictionary_word_forget(VM * vm);
static void dictionary_word_fence(VM * vm);
/* ───────────────────────────── Runtimes ───────────────────────────── */
/* CONSTANT runtime: ( -- n ) Push stored value from header DF cell */
/**
* @brief Runtime behavior for CONSTANT words
*
* Pushes the constant value stored in the word's data field onto the stack.
* Stack effect: ( -- n )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_constant(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vm_push(vm, *df);
log_message(LOG_DEBUG, "CONSTANT runtime: pushed %ld for %.*s",
(long) *df, entry->name_len, entry->name);
}
/* VARIABLE runtime: ( -- addr ) Push VM offset stored in header DF cell */
/**
* @brief Runtime behavior for VARIABLE words
*
* Pushes the variable's address (VM offset) stored in the word's data field.
* Stack effect: ( -- addr )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_variable(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vm_push(vm, *df);
log_message(LOG_DEBUG, "VARIABLE runtime: pushed VM addr %ld for %.*s",
(long) *df, entry->name_len, entry->name);
}
/* CREATE runtime: ( -- addr ) Push DFA (VM byte offset) captured at CREATE */
/**
* @brief Runtime behavior for words defined by CREATE
*
* Pushes the word's data field address (DFA) captured at CREATE time.
* Stack effect: ( -- addr )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_create(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vaddr_t dfa = (vaddr_t)(uint64_t)(*df);
if (!vm_addr_ok(vm, dfa, sizeof(cell_t))) {
vm->error = 1;
return;
}
vm_push(vm, CELL(dfa));
log_message(LOG_DEBUG, "CREATE runtime: pushed DFA %ld for %.*s",
(long) dfa, entry->name_len, entry->name);
}
/* LIT runtime: ( -- n ) — fetch next cell from threaded code via R-stack IP */
/**
* @brief Runtime behavior for literal values in threaded code
*
* Fetches the next cell from threaded code via IP and pushes it.
* Stack effect: ( -- n )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_lit(VM *vm) {
if (!vm) return;
if (vm->rsp < 0) {
vm->error = 1;
return;
}
cell_t *rip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp]; /* peek IP */
if (!rip) {
vm->error = 1;
return;
}
cell_t value = *rip; /* read literal cell */
vm_push(vm, value);
rip++; /* advance IP to after literal */
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
rip; /* write-back */
log_message(LOG_DEBUG, "LIT: pushed %ld", (long) value);
}
/* DODOES runtime: when a created word runs, push PFA+8 (user data addr) then execute DOES>-body
PFA layout: [body_vaddr][user_data...]
We push the address of user_data, then execute body_vaddr code. */
/**
* @brief Runtime behavior for words defined with DOES>
*
* Pushes the address of user data area then executes the DOES> body code.
* PFA layout: [body_vaddr][user_data...]
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_dodoes(VM *vm) {
if (!vm) return;
DictEntry *self = vm->current_executing_entry;
if (!self) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: no current entry");
return;
}
cell_t *df = vm_dictionary_get_data_field(self);
if (!df) {
vm->error = 1;
return;
}
vaddr_t pfa = (vaddr_t)(uint64_t)(*df);
/* PFA[0] holds vaddr of DOES>-body */
if (!vm_addr_ok(vm, pfa, sizeof(cell_t))) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: invalid PFA address %zu", (size_t) pfa);
return;
}
vaddr_t body_vaddr = (vaddr_t)(uint64_t)(*(cell_t *) vm_ptr(vm, pfa));
log_message(LOG_DEBUG, "DODOES: body_vaddr=%zu (loaded from PFA[0] at %zu)", (size_t) body_vaddr, (size_t) pfa);
if (!vm_addr_ok(vm, body_vaddr, sizeof(cell_t))) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: invalid body_vaddr %zu", (size_t) body_vaddr);
return;
}
/* Push address of user data (PFA + sizeof(cell_t)) per DOES> semantics */
vaddr_t user_data_addr = pfa + sizeof(cell_t);
vm_push(vm, (cell_t) user_data_addr);
/* Execute like a colon word until EXIT pops the frame */
int base_rsp = vm->rsp;
cell_t *ip = (cell_t *) vm_ptr(vm, body_vaddr);
vm->rsp++;
if (vm->rsp >= STACK_SIZE) {
vm->error = 1;
vm->rsp = base_rsp;
return;
}
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
while (!vm->error && !vm->abort_requested && !vm->exit_colon && vm->rsp >= base_rsp + 1) {
cell_t *cur_ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
DictEntry *entry = (DictEntry *) (uintptr_t)(*cur_ip++);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
cur_ip;
if (!entry || !entry->func) {
vm->error = 1;
break;
}
vm->current_executing_entry = entry;
physics_execution_heat_increment(entry);
profiler_word_count(entry);
profiler_word_enter(entry);
entry->func(vm);
physics_metadata_touch(entry, entry->execution_heat, sf_monotonic_ns());
profiler_word_exit(entry);
}
/* Handle EXIT from the DOES> body */
if (vm->exit_colon) {
vm->rsp = base_rsp;
vm->exit_colon = 0;
}
/* ABORT already reset rsp (reset_vm_state(), called synchronously by
* system_word_abort() before this loop even notices the flag) --
* restoring it to base_rsp here would partially undo that reset.
* Only clamp back up to base_rsp on a normal/EXIT completion. */
if (!vm->abort_requested && vm->rsp < base_rsp) vm->rsp = base_rsp;
}
/* does_rt: runs inside the defining word at DOES> time.
Converts the just-created child so its runtime is DODOES and
records the DOES>-body start address at the child's PFA[0].
NOTE: PFA already contains user data from , or other compile-time words.
We need to build a new PFA structure: [body_vaddr][old_user_data...]
and update the child's DF to point to this new PFA. */
/**
* @brief Helper routine executed at DOES> time
*
* Converts the just-created child word to use DODOES runtime behavior
* and records the DOES> body address in the child's PFA.
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_does_rt(VM *vm) {
if (!vm) return;
DictEntry *child = vm->latest;
if (!child) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: no child to patch");
return;
}
/* IP (top R-stack) points at the next entry (EXIT we compile below); body starts after that */
if (vm->rsp < 0) {
vm->error = 1;
return;
}
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp]; /* IP is a real pointer, not vaddr */
cell_t *body_ip = ip + 1; /* first entry AFTER EXIT */
vaddr_t body_vaddr = (vaddr_t)((uint8_t *) body_ip - vm->memory);
cell_t *df = vm_dictionary_get_data_field(child);
if (!df) {
vm->error = 1;
return;
}
vaddr_t old_pfa = (vaddr_t)(uint64_t)(*df);
/* Calculate how much user data was allocated between old PFA and current HERE */
vaddr_t current_here = (vaddr_t) vm->here;
size_t user_data_size = (old_pfa < current_here) ? (current_here - old_pfa) : 0;
/* Allocate new PFA: [body_vaddr][user_data...] */
vm_align(vm);
vaddr_t new_pfa = (vaddr_t) vm->here;
/* Allocate space for body_vaddr pointer */
cell_t *body_ptr = (cell_t *) vm_allot(vm, sizeof(cell_t));
if (!body_ptr) {
vm->error = 1;
return;
}
*body_ptr = (cell_t) body_vaddr;
/* Copy user data if any was allocated */
if (user_data_size > 0) {
void *user_data_dest = vm_allot(vm, user_data_size);
if (!user_data_dest) {
vm->error = 1;
return;
}
if (vm_addr_ok(vm, old_pfa, user_data_size)) {
memcpy(user_data_dest, vm_ptr(vm, old_pfa), user_data_size);
}
}
/* Update child's DF to point to new PFA */
*df = (cell_t) new_pfa;
child->func = defining_runtime_dodoes;
}
/* ───────────────────────────── Defining words ─────────────────────── */
/* CREATE ( "name" -- ) — 79: does NOT allocate data; captures cell-aligned HERE as DFA */
static void defining_word_create(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_create);
if (!entry) {
vm->error = 1;
return;
}
vm_align(vm); /* align HERE for cell data */
vaddr_t dfa = (vaddr_t) vm->here;
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)
dfa;
log_message(LOG_DEBUG, "CREATE: '%.*s' DFA=%ld (HERE=%ld)",
nlen, namebuf, (long) dfa, (long) vm->here);
}
/* : ( "name" -- ) begin colon definition — IMMEDIATE */
static void defining_word_colon(VM *vm) {
if (!vm) return;
/* 79: nested colon definitions are illegal */
if (vm->mode == MODE_COMPILE) {
log_message(LOG_ERROR, "Nested ':' inside a definition is not allowed");
vm->error = 1;
return;
}
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
vm_enter_compile_mode(vm, namebuf, (size_t) nlen);
log_message(LOG_DEBUG, ": Started definition of '%.*s'", nlen, namebuf);
}
/* ; ( -- ) end colon definition — IMMEDIATE (compiles EXIT in vm_exit_compile_mode) */
static void defining_word_semicolon(VM *vm) {
if (!vm) return;
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
return;
}
vm_exit_compile_mode(vm);
}
/* CONSTANT ( n "name" -- ) */
static void defining_word_constant(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t value = vm_pop(vm);
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_constant);
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = value;
log_message(LOG_DEBUG, "CONSTANT: '%.*s' = %ld", nlen, namebuf, (long) value);
}
/* VARIABLE ( "name" -- ) allocate one cell in VM and store its addr in header DF */
static void defining_word_variable(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
vm_align(vm);
vaddr_t addr = (vaddr_t) vm->here;
void *p = vm_allot(vm, sizeof(cell_t));
if (!p) {
vm->error = 1;
return;
}
*(cell_t *) p = 0;
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_variable);
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)
addr;
log_message(LOG_DEBUG, "VARIABLE: '%.*s' at VM addr %ld", nlen, namebuf, (long) addr);
}
/* [ ( -- ) enter interpret state — IMMEDIATE */
static void defining_word_left_bracket(VM *vm) {
if (!vm) return;
/* Forth-79 requires STATE to be 0 for interpret and non-zero (typically -1) for compile */
vm_store_cell(vm, vm->state_addr, 0);
vm->mode = MODE_INTERPRET;
log_message(LOG_DEBUG, "[: interpret mode");
}
/* ] ( -- ) enter compile state — IMMEDIATE */
static void defining_word_right_bracket(VM *vm) {
if (!vm) return;
vm_store_cell(vm, vm->state_addr, (cell_t) - 1);
vm->mode = MODE_COMPILE;
log_message(LOG_DEBUG, "]: compile mode");
}
/* STATE ( -- addr ) push VM addr of STATE cell */
static void defining_word_state(VM *vm) {
if (!vm) return;
vm_push(vm, (cell_t) vm->state_addr);
}
/* COMPILE ( "word" -- ) — IMMEDIATE (legacy) */
static void defining_word_compile(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_find_word(vm, namebuf, (size_t) nlen);
if (!entry) {
vm->error = 1;
return;
}
vm_compile_word(vm, entry);
log_message(LOG_DEBUG, "COMPILE: compiled '%.*s'", nlen, namebuf);
}
/* [COMPILE] ( "word" -- ) — IMMEDIATE: compile even if the word is IMMEDIATE */
static void defining_word_bracket_compile(VM *vm) {
defining_word_compile(vm);
}
/* LITERAL ( n -- ) — IMMEDIATE: compile a literal */
static void defining_word_literal(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t v = vm_pop(vm);
vm_compile_literal(vm, v);
log_message(LOG_DEBUG, "LITERAL: compiled %ld", (long) v);
}
/* FORGET ( "name" -- ) remove target and newer words; rewind HERE to target DFA */
static void dictionary_word_forget(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
int forget_failed = 0;
sf_mutex_lock(&vm->dict_lock);
/* Find target */
DictEntry *e = vm->latest;
DictEntry *target = NULL;
while (e) {
if (!(e->flags & WORD_HIDDEN) &&
e->name_len == (uint8_t) nlen &&
memcmp(e->name, namebuf, (size_t) nlen) == 0) {
target = e;
break;
}
e = e->link;
}
if (!target) {
vm->error = 1;
forget_failed = 1;
goto forget_exit;
}
/* Ensure target is NEWER than the fence */
int allowed = 0;
for (e = vm->latest; e; e = e->link) {
if (e == target) {
allowed = 1;
break;
}
if (e == vm->dict_fence_latest) break;
}
if (!allowed) {
vm->error = 1;
forget_failed = 1;
goto forget_exit;
}
/* Compute new HERE from targets DFA (bounded by fence HERE) */
vaddr_t new_here = (vaddr_t) vm->here;
cell_t *df = vm_dictionary_get_data_field(target);
if (df) {
vaddr_t dfa = (vaddr_t)(uint64_t)(*df);
if (vm_addr_ok(vm, dfa, 0)) {
vaddr_t fence_here = (vaddr_t) vm->dict_fence_here;
new_here = (dfa < fence_here) ? fence_here : dfa;
}
} else {
new_here = (vaddr_t) vm->dict_fence_here;
}
/* Save target's link before freeing (needed for relinking) */
DictEntry *target_next = target->link;
/* Free headers from latest down to and including target, stopping before fence */
e = vm->latest;
while (e && e != vm->dict_fence_latest) {
DictEntry *next = e->link;
int is_target = (e == target);
vm_dictionary_untrack_entry(vm, e);
/* Free transition metrics before freeing entry */
if (e->transition_metrics) {
transition_metrics_cleanup(e->transition_metrics);
free(e->transition_metrics);
e->transition_metrics = NULL;
}
free(e);
if (is_target) break;
e = next;
}
/* Relink: FORGET removes target and everything newer (closer to
* vm->latest) -- the free loop above just freed every entry from
* vm->latest through target inclusive, which by definition includes
* any entry between them. There is nothing left to "skip past" --
* target_next (target's own surviving link) is always the correct
* new head, whether or not target was vm->latest itself. (Previously
* this branched on whether an entry existed between vm->latest and
* target, and relinked through it -- but that entry was always inside
* the just-freed range, making it a use-after-free on every FORGET of
* a non-latest word.) */
vm->latest = target_next;
vm->here = (size_t) new_here;
log_message(LOG_DEBUG, "FORGET: forgot '%.*s'; HERE=%ld (fence HERE=%ld)",
nlen, namebuf, (long) vm->here, (long) vm->dict_fence_here);
forget_exit:
sf_mutex_unlock(&vm->dict_lock);
if (forget_failed)
return;
}
/* FENCE ( -- ) raise the FORGET boundary to the current dictionary top.
* dict_fence_latest/dict_fence_here already exist and are already honored
* by FORGET (set once at bootstrap, protecting the base wordset); this is
* the first word exposing that state so callers can advance it further --
* e.g. an SDK capsule calling FENCE after loading protects its own
* definitions the same way the base wordset is already protected. */
static void dictionary_word_fence(VM *vm) {
if (!vm) return;
sf_mutex_lock(&vm->dict_lock);
vm->dict_fence_latest = vm->latest;
vm->dict_fence_here = vm->here;
sf_mutex_unlock(&vm->dict_lock);
log_message(LOG_DEBUG, "FENCE: raised to HERE=%ld", (long) vm->dict_fence_here);
}
/* DOES> — IMMEDIATE: finalize the defining words create-part and compile DOES>-body */
static void defining_word_does(VM *vm) {
if (!vm) return;
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: compile-only");
return;
}
/* Compile helper (void fn; check vm->error after) */
vm_compile_call(vm, defining_runtime_does_rt);
if (vm->error) {
log_message(LOG_ERROR, "DOES>: could not compile does_rt");
return;
}
/* Compile EXIT so the defining word returns immediately after patching */
DictEntry *exitw = vm_find_word(vm, "EXIT", 4);
if (!exitw) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: EXIT not found");
return;
}
vm_compile_word(vm, exitw);
if (vm->error) return;
/* Continue compiling: the following words form the DOES>-body executed by DODOES later. */
}
/* IMMEDIATE ( -- ) mark latest word immediate — IMMEDIATE itself */
static void defining_word_immediate(VM *vm) {
if (!vm) return;
if (!vm->latest) {
vm->error = 1;
return;
}
vm->latest->flags |= WORD_IMMEDIATE;
log_message(LOG_DEBUG, "IMMEDIATE: marked '%.*s' immediate",
(int) vm->latest->name_len, vm->latest->name);
}
/* ───────────────────────────── DEFER / IS ─────────────────────────── */
/* DEFER/IS/DEFER@ are NOT defined here: defer_words.c registers the same
* three names later in boot order (word_registry.c Module 27 vs this
* file's Module 17), and it has no __STARKERNEL__ guard, so it shadows
* this file's versions in both hosted and kernel builds. FORTH's
* newest-first dictionary lookup means defer_words.c's versions (a
* self-contained implementation, unrelated to this file's former
* defining_runtime_defer) are the only ones ever reachable. See
* proof/StarForth_Defer_Words.thy and proof/FINDINGS.md §3. */
/* ───────────────────────────── Registration ───────────────────────── */
/**
* @brief Register all defining words in the dictionary
*
* Registers core defining words like :, ;, CREATE, CONSTANT, VARIABLE,
* and their support words like IMMEDIATE, STATE, etc.
*
* @param vm Pointer to the VM context
*/
void register_defining_words(VM *vm) {
/* Core colon pair — both IMMEDIATE */
register_word(vm, ":", defining_word_colon);
vm_make_immediate(vm);
register_word(vm, ";", defining_word_semicolon);
vm_make_immediate(vm);
/* CREATE / VARIABLE / CONSTANT */
register_word(vm, "CREATE", defining_word_create);
register_word(vm, "VARIABLE", defining_word_variable);
register_word(vm, "CONSTANT", defining_word_constant);
/* IMMEDIATE (make IMMEDIATE itself immediate) */
register_word(vm, "IMMEDIATE", defining_word_immediate);
vm_make_immediate(vm);
/* STATE and mode switchers */
register_word(vm, "STATE", defining_word_state);
register_word(vm, "[", defining_word_left_bracket);
vm_make_immediate(vm);
register_word(vm, "]", defining_word_right_bracket);
vm_make_immediate(vm);
/* Dictionary management */
register_word(vm, "FORGET", dictionary_word_forget);
register_word(vm, "FENCE", dictionary_word_fence);
/* Compile helpers — immediate */
register_word(vm, "COMPILE", defining_word_compile);
vm_make_immediate(vm);
register_word(vm, "[COMPILE]", defining_word_bracket_compile);
vm_make_immediate(vm);
/* LIT runtime + LITERAL (immediate) */
register_word(vm, "LIT", defining_runtime_lit);
register_word(vm, "LITERAL", defining_word_literal);
vm_make_immediate(vm);
/* DOES> plumbing (lowercase internal helper visible for tests/trace) */
register_word(vm, "does_rt", defining_runtime_does_rt); /* internal helper */
register_word(vm, "DOES>", defining_word_does);
vm_make_immediate(vm);
}