Root-cause the aarch64-only D+ DOUBLE-OVERFLOW bug (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

Confirmed the exact mechanism behind the aarch64-specific D+/D. divergence
found in the std79 exerciser campaign (commit cc81edf). The earlier
writeup's cell_t-width check was real but answered the wrong question --
cell_t is 64-bit everywhere, but double_word_d_plus()'s carry-detection
casts through plain `unsigned long`, and sizeof(unsigned long) is 4 (32-bit)
on this aarch64 bare-metal cross-compile target specifically (amd64 and
riscv64 both happen to have a 64-bit long). The low-cell addition silently
truncates to 32 bits, then widens back to cell_t via ordinary (non-sign-
extending) conversion, producing a wrong positive result_low whenever the
true 64-bit sum is negative -- confirmed live by splitting result_low into
hi/lo 32-bit halves: the real pushed value for `-5 S>D 3 S>D D+` is
+4294967294, not -2. D.'s overflow check is correct and is faithfully
reporting a genuinely malformed double; D+ is the actual defect.

vm.h already defines ucell_t for exactly this class of problem (same
conditional as cell_t, guaranteed width-matched on every target) --
print_number_formatted() (format_words.c) already uses it correctly, D+
doesn't. Also flagged (not exercised by the campaign, same bare-`unsigned
long` pattern, same latent risk): D-, DNEGATE, and the d_compare() helper
behind DMAX/DMIN/D</D=.

Report only, per this project's standing rule (report bugs, don't fix
without being asked) -- no source change in this commit, debug probes
added during the investigation were reverted after use.

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 06:41:39 -04:00
co-authored by Claude Sonnet 5
parent cc81edf00a
commit 91f7b39d3c
9 changed files with 27104 additions and 19 deletions
+49 -18
View File
@@ -2179,25 +2179,56 @@ project's standing rule (report, don't fix without being asked):**
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.
Investigated (not fixed): read `double_word_d_plus()` (`double_words.c`) and **Root-caused 2026-09-11 (still not fixed -- report only, per this project's standing
`format_word_d_dot()` (`format_words.c`). `D.`'s check is `(dhigh == 0) || (dhigh == -1 && rule).** First pass (2026-09-10) checked `cell_t`'s width and found it 64-bit on all three
dlow < 0)` -- correct FORTH-79 double-overflow semantics for a properly sign-extended result. architectures (`T08: -1 1 RSHIFT` prints the identical 19-digit `9223372036854775807`
`D+`'s carry-detection casts through `unsigned long` for the low-cell addition, which would be everywhere), and wrongly concluded from that alone that `D+`'s `unsigned long` carry-detection
a real per-architecture bug *if* `unsigned long`'s width differed from `cell_t`'s on any of cast couldn't be a width-mismatch bug -- **that conclusion was wrong**: `cell_t`'s width and
these targets -- checked and ruled out: `cell_t` is `int64_t` on amd64 and aarch64 explicitly plain C `long`'s width are two different things, and only the first was actually checked.
(`vm.h`'s `#if defined(__amd64__) || ... || defined(__aarch64__) || ...`), and `signed Confirmed directly via three targeted probes (`double_word_s_to_d()`, `double_word_d_plus()`,
long`/`unsigned long` (the `#else` branch, since `__riscv` isn't in that `#if`) on riscv64 -- `format_word_d_dot()` -- added, used, reverted): `sizeof(unsigned long)` prints `4` on this
confirmed 64-bit on all three empirically via `T08: -1 1 RSHIFT` printing the identical aarch64 build. `long`/`unsigned long` is 32-bit on this platform's aarch64 cross-compile
19-digit `9223372036854775807` on every architecture, which only a genuine 64-bit arithmetic target (bare-metal freestanding, not a hosted Linux/LP64 environment where 64-bit `long`
shift can produce. So this is not a cell-width mismatch; the actual mechanism producing a wrong would be guaranteed) -- amd64 and riscv64 both happen to use a 64-bit `long`, so the same code
`dhigh` specifically on aarch64, for this specific input, is still open. only breaks here.
**Next step, if picked up:** trace `D+`'s carry computation with a targeted probe on aarch64 Traced the actual data live for `-5 S>D 3 S>D D+`: `d1=(low=-5,high=-1)`, `d2=(low=3,high=0)`
specifically for the `-5 S>D 3 S>D D+` sequence, comparing the pushed `result_high` against the -- correct inputs, `S>D`'s sign-extension is fine. Inside `D+`,
expected `-1` right before `SWAP D.` runs -- narrows whether the defect is in `D+` itself, in `unsigned long ud1low = (unsigned long) d1low;` truncates `d1low` (a 64-bit `cell_t`,
`S>D`'s sign-extension, or somewhere in the stack-manipulation path between them. `M*`'s `0xFFFFFFFFFFFFFFFB`) down to 32 bits (`0xFFFFFFFB`) before the addition. The addition
universal failure (bug 1) is a separate, likely unrelated defect and would need its own trace (`0xFFFFFFFB + 3 = 0xFFFFFFFE`) happens entirely within that 32-bit width, and
starting from `M*`'s own double-cell multiply, not `D+`'s. `cell_t result_low = (cell_t) uresult_low;` then *widens* the 32-bit unsigned value back to a
64-bit signed `cell_t` via ordinary value-preserving conversion -- not the sign-extension the
code actually needs. Confirmed by splitting `result_low`'s real 64-bit value into hi/lo 32-bit
halves in the probe: `low_hi32=0 low_lo32=4294967294`, i.e. the true pushed value is
`+4294967294` (a large **positive** number), not `-2`. `carry` stays `0` (no overflow at the
truncated 32-bit width), so `result_high = d1high + d2high + carry = -1` -- correct on its
own, but paired with a `result_low` that is silently wrong. `D.`'s check
(`dhigh == -1 && dlow < 0`) then correctly, faithfully reports `DOUBLE-OVERFLOW` for this
malformed double -- `D.` is not the bug, `D+` is.
`vm.h` already defines `ucell_t` (`uint64_t` on amd64/aarch64, `unsigned long` on riscv64 --
the same conditional as `cell_t`, guaranteed to match its width on every target) for exactly
this class of problem; `print_number_formatted()` (`format_words.c`) already uses it
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
`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.
`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
multiply, not `D+`'s.
## XIII. Heap corruption under repeated WIREBIND attach/detach cycling — root-caused and CLOSED ## XIII. Heap corruption under repeated WIREBIND attach/detach cycling — root-caused and CLOSED
2026-09-10 2026-09-10
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-11T03:08:21Z --> <!-- Generated by mkcapsule --manifest 2026-09-11T10:40:38Z -->
<!-- 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. -->
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