Fix D+/D-/DNEGATE: use ucell_t instead of unsigned long (FABRIC-3.md §XII.4)
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:
co-authored by
Claude Sonnet 5
parent
91f7b39d3c
commit
bea8d7436a
+35
-17
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-11T10:40:38Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-11T11:36:18Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
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
@@ -79,10 +79,13 @@ void double_word_d_plus(VM *vm) {
|
||||
cell_t d1high = vm_pop(vm);
|
||||
cell_t d1low = vm_pop(vm);
|
||||
|
||||
// Use unsigned arithmetic for carry detection
|
||||
unsigned long ud1low = (unsigned long) d1low;
|
||||
unsigned long ud2low = (unsigned long) d2low;
|
||||
unsigned long uresult_low = ud1low + ud2low;
|
||||
// Use unsigned arithmetic for carry detection. ucell_t (vm.h), not plain
|
||||
// unsigned long -- unsigned long is only 32-bit on some bare-metal
|
||||
// aarch64 cross-compile targets, silently truncating this addition and
|
||||
// losing the sign-extension D. depends on (FABRIC-3.md §XII.4).
|
||||
ucell_t ud1low = (ucell_t) d1low;
|
||||
ucell_t ud2low = (ucell_t) d2low;
|
||||
ucell_t uresult_low = ud1low + ud2low;
|
||||
|
||||
cell_t result_low = (cell_t) uresult_low;
|
||||
cell_t carry = (uresult_low < ud1low) ? 1 : 0;
|
||||
@@ -107,10 +110,11 @@ void double_word_d_minus(VM *vm) {
|
||||
cell_t d1high = vm_pop(vm);
|
||||
cell_t d1low = vm_pop(vm);
|
||||
|
||||
// Use unsigned arithmetic for borrow detection
|
||||
unsigned long ud1low = (unsigned long) d1low;
|
||||
unsigned long ud2low = (unsigned long) d2low;
|
||||
unsigned long uresult_low = ud1low - ud2low;
|
||||
// Use unsigned arithmetic for borrow detection. ucell_t, not plain
|
||||
// unsigned long -- see double_word_d_plus()'s matching comment.
|
||||
ucell_t ud1low = (ucell_t) d1low;
|
||||
ucell_t ud2low = (ucell_t) d2low;
|
||||
ucell_t uresult_low = ud1low - ud2low;
|
||||
|
||||
cell_t result_low = (cell_t) uresult_low;
|
||||
cell_t borrow = (ud1low < ud2low) ? 1 : 0;
|
||||
@@ -132,8 +136,9 @@ void double_word_dnegate(VM *vm) {
|
||||
cell_t dhigh = vm_pop(vm);
|
||||
cell_t dlow = vm_pop(vm);
|
||||
|
||||
// Negate: ~d + 1
|
||||
unsigned long new_low = ~(unsigned long) dlow + 1;
|
||||
// Negate: ~d + 1. ucell_t, not plain unsigned long -- see
|
||||
// double_word_d_plus()'s matching comment.
|
||||
ucell_t new_low = ~(ucell_t) dlow + 1;
|
||||
cell_t new_dhigh = ~dhigh + (new_low == 0 ? 1 : 0);
|
||||
|
||||
vm_push(vm, (cell_t) new_low);
|
||||
|
||||
Reference in New Issue
Block a user