From 5ac52f19d6ab9e6458fa89eedac36e39a2fd1dd1 Mon Sep 17 00:00:00 2001 From: rajames Date: Fri, 2 Oct 2026 22:52:27 -0400 Subject: [PATCH] test(v4.0.0): execute Q.+ and Q.- against v3 Q48.16 DECOMPOSITION.md 5.26 makes Q.+ and Q.- the D+ and D- words. They are checked against v3's q48_add/q48_sub (uint64_t a + b, a - b, wrapping) on every pair of 15 edge Q values (0, ulp, 0.5, 1.0, 1.5, -1.0, -ulp, values across the 32-bit seam, Q max and min, +-12345.0) and 20000 pseudo-random pairs, 2682 of which overflow Q48.16. - 32-bit cells: bit-for-bit v3's result, overflow wrap included. - 64-bit cells, with a Q value as a sign-extended double: the low cell is v3's result; on overflow the high cell holds the true carry where v3 wraps. Whether a Q value is one cell or two on a 64-bit node is not ruled; recorded as open in 5.26. Co-Authored-By: Claude Opus 5.5 --- docs/v4.0.0/DECOMPOSITION.md | 2 +- v4/tests/test_foundation.c | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/v4.0.0/DECOMPOSITION.md b/docs/v4.0.0/DECOMPOSITION.md index a2c96efd..c85b7678 100644 --- a/docs/v4.0.0/DECOMPOSITION.md +++ b/docs/v4.0.0/DECOMPOSITION.md @@ -644,7 +644,7 @@ word. Per D-8, v4 Q values are signed. | Word | Fate | Notes | | --- | --- | --- | -| `Q.+` `Q.-` | CAP | `D+`, `D-` | +| `Q.+` `Q.-` | CAP | `D+`, `D-` — executed on the golden model (2026-10-02) against v3's `q48_add`/`q48_sub` (`uint64_t` wrapping). At 32-bit cells the result is bit-for-bit v3's, overflow included. **Open:** at 64-bit cells a Q value as a sign-extended double has the same low cell as v3, but on Q48.16 overflow the high cell holds the true carry where v3 wraps. Whether a Q value is one cell or two on a 64-bit node is not yet ruled. | | `Q.*` | CAP | 64×64 product from four `UM*` partial products, shifted right 16. | | `Q./` | CAP | Shifted long division. **Division by zero sets an error** (v3 returned 0 silently). | | `Q.ABS` `Q.NEG` | CAP | `DABS`, `DNEGATE` | diff --git a/v4/tests/test_foundation.c b/v4/tests/test_foundation.c index d7ae7676..299d4d8e 100644 --- a/v4/tests/test_foundation.c +++ b/v4/tests/test_foundation.c @@ -25,6 +25,7 @@ #include "v4/asm.h" #include "v4/testcode.h" #include "v4/umul.h" +#include #include static int failures = 0, checks = 0; @@ -455,6 +456,46 @@ static int smrem_exact(v4_cell q, v4_cell nn, v4_cell r) return call(w_smrem, 3, (v4_cell)dl, (v4_cell)dh, nn) && left2(r, q); } +/* Q48.16 (section 5.26). v3's Q.+ and Q.- are uint64_t a + b and a - b, + * wrapping (v3/include/q48_16.h). Here a Q value is a double: at 32-bit + * cells its two halves, at 64-bit cells the value sign-extended (D-8). */ +static void qcells(uint64_t q, v4_cell *lo, v4_cell *hi) +{ +#if V4_CELL_BITS == 32 + *lo = (v4_cell)(v4_ucell)(q & 0xFFFFFFFFu); + *hi = (v4_cell)(v4_ucell)(q >> 32); +#else + *lo = (v4_cell)(v4_ucell)q; + *hi = (q >> 63) ? V4_ALL_ONES : 0; +#endif +} + +/* Run Q.+ (the D+ word) or Q.- (the D- word) on a and b and compare with + * v3's 64-bit wrapping result. At 32-bit cells the double must equal v3's + * value bit for bit. At 64-bit cells the low cell must equal v3's value and + * the high cell must be the true sign of the unwrapped result. */ +static int q_matches_v3(v4_cell word, uint64_t a, uint64_t b, uint64_t v3) +{ + v4_cell al, ah, bl, bh, rl, rh; + qcells(a, &al, &ah); + qcells(b, &bl, &bh); + if (!call4(word, al, ah, bl, bh)) return 0; +#if V4_CELL_BITS == 32 + qcells(v3, &rl, &rh); + return left2(rl, rh); +#else + { + v4_ucell xl, xh, nl, nh; + if (word == w_dminus) { dneg((v4_ucell)bl, (v4_ucell)bh, &nl, &nh); } + else { nl = (v4_ucell)bl; nh = (v4_ucell)bh; } + dadd((v4_ucell)al, (v4_ucell)ah, nl, nh, &xl, &xh); + rl = (v4_cell)(v4_ucell)v3; + rh = (v4_cell)xh; + return (v4_ucell)rl == xl && left2(rl, rh); + } +#endif +} + /* Stack headroom. Run `word` with `dfill` marked cells under the canary and * `rfill` marked cells under its return address, and report whether the * results, the canary and every marked cell come back intact. The F18 stacks @@ -725,6 +766,37 @@ int main(void) } } + /* Q.+ and Q.- (section 5.26: the D+ and D- words) against v3. */ + { + static const uint64_t qv[] = { + 0, 1, 0x8000u, 0x10000u, 0x18000u, /* 0, ulp, 0.5, 1.0, 1.5 */ + (uint64_t)0 - 0x10000u, (uint64_t)0 - 1u, /* -1.0, -ulp */ + 0xFFFFu, 0x10001u, 0xFFFFFFFFu, 0x100000000u, /* across the 32-bit seam */ + ((uint64_t)1 << 63) - 1u, (uint64_t)1 << 63, /* Q max, Q min */ + (uint64_t)12345 << 16, (uint64_t)0 - ((uint64_t)12345 << 16) + }; + const unsigned nq = sizeof qv / sizeof qv[0]; + uint64_t x = 0x9E3779B97F4A7C15u; + unsigned over = 0; + + for (unsigned i = 0; i < nq; i++) + for (unsigned j = 0; j < nq; j++) { + CHECK(q_matches_v3(w_dplus, qv[i], qv[j], qv[i] + qv[j]), "Q.+ [%u,%u]", i, j); + CHECK(q_matches_v3(w_dminus, qv[i], qv[j], qv[i] - qv[j]), "Q.- [%u,%u]", i, j); + } + for (unsigned i = 0; i < 20000; i++) { + uint64_t a, b; + x ^= x << 13; x ^= x >> 7; x ^= x << 17; a = x; + x ^= x << 13; x ^= x >> 7; x ^= x << 17; b = x; + if (i & 1u) a >>= (unsigned)(x & 63u); /* mixed magnitudes */ + if (i & 2u) b = (uint64_t)0 - b; + if (((a + b) ^ a) & ((a + b) ^ b) & ((uint64_t)1 << 63)) over++; + CHECK(q_matches_v3(w_dplus, a, b, a + b), "Q.+ random [%u]", i); + CHECK(q_matches_v3(w_dminus, a, b, a - b), "Q.- random [%u]", i); + } + printf(" Q.+ random cases that overflow Q48.16: %u of 20000\n", over); + } + /* Stack headroom of the two longest definitions. */ { static const v4_cell umstar_args[][4] = {