ABORT is documented and tested in this codebase as standard FORTH-79 behavior -- system_words_test.c:63: "Should clear stacks and return to QUIT" -- meaning it should unwind all the way back to the outermost interpreter loop, abandoning whatever's left of the current line/block. The implementation only unwound one level: every place that checked vm->abort_requested cleared it the instant it saw it, so it never survived to propagate past the first nested frame. This surfaced via Artemis's ART-HALT-UNRECOG (capsules/artemis/init.4th): on an unrecognized disk it correctly printed "ARTEMIS HALT: unrecognized disk content" and called ABORT, but WELCOME (the next line in the same block) ran anyway, and Artemis announced ready to Hermes and joined the fleet normally -- contradicting .claude/ARTEMIS.md's "Refuse to mount... do not overwrite it" requirement. Root cause is general, not Artemis-specific, and present identically in both the hosted and kernel VM cores. Fixed at every level execution can nest through, verified by exhaustively grepping every !vm->error-gated continuation loop and adding the parallel !vm->abort_requested check: - execute_colon_word (src/vm.c, src/starkernel/vm/vm_core.c): stop clearing the flag on return -- every colon-word call is a recursive call to this same function, so leaving it set lets every enclosing frame's own check also unwind. - vm_interpret (src/vm.c, src/starkernel/vm/vm_core.c): stop parsing further words in the current input string once the flag is set. - exec_block_with_retry (src/starkernel/capsule/capsule_loader.c): capsule birth's line-by-line block executor -- stop processing further lines in the current block, but return 0 (not -1), so capsule_exec_payload still loads later blocks in the same capsule payload. Returning -1 here would have silently broken word definitions in blocks that come after the aborting one for reasons unrelated to why it aborted (concretely, Artemis's ART-PING/LOAD-DOE in blocks 4851/4852, which follow the entry block 4133). - THRU and --> (src/word_source/block_words.c): stop processing further blocks/lines in their own loops. - DODOES (src/word_source/defining_words.c): the CREATE...DOES> runtime has its own hand-rolled execution loop, separate from execute_colon_word -- same bug class, same fix. Also guarded the post-loop "if (vm->rsp < base_rsp) vm->rsp = base_rsp" clamp so it doesn't fire on an abort exit -- ABORT's own reset_vm_state() already set rsp; restoring it to base_rsp would have partially undone that. - Both REPL loops (src/repl.c, src/starkernel/repl.c x2 call sites): clear the flag after each line, mirroring the existing vm->error pattern, so a mid-line abort doesn't silently freeze subsequent interactive input. Verified directly: ": AB-TEST 1 2 3 ABORT 999 . ; AB-TEST 42 . CR 777 . CR" -- 999 never prints (stops mid-colon-word), 42 never prints (stops the rest of the same line), 777 prints fine (next line unaffected). Artemis: WELCOME/"Artemis ready" no longer fires after the halt message. No regression: all three architectures still show PASS: persist-read, PASS: E2E msg flow, and matching dict_hash on the normal (non-aborted) boot path; hosted test suite 965 passed / 0 failed. Known follow-up, not fixed here (see memory for details): Artemis still announces ready to Hermes via a separate call path (CD-INIT, block 4141) that never went through capsule_exec_payload's block chain in the first place, and the disk file still picks up incidental writes even on a correctly-halted boot -- likely generic block-subsystem housekeeping, not traced yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
865 lines
26 KiB
C
865 lines
26 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 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) 2023–2025 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);
|
||
|
||
/* ───────────────────────────── 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;
|
||
}
|
||
|
||
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 and its previous entry */
|
||
DictEntry *prev = NULL, *e = vm->latest;
|
||
DictEntry *target = NULL, *target_prev = NULL;
|
||
while (e) {
|
||
if (!(e->flags & WORD_HIDDEN) &&
|
||
e->name_len == (uint8_t) nlen &&
|
||
memcmp(e->name, namebuf, (size_t) nlen) == 0) {
|
||
target = e;
|
||
target_prev = prev;
|
||
break;
|
||
}
|
||
prev = e;
|
||
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 target’s 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: if target_prev exists, point it past the freed chain */
|
||
if (target_prev) {
|
||
target_prev->link = target_next; /* Skip freed entries */
|
||
vm->latest = target_prev; /* Latest is now the entry before target */
|
||
} else {
|
||
/* No prev means target was latest - reset to fence */
|
||
vm->latest = vm->dict_fence_latest;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* DOES> — IMMEDIATE: finalize the defining word’s 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 runtime: execute the XT stored in this word's data field */
|
||
static void defining_runtime_defer(VM *vm) {
|
||
if (!vm) return;
|
||
DictEntry *self = vm->current_executing_entry;
|
||
if (!self) { vm->error = 1; return; }
|
||
|
||
cell_t *df = vm_dictionary_get_data_field(self);
|
||
if (!df) { vm->error = 1; return; }
|
||
|
||
DictEntry *target = (DictEntry *)(uintptr_t)(*df);
|
||
if (!target || !target->func) {
|
||
log_message(LOG_ERROR, "DEFER %.*s: uninitialized — use IS to set",
|
||
(int)self->name_len, self->name);
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
vm->current_executing_entry = target;
|
||
target->func(vm);
|
||
}
|
||
|
||
/* DEFER ( "name" -- ) create a deferred word with an empty XT slot */
|
||
static void defining_word_defer(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_defer);
|
||
if (!entry) { vm->error = 1; return; }
|
||
|
||
cell_t *df = vm_dictionary_get_data_field(entry);
|
||
if (!df) { vm->error = 1; return; }
|
||
*df = 0;
|
||
|
||
log_message(LOG_DEBUG, "DEFER: '%.*s'", nlen, namebuf);
|
||
}
|
||
|
||
/* IS ( xt "name" -- ) store xt into the deferred word's slot */
|
||
static void defining_word_is(VM *vm) {
|
||
if (!vm) return;
|
||
if (vm->dsp < 0) {
|
||
log_message(LOG_ERROR, "IS: stack underflow");
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
cell_t xt = 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_find_word(vm, namebuf, (size_t)nlen);
|
||
if (!entry) {
|
||
log_message(LOG_ERROR, "IS: '%.*s' not found", nlen, namebuf);
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
if (entry->func != defining_runtime_defer) {
|
||
log_message(LOG_ERROR, "IS: '%.*s' is not a DEFER word", nlen, namebuf);
|
||
vm->error = 1;
|
||
return;
|
||
}
|
||
|
||
cell_t *df = vm_dictionary_get_data_field(entry);
|
||
if (!df) { vm->error = 1; return; }
|
||
*df = xt;
|
||
|
||
log_message(LOG_DEBUG, "IS: '%.*s' <- XT %p", nlen, namebuf, (void *)(uintptr_t)xt);
|
||
}
|
||
|
||
/* DEFER@ ( "name" -- xt ) fetch the XT stored in a deferred word */
|
||
static void defining_word_defer_fetch(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) {
|
||
log_message(LOG_ERROR, "DEFER@: '%.*s' not found", nlen, namebuf);
|
||
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, "DEFER@: '%.*s' -> XT %p", nlen, namebuf, (void *)(uintptr_t)(*df));
|
||
}
|
||
|
||
/* ───────────────────────────── 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);
|
||
|
||
/* 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);
|
||
|
||
/* DEFER / IS / DEFER@ */
|
||
register_word(vm, "DEFER", defining_word_defer);
|
||
register_word(vm, "IS", defining_word_is);
|
||
register_word(vm, "DEFER@", defining_word_defer_fetch);
|
||
}
|