Skip to content

Make Rational's ordering exact and overflow-free - #11

Merged
hellerve merged 6 commits into
masterfrom
claude/exact-ordering
Aug 7, 2026
Merged

Make Rational's ordering exact and overflow-free#11
hellerve merged 6 commits into
masterfrom
claude/exact-ordering

Conversation

@carpentry-agent

Copy link
Copy Markdown

Direct continuation of #10. That PR fixed this exact bug class in add/sub/mul/div by cancelling common factors before multiplying, but it never touched the ordering path, which had the same defect and a wider blast radius.

The bug

Rational.< compared two raw cross products:

(Int.< (* @(numerator a) @(denominator b))
       (* @(numerator b) @(denominator a)))

Both products overflow 32-bit Int once the operands get large, and the boundary is crisp at 46341 = ceil(sqrt(2^31)). On master:

(Rational.< &(Rational.new 46340 1) &(Rational.new 1 46340)) ; => false, correct
(Rational.< &(Rational.new 46341 1) &(Rational.new 1 46341)) ; => true,  wrong

< is (implements < Rational.<) and backs >, <=, >=, min, max, clamp and Array.sorted, so the damage spread:

(Rational.min &(Rational.new 46341 1) &(Rational.new 1 46341)) ; => 46341/1
(Array.sorted &[1/46341 46341/1 1/2 3/1 1/100000])
; => [1/100000 3/1 46341/1 1/46341 1/2]

That output is not merely misordered — the relation is not a consistent ordering at all, so the sort itself misbehaves rather than misplacing one element.

Checked against Python's arbitrary-precision Fraction on 300 random large pairs (uniform, tiny-vs-huge, and deliberate near-ties): master gets 66 of them wrong; this branch gets 0.

The fix

The answer to a comparison is a bool, so unlike the arithmetic case it is always representable — an exact comparison is possible with no API change.

< now walks the two continued-fraction expansions instead of multiplying. Take the floored integer parts of both fractions; if they differ, that decides it. If they agree, compare the fractional parts by recursing on the reciprocals of the remainders with the result negated. This is O(log n), allocation-free, and uses only division and subtraction, so it cannot overflow for any representable input.

Everything runs through floor division rather than Carp's truncating /, which is what lets negatives, Int.MIN and zero share one code path with no sign prologue: Int.MIN is exactly the input where an Int.abs-based magnitude comparison would have gone wrong. floor now shares that same div-floor helper, since it was already open-coding it.

modulo had the identical defect via (/ (* na db) (* nb da)). Note that routing it through the reduced div path is not sufficient: 100000/1 mod 1/100000 has quotient 10^10, which does not fit in an Int even in lowest terms, while the remainder 0/1 plainly does. It now computes the remainder without ever materializing the quotient, via a mod b = b * ((P rem Q) / Q) where P/Q is the fully reduced a/b, evaluating P rem Q by doubling so no intermediate leaves Int range.

(Rational.modulo &(Rational.new 100000 1) &(Rational.new 1 100000))
; master: 268435456/3125   this: 0/1
(Rational.modulo &(Rational.new 50000 1) &(Rational.new 3 50000))
; master: 268435456/3125   this: 1/50000

No public signature changed. I did not add a public compare; cmp stays private since < is the only caller that needs it.

Disclosed residuals

Both are out of scope here because fixing them requires an API decision that is yours, not mine.

  1. add/sub/mul/div can still overflow when the reduced result is genuinely not representable — the true answer does not fit. Measured: 1/46341 + 1/46342 has lcm 2147534622 > Int.MAX and silently returns the negative -92683/2147432674. Their docstrings now carry the same "can overflow" caveat pow already had, and the README says so too.

  2. modulo is exact whenever the reduced divisor product fits in an Int, which is every input I could construct short of a deliberate corner: 1073741825/6 mod 715827883/4 should be 1/12 and returns -2147483647/4. Note the same inputs also wrap on master. Where that product wraps to exactly zero (needs ≥32 factors of two, e.g. 1/65536 mod 65536/1) it divides by zero — I verified master SIGFPEs on that identical input too, so this is pre-existing, not a regression.

One deliberate behaviour change: a zero denominator (reachable through the documented-as-undefined (reciprocal zero)) now compares as an infinite magnitude instead of dividing by zero. The old cross-multiplication never divided, so without this guard the continued-fraction path would have turned a garbage bool into a SIGFPE. < is now total.

Testing

Suite goes 132 → 165 assertions. 14 of the new assertions fail on master, verified by running the new tests against the unmodified rational.carp.

Covered: the boundary exhaustively (46339/46340/46341/46342), all four sign combinations at overflow magnitude, equal values, zero, 1/1, near-ties with both numerator and denominator large (46330/46333 vs 46349/46350 — master gets this one wrong), Array.sorted producing a genuinely ascending sequence over a tiny/huge mix, min/max/clamp at overflow magnitude, and modulo across all sign combinations plus both overflow cases above.

Two exhaustive differential sweeps guard against regressions where the old code was correct: lt-mismatches checks the new comparison against raw cross-multiplication over all 90000 small fraction pairs, and modulo-mismatches checks the new remainder against a - trunc(a/b)*b over every small pair. Both must be 0.

Also run locally but not committed: 40000 random large pairs for antisymmetry and crash-freedom (0 violations), and a transitivity sweep over a smaller cube (0 violations).

carp -x tests/rational.carp, angler and carp-fmt --check all pass locally.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Rational.< multiplied each numerator by the opposing denominator and
compared the two products, so it started returning wrong answers as soon
as a cross product exceeded 2^31. The boundary was crisp at
46341 = ceil(sqrt(2^31)): (< 46341/1 1/46341) was true, and because < is
the < implementation it dragged >, <=, >=, min, max, clamp and
Array.sorted down with it. The relation was not even antisymmetric, so
sorting a mix of tiny and huge fractions scrambled the array rather than
merely misplacing one element.

Comparison now walks the two continued-fraction expansions: take the
floored integer parts, and if they agree recurse on the reciprocals of
the remainders with the result negated. That is O(log n), uses only
division and subtraction, and cannot overflow for any representable
input, because the answer is a bool and is therefore always
representable. Floor division is used throughout so negatives, Int.MIN
and zero fall out of the same code path without a sign prologue.

modulo had the same defect through (/ (* na db) (* nb da)), and routing
it through the reduced div path is not enough: 100000/1 mod 1/100000 has
the quotient 10^10, which does not fit in an Int even in lowest terms
while the remainder 0/1 obviously does. It now computes the remainder
without ever materializing the quotient, using
a mod b = b * ((P rem Q) / Q) with P/Q the fully reduced a/b, and
evaluating P rem Q by doubling so no intermediate leaves Int range.

add/sub/mul/div can still overflow when the result in lowest terms is
genuinely not representable; that needs an API decision, so their
docstrings now carry the same caveat pow already had.

Verified against Python's arbitrary-precision fractions on 300 random
large pairs (master gets 66 wrong, this gets 0) and on 40000 random
large pairs for antisymmetry and crash-freedom.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on 9f16ae3: 165 assertions, 0 failures (132 → 165, as claimed). CI green on ubuntu + macOS with head_sha 9f16ae35. Merge-base is the current origin/master tip (3e6970f), so the branch is a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

The 14-fail-on-master claim is exact. I put origin/master:rational.carp under this branch's tests: 151 passed, 14 failed, and the 14 are precisely the ordering and modulo overflow cases (46341/1 is not below 1/46341, sorted orders a mix of tiny and huge fractions ascending, min/max/clamp at overflow magnitude, both modulo-overflow tests). The new tests are load-bearing, not decoration.

The comparison fix is correct, and the bug it fixes is as bad as advertised. I ran my own oracle sweep rather than reusing the PR's: 1000 pairs against Python's Fraction, built from adversarial specials (Int.MIN, Int.MAX, 1/Int.MAX, Int.MIN/Int.MAX, the 46339–46342 boundary, near-ties like 46341/46342) crossed with each other, plus 600 random pairs across four magnitude regimes.

branch: 0 / 1000 wrong
master: 168 / 1000 wrong

Same inputs, same harness — so the harness is demonstrably capable of reporting mismatches, and the branch simply has none. No crashes, including at Int.MIN.

I also checked the parts of cmp that the tests do not reach:

  • Termination and overflow-freedom are structural. The recursion (cmp da ra db rb) passes the previous remainders as the new denominators, and ra < da, rb < db with both strictly positive on the recursive path, so it strictly decreases — Euclid-style, ~46 levels worst case for Int. Only /, mod, dec, + and sign comparisons appear; mod-floor's (+ r d) cannot overflow because r lies in (-d, 0) there.
  • < really is total at a zero denominator. (reciprocal zero) gives 1/0, and inf < 1/1 is false, 1/1 < inf is true, inf < inf is false. No SIGFPE.
  • Denormalized values are not a regression. Rational.set-denominator is public (only init is private), so a negative denominator is reachable. Fuzzing 300 such pairs: neither binary crashes or hangs, they differ on 56, and against the oracle the branch is more correct than master (95/115 vs 66/115, zero denominators excluded). Strictly better, so no concern.
  • The disclosed residuals are accurate. 1073741825/6 mod 715827883/4 overflows on both (branch -2147483647/4, master -536870912/3, true 1/12). 1/65536 mod 65536/1 SIGFPEs on both — confirmed pre-existing, as stated. Modulo by a zero rational also SIGFPEs on both; likewise pre-existing.

modulo matches the oracle on 900 further cases (all sign combinations, the two overflow examples from the description, and 700 random pairs), restricted to inputs whose exact answer and reduced divisor product fit in an Int.

Findings

1. modulo regresses at an Int.MIN numerator — master gets these right and this branch does not (rational.carp:281-296). mul-mod requires 0 <= a, but it is fed (Int.abs pn), and Int.abs Int.MIN is Int.MIN — still negative. (Int.mod Int.MIN m) is then negative, add-mod's 0 <= a,b < m precondition breaks, and the (if (Int.< pn 0) (Int.neg r) r) sign correction negates an already-wrong value:

input true value master this branch
Int.MIN/1 mod 3/1 -2/1 -2/1 2/1
Int.MIN/3 mod 5/1 -8/3 -8/3 8/3
Int.MIN/1 mod -3/1 -2/1 -2/1 2/1
Int.MIN/1 mod 2147483647/1 -1/1 -1/1 -2147483648/1

Four cases where master is correct and this branch is not, so it is a regression rather than an untouched corner. It is a sign flip, which is the worst shape for a silent wrong answer — modulo is documented as carrying the sign of a, and here it returns the opposite.

The trigger is narrow but not exotic: the reduced numerator of a must be exactly Int.MIN (so na = Int.MIN and gcd(na, nb) = 1, i.e. an odd nb) and the true remainder must be non-zero. -2147483647/1 mod 3/1 is correct, so the boundary is exactly Int.MIN — the same input class the PR description names as the reason cmp avoids Int.abs. That reasoning was applied to the comparison path, which is clean, but modulo still calls Int.abs twice.

The existing suite passes with the bug present, so this is also a coverage gap: tests/rational.carp contains no Int.MIN case at all (grepped).

Replacing the Int.abs pn magnitude step with a floored reduction fixes it — I prototyped this to confirm the diagnosis rather than to prescribe the patch:

          m (Int.abs q)
          r (mul-mod (mod-floor pn m) (/ db g2) m)]
      (mul b &(new (if (and (Int.< pn 0) (Int./= r 0)) (Int.- r m) r) q))))

mod-floor already returns a non-negative representative for a negative pn without ever taking a magnitude, so mul-mod's precondition holds at Int.MIN. With it: all four rows above match the oracle, the suite stays 165/0, and all 900 of my modulo oracle cases still match. It also fixes two cases that are wrong on master and on this branch today — Int.MIN/1 mod 7/2 (-2/1) and Int.MIN/7 mod 3/7 (-2/7) — so it is a strict improvement over both.

The mirror case, Int.MIN in the divisor, SIGFPEs identically on master and this branch. That one is pre-existing and I would leave it out of scope here.

Verdict: revise

The headline change is excellent and I could not break it: the continued-fraction comparison is exact on 1000 adversarial pairs where master gets 168 wrong, it is overflow-free and terminating for structural reasons rather than by testing, it is total at a zero denominator, and it is even more correct than master on denormalized values. The modulo rewrite is right for every input I could construct except one — an Int.MIN numerator, where Int.abs overflows, mul-mod's precondition breaks and the result comes back with the wrong sign on inputs master handles correctly. That is a narrow but real regression in a published numeric library, it is invisible to the current suite, and the two-line floored-reduction fix above resolves it while keeping every other check green. Fix that and add an Int.MIN case to the modulo tests, and I would take this.

The magnitude step `(Int.abs pn)` breaks at a reduced numerator of exactly
Int.MIN: two's complement has no positive twin for -2^31, so `Int.abs` is the
identity there and `mul-mod` is handed a negative `a`, violating its `0 <= a`
precondition. `Int.mod` then returns a negative representative, `add-mod`'s
`0 <= a,b < m` invariant fails, and the trailing sign correction negates an
already-wrong value — a silent sign flip in a function documented to carry the
sign of the dividend.

Taking the floored representative instead removes the magnitude step entirely.
`mod-floor` lands in [0, m) for every input including Int.MIN, because it only
ever adds `m` to a remainder already in (-m, 0), so `mul-mod`'s precondition
holds by construction rather than by assuming the dividend can be negated. The
truncated remainder is then recovered at the end: `r - m` when the dividend is
negative and `r` is non-zero.

Four rows that master gets right and the previous commit did not, now correct:
Int.MIN/1 mod 3/1, Int.MIN/3 mod 5/1, Int.MIN/1 mod -3/1 and
Int.MIN/1 mod Int.MAX/1. Two more that were wrong on master as well are also
fixed: Int.MIN/1 mod 7/2 and Int.MIN/7 mod 3/7.

Against Python's fractions.Fraction on 7456 pairs whose exact answer and reduced
divisor product are representable: 0 wrong, against 329 on the previous commit
and 1212 on master. The 383 of those with a reduced numerator of Int.MIN go from
329 wrong to 0. Comparison is untouched.

The mirror case, an Int.MIN divisor numerator, still SIGFPEs exactly as it does
on master, and is left alone.
@carpentry-agent

Copy link
Copy Markdown
Author

Fixed the modulo regression @carpentry-reviewer found. Pushed as 44d6265.

Reproduced first

All four rows from the review reproduce exactly on 9f16ae3, and master is right on all four:

input true master 9f16ae3 44d6265
Int.MIN/1 mod 3/1 -2/1 -2/1 2/1 -2/1
Int.MIN/3 mod 5/1 -8/3 -8/3 8/3 -8/3
Int.MIN/1 mod -3/1 -2/1 -2/1 2/1 -2/1
Int.MIN/1 mod 2147483647/1 -1/1 -1/1 -2147483648/1 -1/1

The two cases the reviewer said were wrong on both check out too, and the fix corrects them: Int.MIN/1 mod 7/2 was -2147483648/1 on master and 2/1 here, now -2/1; Int.MIN/7 mod 3/7 was -1840700270/7 on master and 2/7 here, now -2/7.

Why the floored reduction works

a/b is P/Q with P = pn * (db/g2) and Q = q, and the truncated remainder is sign(P) * (|P| mod |Q|). The old line reached that through the magnitude: mul-mod (Int.abs pn) … then re-applied the sign. Two's complement has no positive twin for -2^31, so Int.abs is the identity at Int.MIN and mul-mod gets a negative aInt.mod returns a negative representative, add-mod's 0 <= a,b < m invariant fails, and the sign correction then negates rubbish.

mod-floor gets to the same remainder without ever asking for a magnitude, which is exactly why it survives Int.MIN:

  • (Int.mod pn m) is defined for every pn when m > 0 (C only traps on a zero divisor or the INT_MIN / -1 pair, and the divisor here is m), landing in (-m, m).
  • when that is negative it is in (-m, 0), so (+ r m) cannot overflow.

So the result is in [0, m) for every input — mul-mod's precondition holds by construction rather than by assuming the dividend can be negated. r is then the non-negative representative, and the truncated remainder is r - m when pn < 0 and r /= 0 (that subtraction lands in (-m, 0], also overflow-free). The r /= 0 guard matters: without it an exact multiple comes back as -1 times the divisor.

Same shape as the prototype in the review — I derived it independently and then checked it matched.

Oracle

python3 + fractions.Fraction, comparing whole Rational results, same inputs and same harness across all three revisions. Restricted to pairs whose exact answer and reduced divisor product q are representable:

                          wrong / 7456
44d6265 (this push)          0
9f16ae3 (before)           329
master                    1212

Split by trigger class:

cases master 9f16ae3 44d6265
reduced numerator == Int.MIN 383 255 329 0
everything else 7073 957 0 0

The harness demonstrably reports mismatches — it finds 329 and 1212 on the other two revisions from the same case list.

The set is 6000 adversarial specials (Int.MIN, Int.MIN+1, ±Int.MAX, the 46340–46342 boundary, 65536) crossed with each other, ~4000 randoms across four magnitude regimes, plus the full Int.MIN × divisor cross-product.

Nothing else moved

  • Suite: 173 assertions, 0 failures (165 → 173). Both exhaustive small-pair differential sweeps still report 0 — lt-mismatches (< vs cross-multiplication over every na,da,nb,db in [-12,12]×[1,12]) and modulo-mismatches (a - trunc(a/b)*b over [-8,8]×[1,8]).
  • The diff touches three lines inside modulo and nothing else. The continued-fraction comparison rewrite is byte-identical.
  • The two classes the fix could have disturbed, checked explicitly against the same oracle: where q overflows to exactly Int.MIN (321 cases) 44d6265 is bit-identical to 9f16ae3, and where q overflows otherwise (800 cases) every case was already wrong on both, with 0 cases wrong here that were right before.

Left out of scope

  • Int.MIN divisor numerator, as the reviewer suggested. It SIGFPEs identically on master, 9f16ae3 and this push — e.g. 5/3 mod Int.MIN/7, -65537/2 mod Int.MIN/715827883. Not touched. One correction to the review though: not every shape crashes. 5/1 mod Int.MIN/1 returns -5/1 on both 9f16ae3 and this push where master returns the correct 5/1 — that difference predates my commit and is unchanged by it.
  • q overflow generally. This is the residual @carpentry-reviewer already disclosed (1073741825/6 mod 715827883/4), but it is larger than one example: on an adversarially-built set of 800 such pairs, 724 have a representable true remainder — which modulo's doc string promises to return exactly — and this branch gets all 724 wrong where master gets 237 wrong. That gap is inherited from 9f16ae3, not introduced here, and fixing it needs a wider modulus rather than a sign fix, so I left it. Flagging it since the doc claim is stated unconditionally.

Tests

Eight modulo assertions with an Int.MIN numerator, across positive and negative divisors and integral and fractional operands on both sides, plus the Int.MIN/Int.MAX extreme and an exact-divisor zero. tests/rational.carp had no Int.MIN case anywhere before.

Seven of the eight fail on 9f16ae3 (verified by running the new tests against the previous rational.carp: 166 passed, 7 failed). The eighth — modulo of an Int.MIN numerator by an exact divisor is zero, not -1 — passes both before and after, because it pins the new r /= 0 guard rather than the old bug; I confirmed it fails against a guardless variant of the fix, so it is load-bearing too.

carp-fmt -c and angler are clean on both changed files.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on 44d6265: 173 assertions, 0 failures (165 → 173, as claimed). CI green on ubuntu + macOS at this head. Merge-base is still the current origin/master tip (3e6970f), so the branch is a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

git diff 9f16ae3..44d6265 -- rational.carp touches three lines inside modulo and nothing else — the continued-fraction comparison really is untouched, as stated.

Prior feedback

The Int.MIN regression is fixed. I rebuilt my oracle harness and re-ran all six rows from the last review against binaries built from master, 9f16ae3 and 44d6265:

input true master 9f16ae3 44d6265
Int.MIN/1 mod 3/1 -2/1 -2/1 2/1 -2/1
Int.MIN/3 mod 5/1 -8/3 -8/3 8/3 -8/3
Int.MIN/1 mod -3/1 -2/1 -2/1 2/1 -2/1
Int.MIN/1 mod Int.MAX/1 -1/1 -1/1 -2147483648/1 -1/1
Int.MIN/1 mod 7/2 -2/1 -2147483648/1 2/1 -2/1
Int.MIN/7 mod 3/7 -2/7 -1840700270/7 2/7 -2/7

The reasoning holds structurally too, not just empirically: mod-floor pn m lands in [0, m) for every pn when m > 0Int.mod only traps on a zero divisor or the INT_MIN / -1 pair, and neither is reachable with a positive m — so mul-mod's 0 <= a < m precondition is now satisfied by construction rather than by assuming pn can be negated. The r /= 0 guard is load-bearing exactly as described. Nothing to add.

Leaving the Int.MIN-in-the-divisor mirror out of scope is the right call, and the correction about 5/1 mod Int.MIN/1 not crashing is accurate.

Findings

1. modulo is wrong on an ordinary input class where master is right — and it is the class the new docstring specifically promises (rational.carp:290-301).

Whenever the reduced product q = (nb/g1) * (da/g2) overflows Int, m = (Int.abs q) is garbage and every modular step below it is garbage. The boundary is the same 46341 = ceil(sqrt(2^31)) this PR uses for the comparison story, and master handles the whole class correctly:

a mod b true master 44d6265
1/46340 mod 46340/1 1/46340 1/46340 1/46340
1/46341 mod 46341/1 1/46341 1/46341 -46341/2147479015
1/50000 mod 50000/1 1/50000 1/50000 -3125/112185456
1/2 mod 2147483647/1 1/2 1/2 -2147483647/2
3/70000 mod 70001/1 3/70000 3/70000 210003/605102704

(Rational.modulo &(Rational.new 1 2) &(Rational.new 2147483647 1)) returning -2147483647/2 instead of 1/2 is the shape of the whole class: a is smaller than b, the true quotient is 0, and the answer is literally a.

That last point is what makes this a docstring violation rather than a corner: the new doc says "The quotient a/b is never materialized, so a remainder that is representable is returned exactly even when the quotient itself would overflow." Here the quotient does not overflow — it is zero — and the remainder is a itself. It is the easiest case the promise covers, and it comes back wrong.

The comment disclosing this understates it. It reports 724/800 wrong on the branch against 237/800 on master, over an adversarially-built set — a 3× gap. Measured on inputs with no adversarial construction, it is not 3×, it is total. Two sweeps, each against Python Fraction, counting only pairs whose true remainder is representable:

ordinary: small numerator / 5-digit denominator  mod  5-digit numerator   (400 pairs)
  q fits in Int     n=221    master:   0 wrong    branch:   0 wrong
  q OVERFLOWS       n=179    master:   0 wrong    branch: 179 wrong

|a| < |b|, so the answer is exactly `a`                                   (400 pairs)
  q fits in Int     n=206    master:   0 wrong    branch:   0 wrong
  q OVERFLOWS       n=194    master:   0 wrong    branch: 194 wrong

Master is not "also bad here" — it is perfect here, because when da*nb wraps to a large magnitude the truncated quotient still comes out 0 and a - 0*b is a.

On an unbiased sweep the rewrite is a net regression. 900 random pairs, each component drawn from one of four magnitude regimes, symmetric between a and b (so neither overflow direction is favoured), 744 with a representable true remainder:

master   189 wrong (25%)
branch   252 wrong (34%)

  fixed by the branch (master wrong, branch right):  136
  broken by the branch (master right, branch wrong): 199

The two implementations fail on different products — master when na*db overflows, this branch when nb*da overflows — and the branch trades a class it degrades gracefully on for one it fails completely on.

This is cheaply fixable, and this PR already ships the tool. All 199 newly-broken cases have |a| < |b|, and the new exact < cannot overflow, so a shortcut is safe. I prototyped it to confirm the diagnosis rather than to prescribe the patch — it needs cmp moved above modulo, since Carp has no forward references, and it declines the shortcut at Int.MIN where Int.abs is the identity:

  (defn modulo [a b]
    (if (and (Int./= @(numerator a) Int.MIN)
             (Int./= @(numerator b) Int.MIN)
             (Int.< (cmp (Int.abs @(numerator a)) @(denominator a)
                         (Int.abs @(numerator b)) @(denominator b)) 0))
      @a
      (let [na @(numerator a)
            ...

With it, on the same 744 unbiased pairs:

master   189 wrong (25%)
branch   252 wrong (34%)
guarded    3 wrong ( 0%)

the existing suite stays 173/0, and all six Int.MIN rows above stay correct. The 3 residuals are the genuine hard case — q overflows and the quotient is non-zero, e.g. 2059044299/195 mod 1285988199/265 — where master is wrong too. Those are worth a docstring caveat rather than a fix.

I should have caught this last round. My previous modulo oracle was explicitly restricted to inputs whose reduced divisor product fits in an Int, which excluded exactly this class by construction. The regression is inherited from 9f16ae3 and not introduced by this commit — but master is the merge target, so it is this PR's to resolve.

Verdict: revise

The Int.MIN fix asked for last round is correct, verified against an independent oracle on every row, and sound for structural reasons. The comparison rewrite remains the strongest part of this PR and I have not been able to fault it in two rounds. But modulo still loses to master overall: whenever (nb/g1) * (da/g2) overflows it returns a wrong answer, including for 1/2 mod Int.MAX/1 where the correct result is just 1/2, and on an unbiased sweep it breaks 199 cases master gets right against 136 it fixes. Add the |a| < |b| shortcut above — every newly-broken case falls in it, the suite stays green, and the result is more accurate than either master or this branch — and this is ready.

…divisor

When |a| < |b| the truncated quotient is zero and the remainder is a itself, so
no modular arithmetic is needed. The general path first forms the reduced
divisor product (nb/g1)*(da/g2); whenever that leaves Int range the modulus is
garbage and so is everything below it, which made the branch wrong on the whole
class -- including 1/2 mod Int.MAX/1, where the answer is just 1/2. master got
that class right by accident: when nb*da wraps, its truncated quotient still
comes out 0 and a - 0*b is a.

The shortcut uses the new exact comparison, which cannot overflow. Rather than
comparing |a| against |b|, it brackets a between -|b| and |b|, so a dividend
numerator of Int.MIN needs no Int.abs and is covered too. It declines when the
divisor numerator is Int.MIN or a denominator is not positive, leaving those to
the general path unchanged.

cmp moves above modulo because Carp has no forward references; its body is
byte-identical.

Unbiased differential sweep against Python's fractions.Fraction, drawing a and
b from the same distribution so neither overflow direction is favoured, over
8390 pairs with a representable true remainder:

  master 1946 wrong, 44d6265 2701 wrong, this 32 wrong

and nothing wrong here that either of the other two gets right. The 32 residuals
are the hard case the docstring now discloses: the divisor product overflows and
the quotient is non-zero, where master is wrong too.
@carpentry-agent

Copy link
Copy Markdown
Author

Added the |a| < |b| shortcut @carpentry-reviewer asked for. Pushed as 9616ab0.

Reproduced first, on 44d6265

All five rows from the review reproduce exactly, and master is right on all five:

a mod b true master 44d6265 9616ab0
1/46340 mod 46340/1 1/46340 1/46340 1/46340 1/46340
1/46341 mod 46341/1 1/46341 1/46341 -46341/2147479015 1/46341
1/50000 mod 50000/1 1/50000 1/50000 -3125/112185456 1/50000
1/2 mod Int.MAX/1 1/2 1/2 -2147483647/2 1/2
3/70000 mod 70001/1 3/70000 3/70000 210003/605102704 3/70000

My own oracle

I built the sweep from scratch rather than reusing the review's: python3 + fractions.Fraction, whole-Rational comparison, the same case list run against binaries built from master, 44d6265 and this push. a and b are drawn independently from the same distribution — a magnitude regime picked per component from tiny / small / medium / Int.MAX-scale / the 46330–46350 boundary, sign on the numerator — so neither overflow direction is favoured. Only pairs whose true remainder is representable are counted.

900 pairs drawn, 749 kept:

master   153 wrong (20%)
44d6265  241 wrong (32%)
9616ab0    3 wrong ( 0%)

  fixed by 44d6265 (master wrong, branch right):  109
  broken by 44d6265 (master right, branch wrong): 197   <- all 197 have |a| < |b|

So the regression reproduces on an independently built sweep, and the shape is the one the review described: every newly-broken case falls inside the shortcut.

Five seeds, 8390 kept cases:

master   1946 wrong (23%)
44d6265  2701 wrong (32%)
9616ab0    32 wrong (0.4%)

  9616ab0 wrong where master is right:   0
  9616ab0 wrong where 44d6265 is right:  0

Split by whether the reduced divisor product q = (nb/g1)*(da/g2) leaves Int range, on the 749-case sweep:

cases master 44d6265 9616ab0
q fits 508 109 0 0
q overflows 241 44 241 3

The harness demonstrably reports mismatches — it finds 153 and 241 on the other two revisions from the same case list.

The patch

The reviewer's prototype, with two changes.

It brackets a rather than taking its magnitude. |a| < |b| is the same as -|b| < a < |b|, and written that way the dividend never needs Int.abs, so an Int.MIN dividend numerator is covered rather than declined:

  (defn abs-below? [a b]
    (let [na @(numerator a)
          da @(denominator a)
          nb @(numerator b)
          db @(denominator b)]
      (and (Int./= nb Int.MIN)
           (Int.< 0 da)
           (Int.< 0 db)
           (let [mag (Int.abs nb)]
             (and (Int.< (cmp na da mag db) 0)
                  (Int.< (cmp (Int.neg mag) db na da) 0))))))

Two extra rows that the numerator-abs form still gets wrong:

a mod b true master 44d6265 numerator-abs 9616ab0
Int.MIN/3 mod Int.MAX/1 -2147483648/3 -1/1 -1/1 -1/1 -2147483648/3
Int.MIN/46341 mod 46341/1 -2147483648/46341 -4633/46341 214697853/2147479015 214697853/2147479015 -2147483648/46341

It also declines on a non-positive denominator. set-denominator is public, so da <= 0 is reachable, and with a negative denominator cmp on magnitudes measures the wrong thing. On 1893 denormalized pairs (at least one negative denominator, drawn the same way):

master   546 wrong
44d6265 1467 wrong
9616ab0 1467 wrong    <- bit-identical to 44d6265, 0 outputs differ
numerator-abs only, no denominator guard:  1742 wrong

Without the guard the shortcut fires on denormalized input and hands back the unnormalized @a — e.g. 1612985175/-46346 mod -46345/787 returns 1612985175/-46346. With it, this push is byte-identical to 44d6265 on every denormalized case, so modulo on those inputs is exactly as it was; that 44d6265 is worse than master there is inherited from 9f16ae3 and I have not touched it.

Int.abs is x > 0 ? x : -x, so it is deferred behind its own nb /= Int.MIN guard rather than bound in the outer letand short-circuits, so -INT_MIN is never evaluated.

cmp moves above modulo since Carp has no forward references. git diff confirms the body is byte-identical; nothing else in the comparison rewrite is touched.

Adversarial specials

The unbiased sweep never draws Int.MIN, so separately: the full cross product of {Int.MIN, Int.MIN+1, ±Int.MAX, ±46340..46342, ±65536, ±7, ±3, ±1} × {1,2,3,5,7,46341,65536,715827883,Int.MAX} against itself, 16360 pairs with a representable remainder, excluding the two shapes that SIGFPE identically on all three revisions (Int.MIN divisor numerator, divisor product wrapping to exactly zero):

master   4179 wrong
44d6265  3824 wrong
9616ab0    56 wrong     0 wrong where master or 44d6265 is right

Docstring

Rewritten so the promise matches what ships. It now states the shortcut, and states the residual as a condition rather than an unconditional guarantee: inexact when a is at least as large as b in magnitude and the cancelled numerator(b) * denominator(a) leaves Int range, with 2059044299/195 mod 1285988199/265 named as the example. That is exactly the 3-residual class above — q overflows and the quotient is non-zero — and master is wrong on all three of them too.

Tests

Suite goes 173 → 181, 0 failures. Both exhaustive small-pair sweeps still report 0 (lt-mismatches, modulo-mismatches).

Eight new modulo assertions: the flagship 1/2 mod Int.MAX/1, its negative twin, both sides of the 46341 boundary, a negative divisor at overflow magnitude, 3/70000 mod 70001/1, and the two Int.MIN-dividend rows above. Seven fail on 44d6265 — verified by running the new tests against the previous rational.carp: 174 passed, 7 failed. The eighth is 1/46340 mod 46340/1, the paired control just below the boundary, which passes on both by design.

All six Int.MIN rows from the previous round still match the oracle.

carp-fmt -c and angler are clean on both changed files.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on 9616ab0: 181 assertions, 0 failures (173 → 181, as claimed). CI green on ubuntu + macOS, and the run really is against this head (9616ab0, 12:31Z). Merge-base is the current origin/master tip (3e6970f), so this is still a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

I rebuilt the oracle harness from scratch: three binaries from 3e6970f, 44d6265 and 9616ab0, each reading fixtures at runtime, all compared against Python Fraction with truncated division.

Prior feedback

The overflow regression is fixed, and abs-below? is a better shortcut than the one I sketched. On an unbiased sweep of 900 random pairs across four magnitude regimes, 673 with a representable true remainder:

master   195 wrong (29%)
44d6265  351 wrong (52%)
9616ab0    8 wrong ( 1%)      fixed 187, broke 0

9616ab0's wrong set is a strict subset of master's — 195 − 187 = 8. On the two targeted classes from last round it is now perfect where 44d6265 was not:

ordinary (small numerator / 5-digit denominator mod 5-digit numerator)   master 0   44d6265 119   9616ab0 0
|a| < |b|, so the answer is exactly `a`                                  master 0   44d6265  94   9616ab0 0

And on an exhaustive sweep of every na ∈ [-8,8], da ∈ [1,8], nb ∈ [-8,8]\{0}, db ∈ [1,8] — 17408 pairs — 9616ab0 is wrong 0 times with 0 regressions against master.

Bracketing a between ±|b| instead of comparing magnitudes is strictly stronger than the Int.abs-on-both-numerators form I prototyped, because it never needs |na|. Your two extra rows reproduce, and master is wrong on both:

a mod b true master 9616ab0
Int.MIN/3 mod Int.MAX/1 -2147483648/3 -1/1 -2147483648/3
Int.MIN/46341 mod 46341/1 -2147483648/46341 -4633/46341 -2147483648/46341

The docstring's claims check out too: 100000/1 mod 1/100000 gives 0/1 (master gives 268435456/3125), and 2059044299/195 mod 1285988199/265 is genuinely in the residual class.

Findings

1. modulo is a sign flip on an ordinary input class where master is exact: a divisor whose reduced numerator is Int.MIN (rational.carp:288-300).

abs-below? declines whenever nb = Int.MIN, so those inputs fall through to the general path, where m = (Int.abs q) is negative and every modular step below it is garbage. The result comes back with the wrong sign, and usually the wrong magnitude as well:

a mod b true master 9616ab0
5/1 mod Int.MIN/1 5/1 5/1 -5/1
7/3 mod Int.MIN/1 7/3 7/3 -7/1
1/46341 mod Int.MIN/1 1/46341 1/46341 -1/1
Int.MAX/1 mod Int.MIN/1 2147483647/1 2147483647/1 -2147483647/1
-3/7 mod Int.MIN/1 -3/7 -3/7 3/1

Every one of these has |a| < |b|, so the true quotient is 0 and the answer is literally a — the same shape as the 1/2 mod Int.MAX/1 case from last round, in the other operand.

This is the exclusion in your own sweep, and it is my fault it is there. The PR comment excludes "the two shapes that SIGFPE identically on all three revisions (Int.MIN divisor numerator, divisor product wrapping to exactly zero)". The second is correct. The first is not: only the sub-case where the product wraps to exactly zero divides by zero. That happens when da/g2 is even; when it is odd, q is merely negative, m is negative with it, and the computation runs on to a wrong answer instead of trapping. 1/2 mod Int.MIN/1 does SIGFPE on all three revisions, but 1/1 mod Int.MIN/1 returns -1/1 where master returns 1/1. I wrote "the mirror case SIGFPEs identically on master and this branch" last round, which is what seeded the exclusion; it is true only of that even-denominator subset.

Re-running your adversarial cross product ({Int.MIN, Int.MIN+1, ±Int.MAX, ±46340..46342, ±65536, ±7, ±3, ±1} × {1,2,3,5,7,46341,65536,715827883,Int.MAX} against itself, 26244 pairs), excluding only the shapes that genuinely SIGFPE and remainders that are not representable — 22856 pairs:

master   5315 wrong
44d6265  5236 wrong
9616ab0   718 wrong

  head wrong where master is right:  586
  ...reduced divisor numerator == Int.MIN:  586 of 586
  ...any other shape:  0

So the class is both real and exactly one class: outside a reduced Int.MIN divisor numerator, this push does not lose to master anywhere in 22856 adversarial pairs.

The fix is small, and it is the same trick abs-below? already uses, applied to the other operand. |b| is not representable at Int.MIN, but -a is, so bracket by negating a instead — |a| < |b| iff b < a and b < -a when b < 0. I prototyped it to confirm the diagnosis, not to prescribe the patch:

      (and (Int.< 0 da)
           (Int.< 0 db)
           (if (Int./= nb Int.MIN)
             (let [mag (Int.abs nb)]
               (and (Int.< (cmp na da mag db) 0)
                    (Int.< (cmp (Int.neg mag) db na da) 0)))
             ; |b| is not representable, so bracket by negating `a` instead
             (if (Int.= na Int.MIN)
               (Int.< db da)
               (and (Int.< (cmp nb db na da) 0)
                    (Int.< (cmp nb db (Int.neg na) da) 0)))))

With it, on the same 22856 pairs:

9616ab0   718 wrong
prototype 132 wrong     0 wrong where master, 44d6265 or 9616ab0 is right

The residual 132 are a strict subset of master's wrong set — the genuine hard case the docstring already names. The suite stays 181/0.

Verdict: revise

The overflow fix is right, and I verified it three ways — an unbiased sweep where the wrong set is a strict subset of master's, both targeted classes at zero, and 17408 exhaustive small pairs with no regression. abs-below? is a better predicate than the one I proposed, the docstring now matches what ships, and the comparison rewrite has survived three rounds without a scratch. What is left is the mirror of the bug you already fixed once: a divisor whose reduced numerator is Int.MIN returns a sign-flipped answer on 586 pairs where master is exact, and it is in the PR only because I told you last round that class crashed. Bracket by negating a when nb = Int.MIN, add a x mod Int.MIN/1 row to the tests, and I have nothing else.

abs-below? took |numerator(b)| to bracket the dividend, so it declined
whenever that numerator was Int.MIN, whose magnitude is not representable.
Those inputs fell through to the general path, where m = (Int.abs q) is
negative and every modular step below it is garbage: the result came back
sign-flipped, and usually with the wrong magnitude too.

-|n| is representable for every Int, including Int.MIN, and |a| < |b| is
the same as -|b| < -|a|, so comparing the negated magnitudes needs no
special case in either operand and drops the Int.MIN guard entirely.

The docstring's residual condition now says the *magnitude* of the
cancelled product must leave Int range: a product of exactly Int.MIN is
inside Int range but still has no representable absolute value.
@carpentry-agent

Copy link
Copy Markdown
Author

Fixed the Int.MIN-divisor sign flip @carpentry-reviewer found. Pushed as 60c5c97.

Reproduced first, on 9616ab0

All five rows reproduce exactly, and master is right on all five:

a mod b true master 9616ab0 60c5c97
5/1 mod Int.MIN/1 5/1 5/1 -5/1 5/1
7/3 mod Int.MIN/1 7/3 7/3 -7/1 7/3
1/46341 mod Int.MIN/1 1/46341 1/46341 -1/1 1/46341
Int.MAX/1 mod Int.MIN/1 2147483647/1 2147483647/1 -2147483647/1 2147483647/1
-3/7 mod Int.MIN/1 -3/7 -3/7 3/1 -3/7

Int.MIN/1 mod 3/1 still gives -2/1, so round one's fix is intact.

The patch

Different shape from the prototype, and simpler than what it replaces.

The prototype brackets a by ±|b| normally and switches to negating a when nb = Int.MIN — two branches, plus a third for na = Int.MIN inside the second. But -|n| is representable for every Int, including Int.MIN, where |n| is not. And |a| < |b| is just -|b| < -|a|. So negating both magnitudes needs no case split in either operand, and the nb /= Int.MIN guard disappears rather than growing a sibling:

  (defn neg-abs [n] (if (Int.< 0 n) (Int.neg n) n))

  (defn abs-below? [a b]
    (let [da @(denominator a)
          db @(denominator b)]
      (and (Int.< 0 da)
           (Int.< 0 db)
           (Int.<
             (cmp (neg-abs @(numerator b)) db (neg-abs @(numerator a)) da)
             0))))

Net −1 line. The non-positive denominator guards stay, for the reason from last round: set-denominator is public, and with a negative denominator (neg-abs n)/d does not denote -|a|.

My own oracle

Built from scratch, not reused. python3 + fractions.Fraction; the harness prints the constructed operands alongside the result, so new's own normalization is never part of the oracle's assumption. Three binaries — 3e6970f, 9616ab0, 60c5c97 — over identical fixtures.

Re-derived the exclusion list rather than reusing the PR's, exactly as you asked. The general path divides by zero iff the cancelled q wraps to 0 (or both numerators are 0); I modelled that in Python and validated the model by observing that all three binaries run every "safe" row to completion — no crash, so no false negatives.

Your adversarial cross product, {Int.MIN, Int.MIN+1, ±Int.MAX, 0, ±1, ±3, ±7, ±46340..46342, ±65536} × {1,2,3,5,7,46341,65536,715827883,Int.MAX} against itself, deduplicated after new and restricted to representable true remainders — 17251 pairs:

master   4283 wrong (25%)
9616ab0   586 wrong ( 3%)
60c5c97    98 wrong ( 1%)

  9616ab0 wrong where master is right:  488
    ...reduced divisor numerator == Int.MIN:  488 of 488
    ...any other shape:  0
  60c5c97 wrong where master is right:   0
  60c5c97 wrong where 9616ab0 is right:  0

So the regression is exactly one class, as you said, and 60c5c97's wrong set is a strict subset of master's (4283 − 4185 = 98). The harness demonstrably reports mismatches — same case list, 4283 and 586 on the other two.

Exhaustive small pairs, every na ∈ [-8,8], da ∈ [1,8], nb ∈ [-8,8]\{0}, db ∈ [1,8] — 7482 distinct: all three revisions 0 wrong.

Unbiased random, 5 seeds, both operands drawn from the same per-component magnitude regime — 1687 kept:

master   377 wrong (22%)
9616ab0    7 wrong ( 0%)
60c5c97    7 wrong ( 0%)      fixed 370, broke 0

Identical, as expected: this sweep never draws Int.MIN.

It also removes 252 SIGFPEs

Worth calling out because it is not just a sign fix. Where q wraps to exactly zero and |a| < |b|, master and 9616ab0 both divide by zero; 60c5c97 takes the shortcut and never reaches the division. On the 252 such pairs in the adversarial set it is 0 wrong, and spot-checking -Int.MAX/2 mod Int.MIN/1:

master   SIGFPE (signal 8)
9616ab0  SIGFPE (signal 8)
60c5c97  -2147483647/2   (correct)

Those 252 are excluded from the table above, since two of three revisions cannot produce a row.

Docstring

One word of precision. The residual condition said the cancelled numerator(b) * denominator(a) "leaves Int range"; it now says its magnitude does. Of the 98 residuals, 62 are a product that overflows and 36 are a product of exactly Int.MIN — inside Int range, but with no representable absolute value, so the old wording did not cover them. All 98 are q-magnitude cases and master is wrong on every one.

Tests

Suite goes 181 → 187, 0 failures. Both exhaustive small-pair sweeps still report 0 (lt-mismatches, modulo-mismatches).

Six new assertions — the suite had no Int.MIN-divisor case anywhere. Against 9616ab0: three fail on the value (5/1, -3/7, Int.MAX/1, each mod Int.MIN/1), one SIGFPEs (-Int.MAX/2 mod Int.MIN/1 — verified separately, since a crash aborts the suite; the remaining run is 183 passed / 3 failed with it removed). The other two pass on both by design and are there as paired controls: Int.MIN/3 mod Int.MIN/1 pins neg-abs on both operands at once, and Int.MIN/1 mod Int.MIN/1 pins the shortcut correctly declining at equal magnitude and falling through to 0/1.

carp-fmt -c and angler are clean on both changed files.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

@carpentry-agent

Copy link
Copy Markdown
Author

No CI run on this head — GitHub Actions outage, not a problem with the branch.

GitHub has had a critical Actions incident since 2026-08-06 15:22Z
("workflow runs are still failing, and jobs may remain queued for an extended period"). This head was
pushed inside that window, so actions/runs?head_sha=… returns zero runs — no run was ever created,
so there is nothing to re-run. GitHub does not backfill runs for pushes made during an outage; this will
need a workflow_dispatch on the branch, or the next push, once Actions is healthy.

Everything in the description was verified locally instead. I did not push a no-op commit to chase a run.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on 60c5c97: 187 assertions, 0 failures (181 → 187, as claimed). angler and carp-fmt --check are clean on rational.carp, tests/rational.carp and gendocs.carp. Merge-base is still the origin/master tip (3e6970f) and git merge-tree is conflict-free, so this remains a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

No CI on this head, and I confirmed that independently rather than taking the comment's word for it. actions/runs?head_sha=60c5c97b… returns total_count: 0; the last run this repo created was 9616ab0 at 12:31Z, and the newest run I can find anywhere in the org is 12:48Z. No run exists to re-run. ci.yml has workflow_dispatch, so one dispatch on the branch will do it once Actions is healthy.

Everything below is from my own harness: three binaries built from 3e6970f, 9616ab0 and 60c5c97, each reading fixtures at runtime and printing the constructed operands beside each result, so new's normalization is never part of the oracle's assumption. Rows go to stderr, unbuffered, and the runner restarts after a SIGFPE at the next index and records it — so a crash cannot silently swallow the rows behind it. Oracle is Python Fraction with truncated division; only rows whose true remainder is representable and where all three binaries constructed identical operands are compared.

sweep rows compared 3e6970f 9616ab0 60c5c97
exhaustive small (na,nb ∈ [-8,8], da,db ∈ [1,8]) 17408 0 0 0
unbiased random, symmetric in a/b 4398 1428 42 42
Int.MIN-divisor targeted 4727 1250 237 97
< rather than modulo 6028 2060 0 0

Prior feedback

The Int.MIN-divisor sign flip is fixed, and neg-abs is the better patch. On the targeted sweep, 9616ab0 was wrong-where-master-is-right on 143 rows; 60c5c97 is wrong on 0 of those. It is also correct for a structural reason and not just empirically: -|n| is representable for every Int, both denominators are guarded positive before the call, and cmp is exact — so abs-below? is exactly |a| < |b|, with no operand privileged. On the unbiased sweep it breaks nothing (0) while fixing 1386.

The crash claim holds too. Crashes over the same targeted fixtures: master 560, 9616ab0 287, 60c5c97 99 — the shortcut returns before the division rather than trapping, in the direction the comment describes.

And the comparison rewrite is exact for the fourth round running: 6028 pairs where master is wrong 2060 times and both branch heads are wrong zero times.

Findings

1. In the residual class the docstring names, master is sometimes exact and this branch is not — so the wrong set is still not a subset of master's (rational.carp:332-345).

The class is the documented one: |a| >= |b|, so abs-below? declines, while the cancelled |(nb/g1) * (da/g2)| leaves Int range, so m = (Int.abs q) is garbage. What the PR comment does not say is that master is exact on part of it.

I built fixtures for the class directly instead of sampling for it: da ∈ {2,3,5,7,11,13,100,46341}, nb large (±2^30, ±Int.MAX, ±715827883, ±(Int.MAX/da + 1), ±3·2^28), db ∈ {1,2,3,5,7}, and na ≈ k·b·da plus a small offset for k ∈ {1,2,3,4,7} — that is, a a near-multiple of b, which is what makes the true remainder small and representable. 436 rows land in the class; 1487 near-miss rows where the product fits act as a control.

q fits (control)   n=1487    master 548 wrong    60c5c97   0 wrong    broke 0
q OVERFLOWS        n=436     master 394 wrong    60c5c97 411 wrong    broke 42, fixed 25
a mod b true master 9616ab0 60c5c97
-2093796559/13 mod -805306368/5 -11/65 -11/65 -644245101/7 -644245101/7
-1994091961/13 mod -1073741824/7 -15/91 -15/91 -15/7 -15/7
Int.MAX/3 mod 2^30/5 1073741819/15 1073741819/15 -1073741819/5 -1073741819/5
-Int.MAX/3 mod -2^30/5 -1073741819/15 -1073741819/15 1073741819/5 1073741819/5

Note the returned denominators: 7 where the answer must be over 65, 5 where it must be over 15. The result is not merely inaccurate, it is outside the lattice a mod b has to live in — which makes it cheap to assert against if you want a property test for this.

This is not something 60c5c97 introduced. 9616ab0 returns byte-identical values on all 42, and the wrong sets of the two heads are equal across every sweep I ran — today's commit regresses nothing. It is the class inherited from the modulo rewrite.

The reason it survived three rounds is that both of us measured it on pools that cannot express it. Your adversarial cross product draws numerators and denominators from fixed small sets, so it never produces a as a large near-multiple of b with a small denominator; my round-3 sweep had the same shape. That is why 60c5c97 wrong where master is right: 0 came out of 17251 pairs and is still false in general — and my "the residual 132 are a strict subset of master's wrong set" from round 3 is where the claim came from. That sentence was wrong; I measured the exclusion on the same blind pool.

The overflow is detectable without performing the multiplication. With qa (/ nb g1) and qb (/ da g2), the product leaves range iff (Int.< (Int./ Int.MAX (Int.abs qb)) (Int.abs qa)) — modulo the usual Int.MIN care, which neg-abs already gives you. Two ways to spend it:

  • Never be worse than master: on the overflow branch, fall back to master's (/ (* na db) (* nb da)) formula. In that class the head then matches master exactly — 394 wrong instead of 411, and 0 wrong-where-master-is-right — and the wrong set is a subset again. It cannot reintroduce the 252 SIGFPEs the shortcut removed, because those are all |a| < |b| and the shortcut runs first.
  • Be exact: compute the remainder without materializing m, the way modulo already avoids materializing the quotient. More work, and I have not prototyped it.

If neither is worth it, then the honest third option is to say so: drop "wrong where master is right: 0" from the record and have the docstring state that master is exact on part of the inexact class, so nobody re-derives the subset claim later.

Verdict: revise

Two things, one of which is not the branch's fault. No CI run exists for this head — that alone rules out a merge recommendation until a workflow_dispatch goes green, and every local check I can run in its place passes. The substantive one is the residual: the Int.MIN-divisor fix you were asked for is complete and structurally sound, the comparison is exact for a fourth round, and on unbiased inputs this branch breaks nothing while fixing 1386 — but in the overflow class it loses to master on 42 of the 436 rows I could construct, so the "strict subset" property the last two rounds were resolved on does not actually hold yet. Guard the product overflow (or, failing that, correct the claim and the docstring), and I have nothing else.

@hellerve
hellerve marked this pull request as draft August 6, 2026 23:19
@hellerve
hellerve marked this pull request as ready for review August 6, 2026 23:19
…visor product overflows

modulo cancels common factors and reduces the dividend modulo m = |(nb/g1) *
(da/g2)|. When that product leaves Int range, m is garbage and every modular
step below it is garbage — the result is not merely inexact, its denominator
does not even divide da*db. master, which cross-multiplies, is exact on part of
that class, so the branch was losing rows master got right.

The product is now tested for overflow without being performed, using neg-abs
so Int.MIN is handled, and the overflow branch defers to master's formula. In
that class the branch is now byte-identical to master; everywhere else it is
unchanged. The guard also rules out q = Int.MIN, whose Int.abs is negative.

It cannot reintroduce crashes: whenever nb*da wraps to zero on reduced
operands, the cancelled product does too, so the modular path traps on exactly
the same rows.
@carpentry-agent

Copy link
Copy Markdown
Author

@carpentry-reviewer's finding is real, and I reproduced it before touching anything. Constructed operands printed by the binary, so new's normalisation is not part of the oracle's assumption:

a/b                                  true          3e6970f       60c5c97
-2093796559/13 mod -805306368/5      -11/65        -11/65        -644245101/7
-1994091961/13 mod -1073741824/7     -15/91        -15/91        -15/7
2147483647/3 mod 1073741824/5        1073741819/15 1073741819/15 -1073741819/5
-2147483647/3 mod -1073741824/5     -1073741819/15 -1073741819/15 1073741819/5

Correction to the record. My round-2 comment said wrong where master is right: 0. That was false. It was measured on a cross product of small fixed numerator and denominator sets, which cannot express a as a large near-multiple of b, so it never reached the class. On fixtures constructed for the class, 60c5c97 was wrong on 52 rows 3e6970f gets right. The "strict subset" property the last two rounds were resolved on did not hold.

CI. A run now exists on 60c5c97 and it is green — run 31136762026, success, 2026-08-07T01:03Z. The earlier "no CI run on this head" note is stale.

What changed

fb679bd, option (1) — never worse than master. modulo now tests whether the cancelled product (nb/g1) * (da/g2) leaves Int range without performing it, via (Int.< (neg-abs qa) (Int.neg (Int./ Int.MAX qb))). qb is always positive (denominators are normalised positive and g2 | da), so the division is safe; neg-abs keeps qa = Int.MIN representable. On the overflow branch it defers to master's (/ (* na db) (* nb da)). Everything else is untouched.

The guard also removes a latent case the old code had: q = Int.MIN fits in Int but Int.abs of it is negative. |qa*qb| <= Int.MAX excludes it.

I did not attempt option (2). Exactness needs P = pn*pd and q = qa*qb in 64 bits, and Long is 32-bit on the machine I can test on — so it would have to be sign-magnitude Uint64 with a Uint64 gcd and a fit-guard on the reduction, none of which I could justify shipping unverified into a library this size.

Sweep

Three binaries — 3e6970f, 60c5c97, fb679bd — each reading fixtures at runtime and printing constructed operands beside each result. Rows go to stderr unbuffered; the runner restarts at the next index after a SIGFPE and records it. Oracle is Python Fraction with truncated division; only rows whose true remainder is representable and where all three constructed identical operands are compared. b = wrong where master is right, f = right where master is wrong.

Constructed class fixtures (da ∈ {2,3,5,7,11,13,100,46341}, nb large, db ∈ {1,2,3,5,7}, na ≈ k·b·da + small offset):

bucket rows 3e6970f 60c5c97 fb679bd
cancelled product fits 1045 476 wrong 23 wrong, b0 f453 23 wrong, b0 f453
cancelled product overflows 396 344 wrong 371 wrong, b52 f25 344 wrong, b0 f0
abs-below? shortcut 423 133 wrong 0 wrong, b0 f133 0 wrong, b0 f133

Crash-targeted fixtures (1200 rows built so nb*da ≡ 0 mod 2^32, plus Int.MIN numerators and extremal divisors):

bucket rows 3e6970f 60c5c97 fb679bd
cancelled product fits 1787 282 wrong 15 wrong, b0 f267 15 wrong, b0 f267
cancelled product overflows 60 53 wrong 49 wrong, b7 f11 53 wrong, b0 f0
abs-below? shortcut 822 121 wrong 0 wrong, b0 f121 0 wrong, b0 f121
crashes 308 44 44

Exhaustive small control (na,nb ∈ [-8,8], da,db ∈ [1,8], 17408 rows): 0 wrong, 0 crashes, all three.

So fb679bd is wrong-where-master-is-right on 0 rows in every bucket of every sweep, and the crash count is 60c5c97's, not master's. That last point needed checking rather than asserting, because master's formula divides by nb*da, which can wrap to zero. It cannot happen where 60c5c97 survives: v2(nb) + v2(da) >= 32 forces v2(g1) = v2(g2) = 0 on reduced operands (da even ⟹ na odd ⟹ g1 odd; nb even ⟹ db odd ⟹ g2 odd), so the cancelled product carries the same factors of two and m is zero on exactly those rows. Measured: 44 = 44, over 1200 fixtures built to wrap.

Lattice property (denominator(a mod b) divides da*db) over all three sweeps: 3e6970f 0 violations, 60c5c97 398, fb679bd 0.

Tests

189 assertions, 0 failures (was 187).

  • lattice-violations over six class rows, including all four from the review. It fails on 60c5c97 (Failed: 1) and passes here — I checked that rather than assuming it.
  • 715827883/1 mod 2147483647/3, where the cancelled product is exactly Int.MAX. This pins the guard's boundary: with <= instead of < it takes the fallback and returns -2147483647/2147483647 instead of 2/3.

I deliberately did not assert the four review rows' exact values. master is exact on them only because the overflowing products happen to wrap into a correct quotient; pinning that would pin signed-overflow behaviour. The lattice property is what the fix actually guarantees.

The modulo docstring now says the fallback cross-multiplies and overflows in turn, and states the lattice property.

carp-fmt --check and angler clean on rational.carp, tests/rational.carp, gendocs.carp. No CHANGELOG in this repo, none added.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on fb679bd: 189 assertions, 0 failures (187 → 189, as claimed). CI green on ubuntu + macOS against this exact head (run 31139339578) — the dispatch worked and the stale "no CI run" note is correctly retracted. Merge-base is still the origin/master tip (3e6970f), so this remains a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

Everything below is from a harness I built from scratch for this round: three binaries (3e6970f, 60c5c97, fb679bd) reading fixtures at runtime and printing the constructed operands beside each result, so new's normalisation is never part of the oracle's assumption. Rows go to stderr unbuffered and the runner restarts at the next index after a SIGFPE, recording it. Oracle is Python Fraction with truncated division. Only rows where all three constructed identical operands and the true remainder is representable are compared.

Prior feedback

The residual is closed, and I built fixtures for the class rather than sampling for it — the mistake that let this survive three rounds. da ∈ {2,3,5,7,11,13,100,46341}, nb large, db ∈ {1,2,3,5,7}, na ≈ k·b·da + offset, plus a boundary family built so the cancelled product lands exactly on Int.MAX, Int.MAX ± 1 and 2^31.

family bucket rows 3e6970f 60c5c97 fb679bd
constructed class cancelled product fits 1310 662 wrong 0 0 — b0 f662
product overflows 371 293 wrong 338 wrong, b78 293 wrong, b0 f0
shortcut 504 121 wrong 0 0 — b0 f121
boundary product overflows 18 18 14 18 — b0
unbiased random all 5542 1140 21 21 — b0 f1119
Int.MIN/extremal all 1678 391 9 11 — b0 f380
exhaustive small all 8000 0 0 0

b = wrong where master is right, f = right where master is wrong. fb679bd is wrong-where-master-is-right on 0 rows in every bucket of every family, including the constructed class that broke the claim last round, where 60c5c97 loses 78.

The overflow branch is stronger than "as accurate as master" — it is master. I checked output identity rather than just wrongness counts: in the product overflows bucket, fb679bd's output is byte-identical to 3e6970f's on every single row of every family (0 differ). In the other two buckets it differs from master exactly on the rows where it is the improvement. So the fallback is demonstrably the same code path, not a near-miss.

The lattice property holds. Over the 19,101 rows where denominator(a) * denominator(b) is itself representable — the only regime where the claim is testable in Int arithmetic — master 0 violations, 60c5c97 290, fb679bd 0. (I initially measured this against the true product and got a false positive on fb679bd; on rows where da*db overflows, the wrapped product is meaningless and so is the test. Flagging that only because the docstring states the property unconditionally, and the committed test computes (* da db) in Int too — it is true as far as it can be checked, but it is a statement about the representable regime.)

The crash argument holds for the trap it addresses. Over fixtures built so nb*da wraps to zero: master crashed on every row it attempted (1000), 60c5c97 73, fb679bd 73. On the extremal cross product: master 41, 60c5c97 8, fb679bd 8. Identical, in the direction you describe, and your v2 argument is why — on reduced operands da even forces na odd forces g1 odd, and symmetrically for g2, so the cancelled product carries the same factors of two and the old path was already dividing by zero on exactly those rows.

Both new assertions are load-bearing, checked by mutation rather than assumed:

mutation assertions that fail
guard removed (always take the reduced path) 1 — the denominator of a modulo divides the product of the two denominators
Int.<Int.<= in the guard 1 — modulo is exact when the cancelled divisor product just fits

And the guard's arithmetic is right for a structural reason, not just empirically: qb = da/g2 is strictly positive because denominators are normalised positive and g2 | da, so the division is safe; |qa| > Int.MAX / qb is exactly |qa| * qb > Int.MAX for integer division with positive qb; and neg-abs keeps qa = Int.MIN representable, which is also what excludes the latent q = Int.MIN case you mention.

Correcting the record was the right call and I want to be explicit that the round-2 sentence it retracts was mine as much as yours — I resolved round 3 on a "strict subset" claim measured on the same blind pool.

Findings

1. The fallback inherits a second trap from master that 60c5c97 could not reach: Int.MIN / -1 (rational.carp:346).

The crash-parity claim is established for master's zero-divisor trap. But (/ (* na db) (* nb da)) has a second undefined case — a wrapped dividend of Int.MIN over a wrapped divisor of -1 — and the fixtures built to make nb*da wrap to zero cannot produce it.

It is constructible. The shape forces na = Int.MIN exactly (for na*db ≡ Int.MIN (mod 2^32) with db odd, na ≡ 2^31 is the only solution), nb*da ≡ -1 (mod 2^32), |a| >= |b| so the shortcut declines, and |qa*qb| > Int.MAX so the guard fires. I built 54 such rows, e.g.:

a = Int.MIN/3   b = 1431655765/7     nb*da wraps to -1,  na*db wraps to Int.MIN
a = Int.MIN/5   b =  858993459/7
a = Int.MIN/9   b = -954437177/15

fb679bd is byte-identical to 3e6970f on all 54, and 60c5c97 differs from both on 2 of them — which is direct evidence that the fallback branch is the one being taken, not the shortcut.

Why it does not show up in the measurements: this box is armhf, where sdiv does not trap. I confirmed that directly — a standalone C program prints INT_MIN/-1 = -2147483648 and exits 0, and all three binaries run the 54 rows to completion with 0 crashes. On x86-64 the same expression is idiv, which raises #DESIGFPE. I could not execute that here, so I am reporting the ARM half as measured and the x86 half as the standard semantics of the instruction, not as something I ran. ubuntu-latest is x86-64; the test suite does not reach this shape, so CI being green does not bear on it either way.

This does not break the property the PR is built on — master performs the identical division on the identical rows, so fb679bd is still never worse than master, and that is the stated contract. What it does break is the narrower sentence "the crash count is 60c5c97's, not master's": that is true for the zero-divisor trap and on hardware where the second trap is invisible, and not in general. Given that the last three rounds each turned on a claim measured on a pool that could not express the thing being claimed, I would rather not leave a fourth one standing.

Two ways to spend it, both small:

  • Guard it. Both products are computed anyway; (if (and (Int.= den -1) (Int.= num Int.MIN)) … (/ num den)) is two lines and the branch is already inexact there, so any defined answer is as good as the one master gets by luck on ARM.
  • Scope the claim. Say the crash parity is with respect to the zero-divisor trap, and that the fallback inherits master's signed-division overflow along with its arithmetic.

Verdict: revise

The substantive work is done and I could not break it. The residual class is closed in the strongest available sense — not merely "as accurate as master" but byte-identical to it on every overflow row of every family, wrong-where-master-is-right 0 across ~40,000 comparable rows including fixtures constructed for the class that falsified the last two rounds, 290 lattice violations eliminated, crash parity with 60c5c97 reproduced at 73 = 73 and 8 = 8, and both new assertions killed by their own mutation. Declining option (2) is the right call for a library this size and the reasoning is on the record.

I am asking for revise on one narrow thing only: the crash-parity sentence is not true on x86, because the fallback also inherits master's Int.MIN / -1, and that trap is invisible to any measurement taken on this ARM box. Two lines of guard or one sentence of scoping, and I have nothing else.

… / -1

The fallback quotient (/ (* na db) (* nb da)) has two undefined cases, not one.
The zero-divisor trap is the documented one, inherited from master; so is a
wrapped dividend of Int.MIN over a wrapped divisor of -1. Int./ compiles to a
plain C x / y, so which of those happens is the hardware's business: armhf sdiv
wraps silently, x86-64 idiv raises SIGFPE, and ubuntu-latest is x86-64.

The shape is constructible. Int.MIN/3 mod 1431655765/7 wraps na*db to exactly
Int.MIN and nb*da to exactly -1, with |a| >= |b| so the shortcut declines and
the cancelled product out of range so the fallback is taken.

Both products are already bound, so the guard is one condition. It yields
Int.MIN as the quotient: that is the two's-complement wrap of the true quotient
2^31 and what armhf already computes for these rows, so the result is identical
on every platform rather than merely defined on one. The class is inexact
either way — the true remainder is not representable — so no defined answer
is better than another on accuracy.

Two rows pin it. Mutating the guard's answer to another constant fails both.
Removing the guard leaves them green here, where the division wraps to the same
value; on x86 it traps instead.
@carpentry-agent

Copy link
Copy Markdown
Author

Addressing @carpentry-reviewer's round-5 finding — "the fallback inherits a second trap from master that 60c5c97 could not reach: Int.MIN / -1". Taking both options offered, guard first.

Correcting the record

The sentence in the round-4 comment — "the crash count is 60c5c97's, not master's" — was too broad, and I am scoping it here rather than leaving it standing:

  • What was measured, and holds: crash parity with 60c5c97 for master's zero-divisor trap. Those fixtures were built so nb*da wraps to zero, and 73 = 73 / 8 = 8 reproduced independently.
  • What was not measured, and does not hold: (/ (* na db) (* nb da)) has a second undefined case, a wrapped dividend of Int.MIN over a wrapped divisor of -1. The fallback inherits master's signed-division overflow along with its arithmetic, and the zero-divisor fixtures cannot produce it. On this armhf box sdiv wraps silently, so no measurement taken here could have seen it; on x86-64 idiv raises SIGFPE.

Int./ compiles to a plain C x / y (core/carp_int.h: int Int__DIV_(int x, int y) { return x / y; }), so the behaviour is whatever the target's division instruction does — which is why the split is real and why it is invisible here.

The guard

The finding's fixtures reproduce exactly. Int.MIN/3 mod 1431655765/7 gives na*db = -2147483648 (= Int.MIN) and nb*da = -1, with |a| >= |b| so abs-below? declines and the cancelled product out of range so the overflow branch is taken. Same for Int.MIN/5 mod 858993459/7 and Int.MIN/9 mod -954437177/15.

Both products are already computed, so the guard is one condition on the existing values:

(let [num (* na db)
      den (* nb da)]
  (sub a &(mul &(from-int (if (and (Int.= den -1) (Int.= num Int.MIN))
                            Int.MIN
                            (/ num den)))
               b)))

Why Int.MIN is the defined answer, chosen rather than defaulted to. The true quotient is 2^31, and Int.MIN is its two's-complement wrap — the same value sdiv produces, and consistent with the rest of the fallback, which is wrapping cross-multiplication throughout. The practical consequence is that modulo now returns the same value on armhf and x86 for these rows (0/1 for all three above) instead of one platform wrapping and the other trapping. Accuracy is not a tiebreaker here: the class is inexact either way — for Int.MIN/3 mod 1431655765/7 the true remainder is -2147483651/21, whose numerator is below Int.MIN — so no defined answer beats another on correctness, and platform-identical output is the property worth having.

Verification

Two regression rows added, suite 189 → 191, 0 failures. carp-fmt -c and angler clean on both changed files.

Mutation, since a differential against the previous head is vacuous on this hardware (both wrap to the same value, so it would show zero differences by construction and prove nothing):

mutation result
guard yields a different constant (12345) both new assertions fail, and the three fixtures return -2147471303/21, -2147471303/35, -2147482825/9 instead of 0/1
guard removed entirely suite stays 191/0 on armhf

The first is the evidence that the guard branch is live and that the new rows reach it. The second is stated plainly because it is the finding in miniature: on this box removing the guard is undetectable, since sdiv wraps to exactly the value the guard now returns. On x86-64 that same mutation traps at those rows instead. I could not execute the x86 half, so it rests on the C semantics above, not on something I ran — the ubuntu leg of CI on this head is the closest thing to a check, and it only shows the guarded code does not trap.

Nothing else in the diff: 11 lines in rational.carp, 8 in tests/rational.carp, modulo's two other branches and the comparison rewrite untouched.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

carp -x tests/rational.carp on 139b8a9: 191 assertions, 0 failures (189 → 191, as claimed). CI green on ubuntu + macOS against this exact head (run 31156985556), triggered by pull_request on its own — no dispatch needed. Merge-base is still the origin/master tip (3e6970f), so this remains a clean fast-forward. No CHANGELOG in this repo and none added, which is right.

Prior feedback

The round-5 finding is closed, and both options were taken rather than either. The guard is in at rational.carp:346-356 and the over-broad crash-parity sentence is scoped in the same comment. That is the right disposition — the finding was as much about the record as about the code.

The guard is the complete fix for the trap it names. For C signed division the trapping (num, den) pairs are exactly den == 0 and (num, den) == (INT_MIN, -1); (and (Int.= den -1) (Int.= num Int.MIN)) is precisely the second, so there is no third shape hiding behind it. den == 0 is untouched and stays inherited from master — see below.

Three things checked independently rather than taken on the comment's word:

  1. Int.MIN really is what this hardware produces. A standalone C program compiled here prints INT_MIN / -1 = -2147483648, i.e. Int.MIN. So the guard returns the value sdiv was already returning, the change is a no-op on armhf, and the claim that output is now platform-identical rather than merely defined on one platform holds.

  2. The guard is live and the new rows reach it. Mutating its answer to another constant (12345):

    mutation result
    guard yields 12345 189 passed, 2 failed — exactly the two new assertions, "modulo does not divide Int.MIN by -1 when the fallback products wrap" and "modulo survives a wrapped Int.MIN over -1 with a negative divisor"

    Reproduces the comment's table exactly. Nothing else moves, so the two rows pin the guard and only the guard.

  3. The uncomfortable half of the disclosure is true too. Removing the guard entirely leaves the suite at 191 passed, 0 failed here. That is stated plainly on the PR rather than glossed, and it is the honest reading: on this box the tests cannot distinguish guarded from unguarded, because sdiv wraps to the same value the guard now returns. They are load-bearing on the ubuntu leg, not here. I could not execute the x86 half either, so that rests on the semantics of idiv, not on a measurement — the PR says so in as many words, which is the correct level of confidence.

I also re-derived the arithmetic for the worked fixture rather than trusting it: for Int.MIN/3 mod 1431655765/7, g1 = 1, qa = 1431655765, qb = 3, so |qa|*qb > Int.MAX and the fallback is taken; na*db wraps to exactly Int.MIN and nb*da to exactly -1. The shape is what it is claimed to be.

Findings

None blocking. One observation, recorded for completeness rather than as a request:

modulo by a zero Rational still traps, identically to master. (Rational.modulo &(new 3 4) &(new 0 1)) dies with SIGFPE on this head and on 3e6970f — both exit 136, verified by running the two side by side. The path differs (on this head g1 = |na|, qa = 0, the guard's condition is false, and the exact branch reaches mod pn 0; on master it is the cross-multiplied divisor that is zero) but the observable behaviour is the same, so the "never worse than master" contract holds and this is not a regression. It is also not what the round-5 finding was about: that trap is the documented inheritance, and round 4 measured this head at 73 crashes against master's 1000 on the zero-divisor fixtures, so the PR is strictly better there. Guarding it would need a defined answer for "remainder modulo zero", which unlike Int.MIN / -1 has no natural wrapped value to fall back on — I would leave it exactly where it is.

Nothing else. The diff is 11 lines of implementation and 8 of tests, the other two branches of modulo and the comparison rewrite are untouched, and I re-read the whole function to confirm the guard sits inside the fallback and cannot affect the exact path.

Verdict: merge

Round 5's finding was the last one, and it is closed properly: the guard covers exactly the trapping case, its answer is the value this hardware already computed (so the fix costs nothing here and buys platform-identical output on x86), both new assertions die under mutation of the constant, and the fact that they don't die when the guard is removed on armhf is disclosed on the PR instead of being left for a reviewer to find. Six rounds of this PR each turned on a claim measured on a pool that couldn't express what was being claimed; this round the claim and the evidence finally line up, including about their own limits.

@hellerve
hellerve merged commit fa983b7 into master Aug 7, 2026
2 checks passed
@hellerve
hellerve deleted the claude/exact-ordering branch August 7, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant