Fix M/MOD: hand-rolled 128/64 bit-serial division (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

mixed_math_word_m_slash_mod() had the identical bug class already fixed
in M* (commit 9a09949): it reconstructed the dividend from
`(dhigh << 32) | (dlow & 0xFFFFFFFF)`, the same wrong "32-bit halves of a
64-bit value" assumption. Latent for small inputs (fits in 32 bits, so the
reconstruction coincidentally worked), confirmed genuinely broken for a
true wide double -- feeding M*'s own correct 10^24 output into it gave a
quotient/remainder wrong by many orders of magnitude and the wrong sign.

Unlike M*, this one can't just switch to __int128 -- __int128's own `/`/`%`
need libgcc's __udivti3/__umodti3 for 128-bit division, unavailable in
this freestanding, -nostdlib build (the exact constraint
src/starkernel/arch/amd64/timer.c's own doc comment already flagged:
__int128 multiply/shift-by-constant/compare/subtract all compile clean,
only division doesn't). Fixed instead with a hand-rolled unsigned
128-by-64-bit bit-serial (restoring) long division -- 128 iterations of
shift-by-1/compare/subtract only, all in the safe set. Operates on
magnitudes via unsigned negation from 0 (well-defined even for the
extreme negative edge); signs reapplied afterward matching the same C99
truncating-toward-zero convention the previous, narrower implementation
already used, unchanged.

Verified: rebuilt all three architectures, confirmed clean link with no
__udivti3/__umodti3 undefined-symbol errors. Booted and tested all three,
identical results: 1000000000000 1000000000000 M* SWAP 1000000 M/MOD ->
1000000000000000000 remainder 0 (exact division, the case that was wrong
by orders of magnitude before); three sign-combination cases all correct
(-+, +-, --), confirming sign handling survived the magnitude-only
rewrite. T15 (original small-input case) unaffected. Full 24-case
exerciser reran clean on all three, no regressions.

All bugs found by the std79 exerciser campaign, including this one found
while fixing another, are now closed.

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 10:28:34 -04:00
co-authored by Claude Sonnet 5
parent 9a09949c69
commit 70db955ac9
10 changed files with 27233 additions and 26 deletions
+37 -14
View File
@@ -2141,10 +2141,9 @@ produced one interesting, unconfirmed data point before the first panic: `-123 4
against the other two architectures once the campaign completes; not yet root-caused or reported
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 — BOTH root-caused and FIXED 2026-09-11 (bug 1 `M*`; bug 2 `D+`/`D-`/
`DNEGATE`/`d_compare`); a third, `M/MOD`, found while fixing bug 1, reported not fixed
(2026-09-10 campaign, after §XIII's WIREBIND fix)
### XII.4 — Campaign completed: full 27-leg run (9 identities × 3 architectures) — ALL bugs
found (`M*`, `D+`/`D-`/`DNEGATE`/`d_compare`, and `M/MOD` found along the way) root-caused and
FIXED, 2026-09-11 (2026-09-10 campaign, 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
@@ -2203,16 +2202,40 @@ project's standing rule (report, don't fix without being asked):**
proof the multiply itself is now genuinely 128-bit, not just sign-extension-correct for small
values). Full 24-case exerciser reran clean on all three, no regressions.
**Found while verifying, NOT fixed (report only, out of scope of what was requested):**
`mixed_math_word_m_slash_mod()` (`M/MOD`, the very next function in the same file) has the
*identical* bug class -- it reconstructs the dividend from `(dhigh << 32) | (dlow &
0xFFFFFFFF)`, the same wrong "32-bit halves of a 64-bit value" assumption `M*` had. Latent for
small inputs (`T15: 100000 S>D SWAP 7 M/MOD . .` -> `14285 5`, correct, because `100000` fits
entirely within 32 bits so the reconstruction coincidentally works), but confirmed genuinely
broken for a true wide double: feeding the now-fixed `M*`'s own correct large-magnitude output
into `M/MOD` (`1000000000000 1000000000000 M* 1000000 M/MOD . .`, expected quotient
`10^18`) produces `-6845471433603` remainder, `-99710` quotient -- wrong by many orders of
magnitude and the wrong sign. Not touched here; flag for a future fix request.
**`M/MOD` found broken by the same bug class, then FIXED 2026-09-11** (on explicit follow-up
request "fix it"): `mixed_math_word_m_slash_mod()` (the very next function in the same file)
reconstructed the dividend from `(dhigh << 32) | (dlow & 0xFFFFFFFF)`, the same wrong "32-bit
halves of a 64-bit value" assumption `M*` had. Latent for small inputs (`T15: 100000 S>D SWAP
7 M/MOD . .` -> `14285 5`, correct, because `100000` fits entirely within 32 bits so the
reconstruction coincidentally worked), confirmed genuinely broken for a true wide double
(feeding the fixed `M*`'s own `10^24` output into it gave a quotient wrong by many orders of
magnitude and the wrong sign).
Fixing this one is harder than `M*`: `__int128`'s own `/`/`%` operators need libgcc's
`__udivti3`/`__umodti3` for 128÷64 division, unavailable in this freestanding, `-nostdlib`
build (confirmed at link time -- exactly the constraint `timer.c`'s own doc comment already
flagged: `__int128` multiply/shift-by-constant/compare/subtract all compile clean, only
division doesn't). Fixed instead with a hand-rolled unsigned 128-bit-by-64-bit bit-serial
(restoring) long division -- 128 iterations of shift-by-1/compare/subtract only, all in the
safe set. Operates on magnitudes (via unsigned negation from `0`, well-defined even for the
extreme negative edge, unlike signed negation); signs reapplied afterward matching C99
truncating-toward-zero division (quotient sign = XOR of operand signs, remainder sign = the
dividend's own sign) -- the exact convention the previous, narrower implementation already
used via plain C `/`/`%`, unchanged here.
**Verified:** rebuilt all three architectures -- confirmed clean link with no
`__udivti3`/`__umodti3` undefined-symbol errors (would have failed exactly the way `timer.c`'s
own history warned about, had `__int128` division been used instead). Booted and tested all
three, identical results everywhere: `1000000000000 1000000000000 M* SWAP 1000000 M/MOD` ->
quotient `1000000000000000000` (10^18), remainder `0` (exact division, the case that was
wrong by many orders of magnitude before); three sign-combination cases
(`-100000 S>D SWAP 7 M/MOD` -> `-14285 -5`; `100000 S>D SWAP -7 M/MOD` -> `-14285 5`;
`-100000 S>D SWAP -7 M/MOD` -> `14285 -5`) all correct, confirming the sign-handling survived
the magnitude-only rewrite. `T15` (the original small-input case) unaffected. Full 24-case
exerciser reran clean on all three, no regressions.
**All bugs found by this campaign, including the one found while fixing another, are now
closed.**
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only — root-caused
2026-09-11, FIXED 2026-09-11 (`D+`/`D-`/`DNEGATE`).**