Fix d_compare: use ucell_t instead of unsigned long (FABRIC-3.md §XII.4)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

d_compare() (double_words.c), the static helper backing DMAX/DMIN/D</D=,
had the same bare-unsigned-long pattern already fixed in D+/D-/DNEGATE
(commit bea8d74) -- 32-bit unsigned long on this aarch64 target silently
truncating the low-cell comparison. Switched to ucell_t.

Verified with four cases designed specifically to expose the old
truncation: two doubles sharing the same high cell but with low cells
differing only in bits 32-63 (invisible to a 32-bit-truncated compare,
e.g. 2^32 vs 0):
  4294967296 0 0 0 D= .            -> 0  (correctly not equal)
  4294967296 0 0 0 DMIN SWAP D.    -> 0  (correctly picks the smaller)
  0 0 4294967296 0 D< .            -> -1 (correct)
  4294967296 0 0 0 D< .            -> 0  (correct)
All four pass identically on amd64, aarch64, and riscv64. These cases
were never run before this fix -- the exerciser campaign never touched
DMAX/DMIN/D</D= at all, so this is the first real evidence d_compare()
was ever exercised on any architecture. Full 24-case exerciser also
reran clean on all three architectures (no regression).

D2*/D2/ use unsigned long long (C-standard-guaranteed >=64-bit) and were
never part of this bug class. All four affected words (D+, D-, DNEGATE,
d_compare) are now fixed; M*'s separate universal DOUBLE-OVERFLOW bug
remains open (unrelated defect, out of scope here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-11 07:44:14 -04:00
co-authored by Claude Sonnet 5
parent bea8d7436a
commit 1a716c8048
10 changed files with 27082 additions and 11 deletions
+21 -8
View File
@@ -2142,8 +2142,8 @@ against the other two architectures once the campaign completes; not yet root-ca
as a bug on its own.
### XII.4 — Campaign completed: full 27-leg run (9 identities × 3 architectures), two real
FORTH-79 engine bugs found — bug 2 (`D+`/`D-`/`DNEGATE`) root-caused and FIXED 2026-09-11;
bug 1 (`M*`) still open, report only (2026-09-10, after §XIII's WIREBIND fix)
FORTH-79 engine bugs found — bug 2 (`D+`/`D-`/`DNEGATE`/`d_compare`) root-caused and FIXED
2026-09-11; bug 1 (`M*`) still open, report only (2026-09-10, after §XIII's WIREBIND fix)
All 27 legs run: `zuse` (auto-attached, exercised directly on Hera's own console) plus `rajames`
(the `bob` thumbdrive's actual registered identity -- see the naming-mismatch note below) and
@@ -2236,13 +2236,26 @@ project's standing rule (report, don't fix without being asked):**
`D+`). `M*`'s universal failure (bug 1, below) is untouched -- unrelated defect, `D+`/`D-`/
`DNEGATE`-specific fix only.
**Still open, not fixed (out of the scope actually requested):** the `d_compare()` helper
(used by `DMAX`/`DMIN`/`D<`/`D=`) uses the same bare-`unsigned long` pattern
(`double_words.c` lines ~170-171) and is equally vulnerable in principle -- never exercised by
`std79-exerciser.fth`, not fixed here since the fix request named `D+`/`D-`/`DNEGATE`
specifically. `double_word_d_two_star()`/`double_word_d_two_slash()` (`D2*`/`D2/`) use
**`d_compare()` fixed too, 2026-09-11** (on explicit follow-up request "fix d_compare too"):
the helper backing `DMAX`/`DMIN`/`D<`/`D=` (`double_words.c` lines ~170-171) had the identical
bare-`unsigned long` pattern, never exercised by `std79-exerciser.fth` itself. Switched to
`ucell_t`, same as the other three. Verified with four cases designed specifically to expose
the old 32-bit-truncation bug -- two doubles sharing the same high cell but with low cells
differing only in bits 32-63 (invisible to a 32-bit-truncated compare, e.g. `2^32` vs `0`):
`4294967296 0 0 0 D= .``0` (correctly *not* equal; the old bug would truncate both low
cells to `0` and wrongly report equal); `4294967296 0 0 0 DMIN SWAP D.``0` (correctly
picks the smaller double's low cell; the old bug's false "equal" tie-break would have
returned the wrong operand for this ordering); `0 0 4294967296 0 D< .``-1` and
`4294967296 0 0 0 D< .``0` (both directions of the same comparison, both correct). All
four pass identically on amd64, aarch64, and riscv64 -- confirms the fix and, since these
cases were never run before this fix, is the first real evidence `d_compare()` was ever
correct or incorrect on any architecture (the campaign itself never touched `DMAX`/`DMIN`/
`D<`/`D=` at all). `double_word_d_two_star()`/`double_word_d_two_slash()` (`D2*`/`D2/`) use
`unsigned long long` instead, which the C standard guarantees is at least 64-bit everywhere --
not part of this bug class, nothing to fix there.
were never part of this bug class, nothing to fix there.
**All four affected words (`D+`, `D-`, `DNEGATE`, `d_compare`) are now closed.** The only
remaining open item from this campaign is bug 1 (`M*`, below) -- a separate, unrelated defect.
`M*`'s universal failure (bug 1) is a separate, unrelated defect (present on every architecture,
so not a `long`-width issue) and would need its own trace starting from `M*`'s own double-cell
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-11T11:36:18Z -->
<!-- Generated by mkcapsule --manifest 2026-09-11T11:42:38Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -172,8 +172,11 @@ static int d_compare(cell_t d1h, cell_t d1l, cell_t d2h, cell_t d2l) {
if (d1h < d2h) return -1;
if (d1h > d2h) return 1;
unsigned long ul1 = (unsigned long) d1l;
unsigned long ul2 = (unsigned long) d2l;
// ucell_t, not plain unsigned long -- see double_word_d_plus()'s
// matching comment (unsigned long is only 32-bit on some bare-metal
// aarch64 cross-compile targets).
ucell_t ul1 = (ucell_t) d1l;
ucell_t ul2 = (ucell_t) d2l;
return (ul1 > ul2) - (ul1 < ul2);
}