10 of 16 registered words modeled (COLD/WARM/BYE/WORDS/VLIST/PAGE/NOP/
QUIT/ABORT/EXECUTE guard-shape), plus the internal (ABORT") runtime
helper. Deferred: ( and \ (TIB dependency), SAVE-SYSTEM (real file I/O),
79-STANDARD (reads a non-per-VM global), ABORT" compile-time half (TIB +
codegen), SEE (TIB + raw threaded-code pointer walk).
Two findings. (1) system_running/forth_79_standard are file-scope C
statics doing per-VM-shaped work -- the 5th and 6th occurrence of this
bug pattern in the sweep (previously: control_words.c's cf_stack,
dictionary_manipulation_words.c's state_variable, string_words.c's
word_scratch_addr). (2) EXECUTE casts a popped FORTH cell straight to a
DictEntry host pointer and calls through it (entry->func(vm)), gated
only by a null check -- same hazard class as format_words.c's ?/DUMP but
far more consequential since EXECUTE is a core, ubiquitous primitive
rather than a diagnostic word. Flagged as the highest-severity finding
this sweep has produced.
editor_words.c: zero tractable words (first such file in this sweep) --
every word routes through the same deferred block-window cache as
block_words.c, and EDIT is an interactive stdin/stdout REPL loop, not a
single-step transition.
format_words.c: 17 of 19 registered words modeled (# and #S deferred,
multi-precision division out of scope). Two genuine C findings recorded:
(1) DECIMAL/HEX/OCTAL write only the FORTH-visible memory cell at
base_addr, never the separate vm->base host-mirror field that number
OUTPUT words actually read -- proved formally
(decimal_does_not_change_vm_base et al.), so HEX/OCTAL/DECIMAL silently
never affect printed output, only parsed input. (2) ? and DUMP cast the
popped cell directly to a host pointer and dereference it, bypassing
vm_addr_ok entirely -- an out-of-VM-bounds read, not modeled since it
isn't a vm->memory access at all.
Adds base_addr/hold_addr/hold_pos to vm_state (StarForth_Base.thy),
matching the scr_addr/here pattern from earlier files.
7 of 9 registered words modeled (EMIT/CR/?TERMINAL/TYPE/SPACE/SPACES/
(do-string)); KEY and ." deferred (real external input / TIB-adjacent
input-buffer dependency, same categories as earlier deferrals in this
sweep). Two genuine C findings recorded: ?TERMINAL is a permanent stub
always returning false, and TYPE's bounds check has a signed-integer-
overflow bypass (addr+count wraps negative for large addr/count,
defeating the VM_MEMORY_SIZE guard) with a machine-checked witness.
block_words.c is categorically different from every file covered so far in
this sweep: every other word_source file operates on pure per-VM internal
state already in vm_state (data_stack/return_stack/memory/dictionary).
block_words.c sits on top of a real disk-backed I/O subsystem
(block_subsystem.h) plus a per-VM in-memory cache of it
(vm->blk_vm_lbn/blk_vm_cbuf/blk_vm_dirty/blk_vm_next), none of which are
in vm_state.
Only SCR is self-contained (just needs vm->scr_addr, added to vm_state
the same way here/ecw_nesting were for earlier files). The other 11 words
are deferred for three reasons documented in the theory header: the
block-window cache subsystem (a modeling project on the scale of the
deferred TIB input subsystem, not a one-word extension), real disk I/O via
block_subsystem.h, and recursive vm_interpret()/printf() in LOAD/LIST/
THRU/-->.
Noted in passing: blk_vm_evict/blk_vm_flush_all's own comments document a
real raw-pointer-lifetime bug (stale C buffer pointers after block-
subsystem struct-copy eviction) that was already found and fixed by hand
in the C, before this suite ever looked at the file -- not an open issue,
just worth recording as prior art for exactly the class of bug this sweep
exists to catch.
30 theory files verify with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the 9 self-contained words in string_words.c that operate purely on
data_stack/memory with no dependency outside the existing model. Introduces
vm_addr_ok_m, a literal transcription of the real C vm_addr_ok bounds check
(src/vm.c:815-820) using VM_MEMORY_SIZE -- more precise than the sign-only
check earlier memory words used -- and resolve_span, a shared helper for
the auto-detect-counted-string pattern that recurs across six of this
file's words.
16 words deliberately not modeled, in three groups (full reasoning in the
theory header): (a) WORD/SPAN/TIB/>IN/SOURCE/QUERY/EXPECT depend on the
lazily-allocated TIB input subsystem (vm->tib_buf via vm_input_ensure),
which has no vm_state counterpart; QUERY/EXPECT also call fgets(stdin)
directly, real I/O with no HOL formalization; (b) CONVERT/NUMBER/ENCLOSE
depend on raw C-string scanning (strlen past a single vm_addr_ok-checked
byte -- a genuine unbounded-read hazard, noted not chased) or strtol(); (c)
S"/(s")/LITERAL/[LITERAL]/['] depend on the same compile-time/threaded-code
machinery already out of scope from control_words.c. SEARCH is deferred
despite being self-contained -- its nested substring search needs a bigger
proof-engineering lift than the single-pass helpers used here.
Third occurrence of the file-scope-static-instead-of-per-VM-field bug
pattern noted (WORD's word_scratch_addr), matching control_words.c's
cf_stack and dictionary_manipulation_words.c's state_variable -- not fixed,
flagged for aggregation when raised to Bob.
29 theory files verify with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the runtime half of control_words.c fully: (BRANCH), (0BRANCH),
(?DO), (DO), (LOOP), (+LOOP), (LEAVE), UNLOOP, I, J, EXIT. The "vm_ip as
raw pointer" gap flagged at every earlier resume point turns out not to
need a new model extension -- return_stack-held addresses dereference into
vm->memory exactly like @/! addresses from the data stack, so the existing
mem_read/unat machinery from StarForth_Memory_Words covers it directly.
The compile-time half (IF/ELSE/THEN, BEGIN/WHILE/REPEAT/AGAIN/UNTIL, the
compiling halves of ?DO/DO/LOOP/+LOOP/LEAVE, CASE/OF/ENDOF/ENDCASE) is left
unmodelled, not from a model gap but a genuine architectural finding:
Headline finding, not fixed: every compile-time control-flow word operates
on FILE-SCOPE C statics (cf_stack/cf_sp, cf_last_mode, leave_addrs/leave_sp,
endof_addrs/endof_sp, and their mark-stacks) -- none are struct VM fields,
none are keyed by VM instance. In the Tripod multi-VM fleet, two VMs
compiling control structures at overlapping times corrupt each other's
IF/DO/CASE nesting through this shared global state, and a VM whose
compilation aborts mid-structure leaves stale cf_sp/leave_sp/endof_sp state
for whichever VM compiles next. cf_epoch_sync's mode-transition reset
heuristic is itself keyed off a single global (cf_last_mode), not per-VM,
so it can neither reliably detect nor reliably avoid false resets across
VMs. Modelling these words against vm_state would require either inventing
a field the real implementation doesn't have (silently fixing the bug in
the proof) or modelling a bare global with no plumbing precedent in this
suite -- both out of scope, left as documented gaps.
28 theory files verify with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the mode/flag half of dictionary_manipulation_words.c that's provable
against the existing vm_mode/dictionary/latest_id model. The raw-pointer
DictEntry navigation half (>BODY/>NAME/NAME>/>LINK/LINK>/CFA/LFA/NFA/PFA/
TRAVERSE/FIND/') is left unmodelled -- same class of gap as control_words.c's
deferred vm_ip/return-stack-as-raw-pointers issue, since the abstract
dict_entry record is word_id-indexed, not addressed, and has no counterpart
for struct-relative pointer arithmetic (name_len, link, body offset).
Genuine findings recorded in comments, not fixed:
- [, ], STATE, and INTERPRET all read/write a file-scope `static cell_t
state_variable` -- NOT vm->state_var, the real per-VM STATE field used
everywhere else in the interpreter. In the Tripod multi-VM fleet this
static is shared across every VM instance, not per-VM.
- dictionary_m_word_hidden's dead #else branch (unreachable since
WORD_HIDDEN is always defined) calls a function that doesn't exist
(dictionary_word_smudge vs. the real static dictionary_m_word_smudge).
27 theory files verify with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds VM_MEMORY_SIZE and DICTIONARY_MEMORY_SIZE constants to StarForth_Base.thy
(previously only STACK_SIZE existed). SP@/SP! left unmodelled (oops-flagged
with explanation) -- the list-based data_stack model has no independent dsp
register distinct from list length, which is exactly what SP! manipulates.
Genuine findings recorded in comments, not fixed:
- LATEST has an identical body to HERE (both just push vm->here) rather than
consulting vm->latest -- doesn't return what its own doc comment claims.
- ALIGN (via vm_align/vm_allot) bounds-checks here against
DICTIONARY_MEMORY_SIZE (2MB), while ALLOT/,/C,/2, bound-check directly
against VM_MEMORY_SIZE (5MB) instead -- two different ceilings for the
same dictionary pointer.
Full suite (26 theory files) verifies with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers word_source/mixed_arithmetic_words.c. Two genuine findings recorded
in comments rather than fixed:
- register_mixed_arithmetic_words registers MOD and /MOD a second time,
after arithmetic_words.c's own registrations; vm_create_word links new
entries at the head of vm->latest and FIND scans from vm->latest forward,
so arithmetic_words.c's MOD//MOD are permanently shadowed, unreachable
dead code once bootstrap completes (verified against
dictionary_management.c and the module order in word_registry.c).
- M*, M/MOD, and the "avoids intermediate overflow" claim on */ and */MOD
are false on 64-bit builds: cell_t and "long long" are the same width
there, so the long-long intermediate does not actually widen the
product -- it wraps mod 2^64 like plain cell multiplication before the
32-bit-style split/reconstruction runs. M*/M/MOD are left undefined
here (oops-equivalent: documented as not modelled, since formalizing
"the wrong thing, faithfully" adds no proof value) rather than fixed.
MOD//MOD/*//*/MOD reuse cell_sdiv/cell_smod from the arithmetic-words
migration; M+/M- transcribe the C's hand-rolled signed carry/borrow
detection literally, proving only stack-level plumbing (not double-
precision correctness, which needs an interpretation function this
suite doesn't build).
All 24 theory files verify with zero errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the pure double-cell data-stack shuffle words from
src/word_source/double_words.c. Deliberately scoped to exclude:
- 2>R/2R>/2R@: branch on vm->ecw_nesting, a field vm_state doesn't track
at all -- needs a model extension first, not attempted here.
- S>D/D+/D-/DNEGATE/DABS/DMAX/DMIN/D</D=/D0=/D0</D2*/D2/: depend on
cell_t being a fixed-width (64-bit) wrapping integer (explicit
unsigned-long carry/borrow arithmetic, bitwise complement with
wraparound). StarForth_Base.thy's "cell = int" is unbounded, not
fixed-width, so this isn't expressible as currently modeled. Fixing it
means deciding whether cell becomes a 64-bit word type everywhere
(ripples into all 23 already-verified theories) -- a foundational
decision, flagged for later, not made as a side effect of this file.
24 theory files now verify with zero errors.