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.
|
as a bug on its own.
|
||||||
|
|
||||||
### XII.4 — Campaign completed: full 27-leg run (9 identities × 3 architectures), two real
|
### 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`
|
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
|
(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
|
`D.`'s own overflow check, or both), not architecture-specific. Universal, 100% reproducible
|
||||||
across all 9 identities × 3 architectures.
|
across all 9 identities × 3 architectures.
|
||||||
|
|
||||||
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only.**
|
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only — root-caused
|
||||||
`T19: -5 S>D 3 S>D D+ SWAP D. CR` (computing -5 + 3 in double precision) correctly prints `-2`
|
2026-09-11, FIXED 2026-09-11 (`D+`/`D-`/`DNEGATE`).**
|
||||||
on amd64 and riscv64, for all 9 identities -- but prints `DOUBLE-OVERFLOW` on aarch64, for all
|
`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
|
9 identities, 100% consistently. A genuine cross-ISA divergence, not a flaky/intermittent
|
||||||
result.
|
result.
|
||||||
|
|
||||||
**Root-caused 2026-09-11 (still not fixed -- report only, per this project's standing
|
**Root-caused 2026-09-11.** First pass (2026-09-10) checked `cell_t`'s width and found it 64-bit on all three
|
||||||
rule).** 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`
|
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
|
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
|
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 --
|
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.
|
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-`),
|
**Fixed 2026-09-11, on explicit request ("fix the D+/D-/DNEGATE bugs using ucell_t"):**
|
||||||
`double_word_dnegate()` (`DNEGATE`), and the `d_compare()` helper (used by `DMAX`/`DMIN`/
|
`double_word_d_plus()`, `double_word_d_minus()` (`D-`), and `double_word_dnegate()`
|
||||||
`D<`/`D=`) all use the identical bare-`unsigned long` pattern (`double_words.c` lines
|
(`DNEGATE`) all shared 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
|
~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
|
`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 --
|
DNEGATE SWAP D. CR`) and happened to print the correct `5` on all three architectures for
|
||||||
traced why: for this specific small input, the 32-bit-truncated magnitude computation still
|
that specific small input -- traced why: the 32-bit-truncated magnitude computation still
|
||||||
produces the mathematically correct positive result even without proper 64-bit sign
|
produces the mathematically correct positive result without proper 64-bit sign extension for
|
||||||
extension, so the bug is latent, not absent, for `DNEGATE` here. `D-`/`DMAX`/`DMIN`/`D</D=`
|
a value that small, so the bug was latent, not absent, there. `D-` was never exercised by
|
||||||
were never exercised by `std79-exerciser.fth` at all. `double_word_d_two_star()`/
|
`std79-exerciser.fth` at all. All three now use `ucell_t` in place of `unsigned long`, same
|
||||||
`double_word_d_two_slash()` (`D2*`/`D2/`) use `unsigned long long` instead, which the C
|
pattern as `print_number_formatted()`'s existing correct usage.
|
||||||
standard guarantees is at least 64-bit everywhere -- not part of this bug class.
|
|
||||||
|
**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,
|
`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
|
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
|
# 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. -->
|
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||||
<!-- Hand-written justifications and immutability notes live -->
|
<!-- Hand-written justifications and immutability notes live -->
|
||||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
<!-- 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 d1high = vm_pop(vm);
|
||||||
cell_t d1low = vm_pop(vm);
|
cell_t d1low = vm_pop(vm);
|
||||||
|
|
||||||
// Use unsigned arithmetic for carry detection
|
// Use unsigned arithmetic for carry detection. ucell_t (vm.h), not plain
|
||||||
unsigned long ud1low = (unsigned long) d1low;
|
// unsigned long -- unsigned long is only 32-bit on some bare-metal
|
||||||
unsigned long ud2low = (unsigned long) d2low;
|
// aarch64 cross-compile targets, silently truncating this addition and
|
||||||
unsigned long uresult_low = ud1low + ud2low;
|
// 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 result_low = (cell_t) uresult_low;
|
||||||
cell_t carry = (uresult_low < ud1low) ? 1 : 0;
|
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 d1high = vm_pop(vm);
|
||||||
cell_t d1low = vm_pop(vm);
|
cell_t d1low = vm_pop(vm);
|
||||||
|
|
||||||
// Use unsigned arithmetic for borrow detection
|
// Use unsigned arithmetic for borrow detection. ucell_t, not plain
|
||||||
unsigned long ud1low = (unsigned long) d1low;
|
// unsigned long -- see double_word_d_plus()'s matching comment.
|
||||||
unsigned long ud2low = (unsigned long) d2low;
|
ucell_t ud1low = (ucell_t) d1low;
|
||||||
unsigned long uresult_low = ud1low - ud2low;
|
ucell_t ud2low = (ucell_t) d2low;
|
||||||
|
ucell_t uresult_low = ud1low - ud2low;
|
||||||
|
|
||||||
cell_t result_low = (cell_t) uresult_low;
|
cell_t result_low = (cell_t) uresult_low;
|
||||||
cell_t borrow = (ud1low < ud2low) ? 1 : 0;
|
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 dhigh = vm_pop(vm);
|
||||||
cell_t dlow = vm_pop(vm);
|
cell_t dlow = vm_pop(vm);
|
||||||
|
|
||||||
// Negate: ~d + 1
|
// Negate: ~d + 1. ucell_t, not plain unsigned long -- see
|
||||||
unsigned long new_low = ~(unsigned long) dlow + 1;
|
// 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);
|
cell_t new_dhigh = ~dhigh + (new_low == 0 ? 1 : 0);
|
||||||
|
|
||||||
vm_push(vm, (cell_t) new_low);
|
vm_push(vm, (cell_t) new_low);
|
||||||
|
|||||||
Reference in New Issue
Block a user