Skip to content

Cover the remainder-only Mod kernels' rare Knuth paths - #116

Merged
benaadams merged 1 commit into
mainfrom
test/mod-remainder-kernel-coverage
Sep 3, 2026
Merged

Cover the remainder-only Mod kernels' rare Knuth paths#116
benaadams merged 1 commit into
mainfrom
test/mod-remainder-kernel-coverage

Conversation

@LukaszRozmej

@LukaszRozmej LukaszRozmej commented Sep 3, 2026

Copy link
Copy Markdown
Member

Follow-up to #115, which landed the remainder-only Mod kernels. Those kernels' branches divide by how far the Knuth quotient estimate is off — the D3 correction, its second iteration, the saturating digit where the window's top limb equals the divisor's, and the add-back that fires on a borrow out of a limb no correction carried into.

I instrumented all 24 of those branches and ran the suite against the counters. It reaches 23 of 24, and several only incidentally: operands drawn at random almost never put the estimate near its bound. The one it never executes is the four-limb kernel's second D3 correction.

What is in it

One new test file, ModKernelTests.cs. Cases are constructed rather than sampled: x = q*y + r with q and r on the boundaries that drive those branches, swept over every (dividend limbs)x(divisor limbs) shape and every normalising shift, plus bit boundaries, the signed sign-combination arms, and single-limb moduli for MultiplyMod.

Two operand pairs found by search are pinned as literals for the uncovered branch. It needs a divisor whose normalised top limb sits just above 2^63 together with a dividend that fills limb 3, which a randomised search reaches about eight times in twenty million pairs — not something a generator should be relied on to rediscover.

Expected values come from BigInteger, never from limb arithmetic, so a shared mistake in the limb code cannot make a test agree with itself. Each pair is also checked against the remainder implied by Divide, which still goes through the full-quotient routine — a separate body of limb code, so the two agreeing is independent evidence, and a mismatch localises which of the two is wrong.

Validation

Branch coverage, from the instrumented build: 24 of 24, this file alone.

Injected faults, to check the tests bite rather than just pass — 8 of 10 caught:

Injected fault Tests failed
add-back dropped, four-limb kernel 65
add-back dropped, three-limb kernel 66
add-back ignores the carry (borrow instead of borrow & (rcarry ^ 1)) 3
saturation guard removed, three-limb 66
saturation guard removed, two-limb 68
second D3 correction removed, two-limb 66
digit skipped when it was needed, three-limb 3
digit skipped when it was needed, two-limb 7
second D3 correction removed, three-limb 0
second D3 correction removed, four-limb 0

The two survivors are not test escapes. Both kernels have an add-back, and Knuth's D3 test firing at all means the estimate was too large, so one correction plus the add-back reaches the same remainder — the second iteration is a performance refinement there, not a correctness requirement. Disabling it in the four-limb kernel moved two operations from the correction onto the add-back and changed no answer. The two-limb kernel has no add-back and its corrections must be exact, which is why removing its second correction fails 66 tests.

That is worth a look on its own: if the second correction is behaviourally dead in the three- and four-limb kernels, and it fires roughly eight times in twenty million operations, it may not be paying for the code and the branch. Not changed here — this PR only adds tests.

Separately, Mod/Divide/MultiplyMod/AddMod/SubtractMod/Int256.Mod were fuzzed against BigInteger over ~27M pairs with zero mismatches, in the default, no-AVX-512, no-AVX2 and no-intrinsics configurations — the last being the reciprocal path ARM64 takes. That fuzzing is what found the pinned pairs; it is not checked in.

  • 587,786 tests pass (587,686 + 100).
  • dotnet format whitespace --verify-no-changes clean, git diff --check clean.

Mod's kernels answer x % y without forming the quotient, and their branches
divide by how far the quotient estimate is off: the D3 correction, its second
iteration, the saturating digit where the window's top limb equals the divisor's,
and the add-back that fires on a borrow out of a limb no correction carried into.
Instrumenting those branches showed the suite reaches all of them except one, and
reaches several only incidentally, because operands drawn at random almost never
put the estimate near its bound.

So the cases here are constructed rather than sampled: x = q*y + r with q and r
on the boundaries that drive those branches, swept over every shape and every
normalising shift. Two operand pairs found by search are pinned as literals, for
the four-limb kernel's second D3 correction - it needs a divisor whose normalised
top limb sits just above 2^63 together with a dividend that fills limb 3, which a
randomised search reaches about eight times in twenty million pairs.

Expected values come from BigInteger, never from limb arithmetic, so a shared
mistake in the limb code cannot make a test agree with itself. Each pair is also
checked against the remainder implied by Divide, which still goes through the
full-quotient routine: a separate body of limb code, so the two agreeing is
independent evidence and a mismatch says which of the two is wrong.

Every kernel branch is now covered, and injected faults are caught - a dropped
add-back, an add-back that ignores the carry, a missing saturation guard, a digit
skipped when it was needed, and the second correction removed from the two-limb
kernel. The second correction in the three- and four-limb kernels survives
removal, because both have an add-back and Knuth's test firing at all means the
estimate was too large, so one correction plus the add-back reaches the same
remainder; the two-limb kernel has no add-back and its corrections must be exact.

A hundred cases, a few seconds on top of the suite.
@benaadams
benaadams merged commit d82c640 into main Sep 3, 2026
14 checks passed
@benaadams
benaadams deleted the test/mod-remainder-kernel-coverage branch September 3, 2026 14:11
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.

2 participants