proof/: migrate cell from int to 64-bit signed word, full suite verifies

cell_t is a 64-bit signed C long; the formal model previously used
unbounded HOL int, hiding wraparound and signed/unsigned distinctions
entirely. Switches cell to "64 word" throughout and fixes every proof
site that assumed int semantics:

- StarForth_Base.thy: cell_safe/cell_abs/cell_sdiv/cell_smod plus the
  sint-bridging lemmas used across the suite
- StarForth_Loop1_Heat.thy, StarForth_Loop3_Decay.thy: heat tracking
  converted to signed word comparisons (<s/\<le>s)
- StarForth_Stack_Words.thy: PICK/ROLL against real C ground truth
- StarForth_Arithmetic_Words.thy: ABS/MIN/MAX/div/mod rebuilt on signed
  word semantics (cell_sdiv/cell_smod match C99 truncating division;
  2/ uses signed_drop_bit to match "n >> 1"); documents a genuine
  ABS(INT64_MIN) wraparound hazard mirroring the real C behavior
- StarForth_Memory_Words.thy: @/!/C@/C! address checks converted to
  the signed order

All 23 theory files verify with zero errors, including
StarForth_Concurrent and StarForth_Correctness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-13 13:37:07 -04:00
co-authored by Claude Sonnet 5
parent 9b4bbc9de6
commit fe6e705867
6 changed files with 438 additions and 143 deletions
+140 -7
View File
@@ -1,7 +1,12 @@
theory StarForth_Base
imports Main
imports "HOL-Library.Word"
begin
(* AND/OR/XOR infix notation moved behind an opt-in bundle at some point
after 2011 -- unbundled by default now. Needed here since cell is now a
word type and several word derivations use it (see Section 1 below). *)
unbundle bit_operations_syntax
(* =========================================================================
SPECIFICATION AUTHORITY NOTICE
─────────────────────────────────────────────────────────────────────────
@@ -29,18 +34,33 @@ begin
(* ○ CODE-MUST-MATCH: cell_t in include/vm.h is `typedef signed long cell_t`.
On x86-64 Linux, signed long = 64-bit signed integer.
We model it as HOL int (arbitrary precision).
All word proofs hold in C provided no intermediate value overflows the
64-bit signed range. This is not a hidden assumption — it is the stated
correctness domain for FORTH programs that avoid overflow UB. *)
type_synonym cell = int
CORRECTED 2026-08-13: was modeled as HOL int (arbitrary precision,
never wraps). Changed to a genuine 64-bit word, matching how
StarForth_Q48_16.thy already models q48. The old int model made every
wraparound-dependent C behavior inexpressible (confirmed while scoping
StarForth_Double_Words.thy: D+/D-/DNEGATE explicitly do carry/borrow
arithmetic and bitwise-complement wraparound that only make sense
against a fixed-width type). cell_t is C's *signed* long, so value
comparisons on cell throughout this proof suite use the Word library's
SIGNED order (<s, \<le>s, sint) rather than the default unsigned (<, \<le>),
which does not match FORTH's signed-integer semantics (e.g. -1 must
compare less than 0, not greater, as it would under unsigned order).
Structural comparisons (list lengths, nat indices) are unaffected and
stay plain nat as before -- only actual cell VALUES need <s/\<le>s. *)
type_synonym cell = "64 word"
(* ○ CODE-MUST-MATCH: include/vm.h defines:
#define FORTH_TRUE ((cell_t)-1)
#define FORTH_FALSE ((cell_t) 0)
Any C logical word that produces a boolean result MUST use these macros,
not raw 1/0 or any other encoding. Verified in:
src/word_source/logical_words.c — ALL comparison and test words *)
src/word_source/logical_words.c — ALL comparison and test words
-1 as a cell (64 word) numeral is already the correct two's-complement
all-ones bit pattern (0xFFFFFFFFFFFFFFFF) via Word's standard numeral
semantics -- identical to C's (cell_t)-1 bit-for-bit, no change needed
here despite the type change above. *)
definition forth_true :: cell where "forth_true = -1"
definition forth_false :: cell where "forth_false = 0"
@@ -54,6 +74,119 @@ lemma to_forth_bool_False [simp]: "to_forth_bool False = 0"
lemma to_forth_bool_eq: "to_forth_bool b = (if b then -1 else 0)"
by (cases b; simp)
(* =========================================================================
Reusable safe-arithmetic infrastructure for cell (64 word) values,
added during the cell-as-word migration 2026-08-13.
Every numeric constant this proof suite actually uses (HEAT_MAX =
10^6, ACL_MAX_TTL = 65535, DECAY_SLOPE_MAX in the low millions, etc.)
is many orders of magnitude below the 64-bit signed range
[-2^63, 2^63-1]. cell_safe below names a single generous safe zone
(|c| < 2^32, still 2^31 times larger than any constant actually in
use) so every call site needs only "cell_safe c" (or the two bare
inequalities it unfolds to) instead of re-deriving a
signed_take_bit/sint no-overflow argument from scratch every time a
proof adds or subtracts 1 from a cell value near a bound. Without
this, +1/-1 near a bound is NOT provably safe in general -- a bare
64-bit word wraps -- so this is a genuine hypothesis every such lemma
needs, not a formality. *)
definition cell_safe :: "cell \<Rightarrow> bool" where
"cell_safe c \<longleftrightarrow> -4294967296 \<le>s c \<and> c \<le>s 4294967296" \<comment> \<open>|c| < 2^32\<close>
lemma cell_safe_sint_bounds:
assumes "cell_safe c"
shows "-4294967296 \<le> sint c" "sint c \<le> 4294967296"
using assms by (simp_all add: cell_safe_def word_sle_eq)
(* The one lemma every "+1 near a bound" argument actually needs: adding
or subtracting a small delta to a cell_safe value produces exactly
the expected sint, no wraparound -- because the safe zone leaves a
2^31-times margin against the true 2^63 boundary. *)
lemma cell_safe_add_sint:
assumes "cell_safe c"
assumes "\<bar>delta\<bar> \<le> 4294967296"
shows "sint (c + word_of_int delta) = sint c + delta"
proof -
have b: "-4294967296 \<le> sint c" "sint c \<le> 4294967296"
using assms(1) by (simp_all add: cell_safe_sint_bounds)
have step1: "sint (c + word_of_int delta)
= signed_take_bit 63 (sint c + sint (word_of_int delta :: cell))"
by (simp add: sint_word_add)
have step2: "sint (word_of_int delta :: cell) = delta"
using assms(2)
by (simp add: sint_sbintrunc' signed_take_bit_int_eq_self)
have step3: "signed_take_bit 63 (sint c + delta) = sint c + delta"
by (rule signed_take_bit_int_eq_self) (use b assms(2) in auto)
from step1 step2 step3 show ?thesis by simp
qed
lemma cell_safe_1 [simp]: "cell_safe 1"
by (simp add: cell_safe_def word_sle_eq)
lemma cell_safe_0 [simp]: "cell_safe 0"
by (simp add: cell_safe_def word_sle_eq)
(* Subtraction variant, for the common "de_heat e - amount" shape (amount
is itself a cell parameter, not a fixed literal, so cell_safe_add_sint
with a literal delta doesn't directly apply). *)
lemma cell_safe_sub_sint:
assumes "cell_safe c"
assumes "cell_safe amount"
shows "sint (c - amount) = sint c - sint amount"
proof -
have bc: "-4294967296 \<le> sint c" "sint c \<le> 4294967296"
using assms(1) by (simp_all add: cell_safe_sint_bounds)
have ba: "-4294967296 \<le> sint amount" "sint amount \<le> 4294967296"
using assms(2) by (simp_all add: cell_safe_sint_bounds)
have "sint (c - amount) = signed_take_bit 63 (sint c - sint amount)"
by (simp add: sint_word_diff)
also have "\<dots> = sint c - sint amount"
by (rule signed_take_bit_int_eq_self) (use bc ba in auto)
finally show ?thesis .
qed
(* Every "nat n" conversion the old int-based PICK/ROLL/etc. definitions
did becomes "unat n" under the word migration -- but unat reinterprets
the raw bit pattern as unsigned, which only agrees with the natural
"nat of the signed value" reading when n is non-negative in the SIGNED
sense (0 \<le>s n). Every call site that uses this already checks n <s 0
first and only reaches the nat-conversion in the non-negative case, so
this lemma is exactly the bridge those proofs need, proved once here
rather than re-derived at every site. *)
lemma unat_eq_nat_sint_of_nonneg:
assumes "0 \<le>s (n :: cell)"
shows "unat n = nat (sint n)"
proof -
have lo: "0 \<le> sint n" using assms by (simp add: word_sle_eq)
have hi63: "sint n < 2 ^ 63" using sint_lt[of n] by simp
have hi: "sint n < 2 ^ (64::nat)" using hi63 by simp
have "uint n = take_bit (64::nat) (sint n)" by (simp add: uint_sint)
also have "\<dots> = sint n"
by (rule take_bit_int_eq_self) (use lo hi in auto)
finally show ?thesis by (simp add: unat_eq_nat_uint)
qed
(* Signed absolute value on cell. word's inherited "abs" (if it resolved at
all) would be unsigned-magnitude, which is wrong for cell_t's signed
semantics -- must build this explicitly from the signed order <s.
NOTE: cell_abs min_word = min_word (two's-complement negation of
INT64_MIN wraps back to itself, matching C's ABS(LONG_MIN) hazard) --
this is a genuine, documented exclusion, not an oversight. *)
definition cell_abs :: "cell \<Rightarrow> cell" where
"cell_abs n = (if n <s 0 then -n else n)"
(* Signed truncating division/remainder, matching C99's "/" and "%" on
signed cell_t: quotient truncates toward zero, remainder takes the sign
of the dividend. word's inherited div/mod are unsigned-magnitude and
would silently give the wrong answer for negative operands -- e.g.
(-7) div 2 under unsigned word semantics treats the bit pattern of -7 as
a huge positive number, nothing like C's -7 / 2 = -3. *)
definition cell_sdiv :: "cell \<Rightarrow> cell \<Rightarrow> cell" where
"cell_sdiv a b = word_of_int (sgn (sint a) * sgn (sint b) * (\<bar>sint a\<bar> div \<bar>sint b\<bar>))"
definition cell_smod :: "cell \<Rightarrow> cell \<Rightarrow> cell" where
"cell_smod a b = word_of_int (sgn (sint a) * (\<bar>sint a\<bar> mod \<bar>sint b\<bar>))"
(* =========================================================================
Section 2: Stack type and capacity constants
======================================================================== *)