test(v4.0.0): D-10 two-cell Q48.16; execute Q.ABS and Q.NEG against v3

D-10 (ruled 2026-10-02): a Q48.16 value is two cells at every cell
width, so every Q word is the same double word on 32- and 64-bit nodes.
Recorded in DECOMPOSITION.md section 3 and 5.26; the open note on the
Q.+/Q.- row is resolved by it.

Q.ABS and Q.NEG are the DABS and DNEGATE words. Checked against v3's
q48_abs and 0 - q on the 15 edge Q values and 20000 pseudo-random
values, at 32- and 64-bit cells, optimised and ASan+UBSan:
- 32-bit cells: bit-for-bit v3, including v3's wrap of Q min to itself.
- 64-bit cells: the low cell is v3's; the high cell is the true sign
  (so ABS and NEG of Q min give +2^63 rather than wrapping), per D-10.
Breaking DNEGATE's carry fails Q.NEG, Q.ABS and Q.- checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-02 22:54:40 -04:00
co-authored by Claude Opus 5.5
parent 4f493fa317
commit d948e0c1a3
2 changed files with 46 additions and 5 deletions
+4 -3
View File
@@ -171,6 +171,7 @@ definition below depends on one, it says so.
| **D-7** | `ROLL` semantics. | **Moot.** `ROLL` is retired under D-2. |
| **D-8** | Q48.16 signedness. | **Signed.** v3's unsigned comparisons and `Q.FROM-INT` clamping are retired. |
| **D-9** | Instruction word width on a 64-bit-cell host (ruled 2026-10-02). | **32 bits at every cell width.** Six 5-bit slots plus 2 spare bits, as in §1.2. On a 64-bit host the instruction word is the low 32 bits of the cell and the high half is ignored, so compiled code is identical at both widths. Only data and `@p` literals are a full cell wide. |
| **D-10** | Q48.16 width on a 64-bit-cell node (ruled 2026-10-02). | **Two cells at every cell width.** A Q value is a signed double on 32- and 64-bit nodes alike, so every Q word is the same double word at both widths. At 32-bit cells this is bit-for-bit v3's 64-bit Q. At 64-bit cells the low cell is v3's value, and where v3 wraps on Q48.16 overflow (a sum past Q max, `ABS` or `NEG` of Q min) the high cell carries the true result instead. |
**Consequences of D-2 that every definition must respect.** The data stack holds 10 items and the
return stack 9, and every `call`, `FOR`, `DO` loop frame and `push` uses return-stack slots. Nesting
@@ -640,14 +641,14 @@ index should use them; they cost one instruction per iteration.
### 5.26 Q48.16 fixed-point math
A Q48.16 value is 64 bits, so on a 32-bit node it occupies **two cells** and every Q word is a double
word. Per D-8, v4 Q values are signed.
word. Per D-10 it occupies two cells on a 64-bit node too. Per D-8, v4 Q values are signed.
| Word | Fate | Notes |
| --- | --- | --- |
| `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.+` `Q.-` | CAP | `D+`, `D-` — executed on the golden model (2026-10-02) against v3's `q48_add`/`q48_sub` (`uint64_t` wrapping): bit-for-bit at 32-bit cells; at 64-bit cells the low cell is v3's, per D-10. |
| `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` |
| `Q.ABS` `Q.NEG` | CAP | `DABS`, `DNEGATE` — executed on the golden model (2026-10-02) against v3's `q48_abs` and `0 - q`: bit-for-bit at 32-bit cells, including v3's wrap of Q min to itself; at 64-bit cells the low cell is v3's and the high cell is the true sign, per D-10. |
| `Q.LOG` `Q.EXP` `Q.SQRT` `Q.SIN` `Q.COS` | CAP | Algorithms ported from `q48_words.c`; the hosted C versions are the golden model. |
| `Q.FROM-INT` | CAP | `S>D` shifted left 16. Negative values are no longer clamped to 0. |
| `Q.TO-INT` | CAP | Shift right 16, take the low cell. |
+42 -2
View File
@@ -15,8 +15,9 @@
* built as q*n + r. D+ is the call-free version in 5.7, checked against C
* over every quadruple of the edge vectors and pseudo-random pairs. /MOD,
* U>, ABS, S>D, DABS, M+, D-, D0= and D= are as written in section 5 and
* checked against C. Q.+ and Q.- (section 5.26: the D+ and D- words) are
* checked against v3's 64-bit wrapping q48_add and q48_sub. Every word is
* checked against C. Q.+, Q.-, Q.ABS and Q.NEG (section 5.26: the D+, D-,
* DABS and DNEGATE words) are checked against v3's 64-bit q48_add, q48_sub,
* q48_abs and 0 - q, with Q values two cells wide at both widths (D-10). Every word is
* also probed for how much of the 10- and 9-deep circular stacks (D-2) it
* leaves to its caller.
*
@@ -498,6 +499,28 @@ static int q_matches_v3(v4_cell word, uint64_t a, uint64_t b, uint64_t v3)
#endif
}
/* The same for the one-operand Q words: Q.ABS (the DABS word) and Q.NEG
* (the DNEGATE word). `wl`, `wh` is the exact double result, which at
* 64-bit cells may differ from v3 in the high cell only (D-10). */
static int q1_matches_v3(v4_cell word, uint64_t a, uint64_t v3)
{
v4_cell al, ah, rl, rh;
v4_ucell xl, xh;
qcells(a, &al, &ah);
if (!call(word, 2, al, ah, 0)) return 0;
#if V4_CELL_BITS == 32
(void)xl; (void)xh;
qcells(v3, &rl, &rh);
return left2(rl, rh);
#else
if (word == w_dnegate || ah < 0) dneg((v4_ucell)al, (v4_ucell)ah, &xl, &xh);
else { xl = (v4_ucell)al; xh = (v4_ucell)ah; }
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
@@ -797,6 +820,23 @@ int main(void)
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);
/* Q.ABS and Q.NEG against v3's q48_abs and 0 - q. */
for (unsigned i = 0; i < nq; i++) {
uint64_t q = qv[i];
CHECK(q1_matches_v3(w_dabs, q, (q < ((uint64_t)1 << 63)) ? q : (uint64_t)0 - q),
"Q.ABS [%u]", i);
CHECK(q1_matches_v3(w_dnegate, q, (uint64_t)0 - q), "Q.NEG [%u]", i);
}
for (unsigned i = 0; i < 20000; i++) {
uint64_t q;
x ^= x << 13; x ^= x >> 7; x ^= x << 17; q = x;
if (i & 1u) q >>= (unsigned)(x & 63u);
if (i & 2u) q = (uint64_t)0 - q;
CHECK(q1_matches_v3(w_dabs, q, (q < ((uint64_t)1 << 63)) ? q : (uint64_t)0 - q),
"Q.ABS random [%u]", i);
CHECK(q1_matches_v3(w_dnegate, q, (uint64_t)0 - q), "Q.NEG random [%u]", i);
}
}
/* Stack headroom of the two longest definitions. */