Fix D+/D-/DNEGATE: 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

double_word_d_plus(), double_word_d_minus(), and double_word_dnegate()
(double_words.c) all cast through plain `unsigned long` for their carry/
borrow-detection arithmetic. On this aarch64 bare-metal cross-compile
target, unsigned long is 32-bit (confirmed: sizeof(unsigned long)==4) --
amd64 and riscv64 both happen to have a 64-bit long, so the identical code
only broke on aarch64. The low-cell arithmetic silently truncated to 32
bits, then widened back to cell_t via ordinary (non-sign-extending)
conversion, producing a wrong result whenever the true 64-bit result was
negative -- D. then correctly, faithfully reported DOUBLE-OVERFLOW on the
resulting malformed double.

vm.h already defines ucell_t for exactly this: same conditional as cell_t,
guaranteed width-matched on every target. print_number_formatted()
(format_words.c) already used it correctly; these three words didn't.
Switched all three to ucell_t -- a one-word-class fix, no logic change.

Verified: rebuilt and booted all three architectures clean. T19 (D+) on
aarch64 now correctly prints -2, matching amd64/riscv64; T20 (DNEGATE)
unaffected everywhere. Additional manual cases beyond the original
exerciser, run live on aarch64 to specifically exercise the
>32-bit-magnitude path the old bug depended on: D- (-5-3=-8), DNEGATE on
2^33 (8589934592 -> -8589934592), D+ crossing the same boundary
(3+8589934592=8589934595) -- all correct.

d_compare() (backing DMAX/DMIN/D</D=) has the identical latent pattern but
is out of scope for this fix (not named in the request, never exercised by
the campaign) -- left open, flagged in FABRIC-3.md. M*'s separate,
universal-across-all-three-architectures DOUBLE-OVERFLOW bug is also
untouched -- unrelated defect, not part of this fix.

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:37:57 -04:00
co-authored by Claude Sonnet 5
parent 91f7b39d3c
commit bea8d7436a
10 changed files with 27227 additions and 28 deletions
+35 -17
View File
@@ -2142,7 +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 — reported, NOT fixed (2026-09-10, after §XIII's WIREBIND fix)
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)
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
@@ -2173,14 +2174,14 @@ project's standing rule (report, don't fix without being asked):**
`D.`'s own overflow check, or both), not architecture-specific. Universal, 100% reproducible
across all 9 identities × 3 architectures.
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only.**
`T19: -5 S>D 3 S>D D+ SWAP D. CR` (computing -5 + 3 in double precision) correctly prints `-2`
on amd64 and riscv64, for all 9 identities -- but prints `DOUBLE-OVERFLOW` on aarch64, for all
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only — root-caused
2026-09-11, FIXED 2026-09-11 (`D+`/`D-`/`DNEGATE`).**
`T19: -5 S>D 3 S>D D+ SWAP D. CR` (computing -5 + 3 in double precision) correctly printed `-2`
on amd64 and riscv64, for all 9 identities -- but printed `DOUBLE-OVERFLOW` on aarch64, for all
9 identities, 100% consistently. A genuine cross-ISA divergence, not a flaky/intermittent
result.
**Root-caused 2026-09-11 (still not fixed -- report only, per this project's standing
rule).** First pass (2026-09-10) checked `cell_t`'s width and found it 64-bit on all three
**Root-caused 2026-09-11.** First pass (2026-09-10) checked `cell_t`'s width and found it 64-bit on all three
architectures (`T08: -1 1 RSHIFT` prints the identical 19-digit `9223372036854775807`
everywhere), and wrongly concluded from that alone that `D+`'s `unsigned long` carry-detection
cast couldn't be a width-mismatch bug -- **that conclusion was wrong**: `cell_t`'s width and
@@ -2213,18 +2214,35 @@ project's standing rule (report, don't fix without being asked):**
correctly. `D+`'s carry-detection using bare `unsigned long` instead is the actual defect --
a one-word class of bug (`unsigned long` should be `ucell_t`), not a logic error.
**Wider scope, not yet empirically exercised:** `double_word_d_minus()` (`D-`),
`double_word_dnegate()` (`DNEGATE`), and the `d_compare()` helper (used by `DMAX`/`DMIN`/
`D<`/`D=`) all use the identical bare-`unsigned long` pattern (`double_words.c` lines
~111-113, ~136, ~170-171) and are equally vulnerable in principle on any platform where
**Fixed 2026-09-11, on explicit request ("fix the D+/D-/DNEGATE bugs using ucell_t"):**
`double_word_d_plus()`, `double_word_d_minus()` (`D-`), and `double_word_dnegate()`
(`DNEGATE`) all shared the identical bare-`unsigned long` pattern (`double_words.c` lines
~83-85, ~111-113, ~136) and were all equally vulnerable in principle on any platform where
`long` is narrower than `cell_t`. `DNEGATE` was exercised by the campaign (`T20: -5 S>D
DNEGATE SWAP D. CR`) and happened to print the correct `5` on all three architectures --
traced why: for this specific small input, the 32-bit-truncated magnitude computation still
produces the mathematically correct positive result even without proper 64-bit sign
extension, so the bug is latent, not absent, for `DNEGATE` here. `D-`/`DMAX`/`DMIN`/`D</D=`
were never exercised by `std79-exerciser.fth` 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.
DNEGATE SWAP D. CR`) and happened to print the correct `5` on all three architectures for
that specific small input -- traced why: the 32-bit-truncated magnitude computation still
produces the mathematically correct positive result without proper 64-bit sign extension for
a value that small, so the bug was latent, not absent, there. `D-` was never exercised by
`std79-exerciser.fth` at all. All three now use `ucell_t` in place of `unsigned long`, same
pattern as `print_number_formatted()`'s existing correct usage.
**Verified:** rebuilt and booted all three architectures (amd64, aarch64, riscv64) clean.
`T19` on aarch64 now correctly prints `-2` (matching amd64/riscv64), `T20` (`DNEGATE`)
unaffected on all three. Additional manual cases beyond the original exerciser, run live on
aarch64 to specifically exercise the >32-bit-magnitude path the old bug depended on:
`-5 S>D 3 S>D D- SWAP D.``-8` (correct); `8589934592 S>D DNEGATE SWAP D.``-8589934592`
(correct, `8589934592` = 2^33, well past the 32-bit boundary the bug truncated at);
`3 S>D 8589934592 S>D D+ SWAP D.``8589934595` (correct, same boundary-crossing check for
`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
`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.
`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