proof/: model the TIB name-parse primitive, close it into CONSTANT's full model

input_buffer/input_length/input_pos (include/vm.h:415-417) turned out to
be plain per-VM array/scalar fields, not host pointers -- unlike almost
every other input-adjacent gap in this suite. vm_parse_word (src/vm.c:
137-160) is a pure whitespace-delimited scan over them, now modelled as
forth_parse_word in StarForth_Base.thy (is_ws + dropWhile/takeWhile,
faithful to the C's skip-then-copy-with-truncation loop, including that
input_pos only advances past a truncated token by what was actually
copied, matching the C's `len < max_len - 1` bound exactly).

dict_insert_entry (added last session) now takes the entry's name as a
parameter instead of hardcoding the empty string. forth_constant_full
composes forth_parse_word with dict_insert_entry end-to-end as a worked
example: CONSTANT's real order (stack-underflow guard -> pop value ->
parse name -> vm_create_word) is modelled in full up to the data-field
write, which remains the one still-open gap. The other four entry-half
definitions (:/CREATE/VARIABLE/DEFER) take the parsed name as a caller
parameter for now rather than repeating the same composition four more
times in one pass.

Full suite (54 theories) verifies green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-14 22:53:11 -04:00
co-authored by Claude Sonnet 5
parent cc46cf83f1
commit 1aca77d55c
4 changed files with 229 additions and 54 deletions
+102
View File
@@ -541,6 +541,27 @@ record vm_state =
for the correction). *)
state_addr :: nat
(* ── Input system (TIB), added 2026-08-14 ─────────────────────────────
○ CODE-MUST-MATCH: C: char input_buffer[INPUT_BUFFER_SIZE] (=1025),
size_t input_length, size_t input_pos (include/vm.h:415-417). This is
the REAL, live interpreter input buffer -- vm_interpret's dispatch
path for both interactive REPL lines and LOAD'd block content (see
.claude/CLAUDE.md's INPUT_BUFFER_SIZE note) -- distinct from the
separate `tib_buf`/`tib_cap`/`in_var`/`span_var` fields (vm.h:444-448,
the C's own comment marks them "legacy; will migrate to VM addr") and
from `hold_addr`/`hold_pos` above (the pictured-number OUTPUT buffer,
unrelated). Modelled as `input_buffer :: string` holding exactly the
meaningful prefix (not the full fixed 1025-byte physical array, which
has no abstract counterpart -- content past `input_length` in the
real C is stale/undefined and never read), with `input_length` kept
as a separate field even though it always equals `length input_buffer`
here, to mirror the real C's two-field structure precisely. `input_pos`
is FORTH's `>IN` (parse position), advanced by `vm_parse_word`
(src/vm.c:137-160, modelled below as `forth_parse_word`). *)
input_buffer :: string
input_length :: nat
input_pos :: nat
(* ── Physics Loop #1: Execution heat tracking ───────────────────────── *)
(* ○ CODE-MUST-MATCH: heat_threshold_{25th,50th,75th} in C VM struct.
⚠ HUMAN-REVIEW: Thresholds are recomputed periodically by the heat bucket
@@ -634,6 +655,87 @@ record vm_state =
return_stack, memory), never on physics state. *)
consts word_table :: "nat \<Rightarrow> vm_state \<Rightarrow> vm_state"
(* =========================================================================
Section 4b: TIB parsing (added 2026-08-14)
Closes, for the first time in this suite, the "name parse" dependency
named as an unmodelled precondition by nearly every name-consuming word
swept so far (CREATE/VARIABLE/CONSTANT/`:`/DEFER/IS/DEFER@/COMPILE/
[COMPILE]/FIND/WORD and others -- see StarForth_Defining_Words.thy's
file header, StarForth_Defer_Words.thy, etc.). `vm_parse_word`
(src/vm.c:137-160) turns out to be a pure scan over the input_buffer/
input_length/input_pos fields added above -- no host pointers, no C-
string tricks, unlike almost everything else this suite has deferred.
Individual per-word applications (composing this with e.g.
`dict_insert_entry`) are done in the files that use them, not here --
this section is only the shared parsing primitive.
======================================================================== *)
definition is_ws :: "char \<Rightarrow> bool" where
"is_ws c \<longleftrightarrow> c = CHR '' '' \<or> c = char_of (9::nat) \<or> c = char_of (10::nat) \<or> c = char_of (13::nat)"
\<comment> \<open>space, tab, LF, CR -- matches vm_parse_word's `c==' '||c=='\t'||c=='\n'||c=='\r'` exactly\<close>
(* C: `vm_parse_word` skips leading whitespace in input_buffer[input_pos..
input_length), then copies the following run of non-whitespace
(truncated to max_len-1 chars) into the caller's buffer, advancing
input_pos by exactly what was skipped plus what was copied (NOT past
any untruncated remainder of a token longer than max_len-1 -- the C
loop's own `len < max_len - 1` condition stops consuming input_pos at
the same point it stops writing `word`). Returns the parsed token
(empty string signals the C's `return 0`, matching every caller's
`nlen <= 0` failure check) and the updated vm_state. *)
definition forth_parse_word :: "nat \<Rightarrow> vm_state \<Rightarrow> (string \<times> vm_state)" where
"forth_parse_word max_len vm =
(let s = drop (input_pos vm) (input_buffer vm);
s1 = dropWhile is_ws s;
skipped = length s - length s1;
pos_ws = input_pos vm + skipped
in if s1 = []
then ('''', vm\<lparr>input_pos := pos_ws\<rparr>)
else
let tok = take (max_len - 1) (takeWhile (\<lambda>c. \<not> is_ws c) s1)
in (tok, vm\<lparr>input_pos := pos_ws + length tok\<rparr>))"
lemma forth_parse_word_all_whitespace_yields_empty:
assumes "dropWhile is_ws (drop (input_pos vm) (input_buffer vm)) = []"
shows "fst (forth_parse_word max_len vm) = ''''"
using assms by (simp add: forth_parse_word_def Let_def)
lemma forth_parse_word_success_nonempty:
assumes "dropWhile is_ws (drop (input_pos vm) (input_buffer vm)) \<noteq> []"
assumes "max_len \<ge> 2"
shows "fst (forth_parse_word max_len vm) \<noteq> ''''"
proof -
let ?s1 = "dropWhile is_ws (drop (input_pos vm) (input_buffer vm))"
from assms(1) obtain c cs where s1_eq: "?s1 = c # cs" by (cases ?s1) auto
have "\<not> is_ws c" using dropWhile_eq_Cons_conv[of is_ws "drop (input_pos vm) (input_buffer vm)" c cs]
using s1_eq by auto
hence "takeWhile (\<lambda>x. \<not> is_ws x) ?s1 = c # takeWhile (\<lambda>x. \<not> is_ws x) cs"
by (simp add: s1_eq)
hence "take (max_len - 1) (takeWhile (\<lambda>x. \<not> is_ws x) ?s1) \<noteq> []"
using assms(2) by simp
thus ?thesis
using assms(1) by (simp add: forth_parse_word_def Let_def)
qed
lemma forth_parse_word_input_pos_monotone:
"input_pos vm \<le> input_pos (snd (forth_parse_word max_len vm))"
by (simp add: forth_parse_word_def Let_def)
lemma forth_parse_word_preserves_buffer:
"input_buffer (snd (forth_parse_word max_len vm)) = input_buffer vm"
by (simp add: forth_parse_word_def Let_def)
lemma forth_parse_word_preserves_data_stack:
"data_stack (snd (forth_parse_word max_len vm)) = data_stack vm"
by (simp add: forth_parse_word_def Let_def)
lemma forth_parse_word_never_sets_error: True
\<comment> \<open>vm_parse_word's own C body never touches vm->error -- callers check
the returned length themselves and set it. Faithfully NOT set here
either.\<close>
by simp
(* =========================================================================
Section 5: Well-formedness, error signalling, capacity predicates
======================================================================== *)