Repository navigation
Conversation
…gating The Linux, macOS and Windows jobs enable `set -o pipefail`, and primesearch#216's self-test loops pipe into tee. A self-test exiting 2 - "too few usable radix sets", which the gate is written to tolerate - therefore propagated out of the pipeline and, with `set -e`, ended the step before the PIPESTATUS check could classify it. Observed on this branch: several threaded Linux jobs failed with exit code 2 immediately after a self-test, no error message. Turn pipefail off for the duration of those loops so the pipeline status is tee's, and read the self-test's real status from PIPESTATUS[0] as intended. Verified the idiom behaves as claimed: a command exiting 2 through `|& tee` is tolerated and PIPESTATUS[0] still reports 2, while a 1 would set rc=1. The same flaw is in primesearch#216 itself, which is where the loops come from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
primesearch#216 defines two self-test statuses - 1 for a wrong answer, 2 for a length with too few usable radix sets - and the gates test for exactly 1. Everything else passes, including a crash. That is not hypothetical: the macOS jobs on the previous push aborted eight self-tests with Assertion '0' failed: Multithreading must be enabled in build to permit -cpu and the gate waved all eight through with exit 134. The job only went red later, when config-fermat.sh hit the same assertion under `bash -e`. Accept the two documented statuses and fail on anything else. Verified against exits 0, 1, 2 and 134. Same flaw is in primesearch#216, where the gates come from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per review, use GitHub's built-in paths filter rather than a gate job. This replaces the dorny/paths-filter 'changes' job and the fourteen per-job 'needs: changes' + if-expression pairs with seven lines on the trigger, so the PR is now a net addition of 7 lines rather than 49, and it no longer touches the parts of ci.yml that primesearch#103, primesearch#155 and primesearch#216 also edit. A docs-only PR currently spends 65 jobs building and testing every platform for nothing. Filtering is on pull_request only: pushes to main and the monthly schedule still run everything, so nothing is skipped on the authoritative branch or the cron sweep. paths-ignore rather than paths, deliberately - a denylist keeps running CI for anything unrecognised, where an allowlist would fail open to skipping the moment someone adds a file type nobody thought to list. GitHub skips the run only when *every* changed file matches, so a PR touching both docs and source still runs the full matrix. Checked against the cases that matter: docs-only skips; source, workflow, shell-script, help.txt, mixed docs+source, and an unrecognised new file type all still run. Dropped the separate 'shell' category and its ShellCheck gating: that job takes a few seconds, so separating it saved nothing. No branch protection is configured on main, so there are no required status checks that a skipped run could leave permanently pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7599ecc to
315415f
Compare
|
Rebased onto current Nine conflicts, all in The gate is now checked end to endWorth showing, because the first control I ran did not actually test anything. so the job stays green on 2 and goes red on 1, which is what the gate is for. Getting there took two false negatives worth recording. Corrupting the 1K row proved nothing: that case halts on a roundoff error in an AVX2 build before any residue is compared. Corrupting nineteen larger rows then also proved nothing, because Build OK, and the stock |
Per review, use GitHub's built-in paths filter rather than a gate job. This replaces the dorny/paths-filter 'changes' job and the fourteen per-job 'needs: changes' + if-expression pairs with seven lines on the trigger, so the PR is now a net addition of 7 lines rather than 49, and it no longer touches the parts of ci.yml that primesearch#103, primesearch#155 and primesearch#216 also edit. A docs-only PR currently spends 65 jobs building and testing every platform for nothing. Filtering is on pull_request only: pushes to main and the monthly schedule still run everything, so nothing is skipped on the authoritative branch or the cron sweep. paths-ignore rather than paths, deliberately - a denylist keeps running CI for anything unrecognised, where an allowlist would fail open to skipping the moment someone adds a file type nobody thought to list. GitHub skips the run only when *every* changed file matches, so a PR touching both docs and source still runs the full matrix. Checked against the cases that matter: docs-only skips; source, workflow, shell-script, help.txt, mixed docs+source, and an unrecognised new file type all still run. Dropped the separate 'shell' category and its ShellCheck gating: that job takes a few seconds, so separating it saved nothing. No branch protection is configured on main, so there are no required status checks that a skipped run could leave permanently pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
315415f to
c47251b
Compare
…rimesearch#296's order Merging PR primesearch#296 - which moves the ASan ladder to after config-fermat.sh and the Mfactor runs - with PR primesearch#216, which edited the ladder in its old position, left ASan-macOS and ASan-Windows with both copies: primesearch#216's gated one before config-fermat.sh and primesearch#296's ungated one after the Mfactor loop. Both jobs ran the whole ladder twice, and the ungated second copy ends the job under set -e on the status-2 the first copy was written to tolerate. Keep one ladder, gated as in primesearch#216 and placed as in primesearch#296. ASan-Linux already merged that way; this brings the other two into line with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Two follow-up commits, both fixing defects in this PR's own gating. The status was never examined. The pipeline form time ./Mlucas -s $s -cpu "..." |& tee -a test.log
st=${PIPESTATUS[0]}runs under The gate was too narrow.
|
76f51f8 to
0d68e2c
Compare
|
Rebased onto
I tested both gates against a stub |
…cover the FFT lengths it never reached) into all-fixes-integration-v5 # Conflicts: # src/Mlucas.c
…cover the FFT lengths it never reached) into all-fixes-integration-v5 # Conflicts: # src/Mlucas.c
0d68e2c to
02718b0
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Residue checks and result accounting src/Mlucas.c, help.txt |
Self-tests detect incorrect residues, count test outcomes, and return status 1 for wrong answers or status 2 when an FFT length has too few usable radix sets. The help text documents the exit statuses. |
Unlisted FFT-length coverage sweep src/Mlucas.c, help.txt |
The -s c, -s cover, and -s coverage options sweep supported FFT lengths without reference rows. Each length uses the nearest lower-or-equal table row for its exponent and reference residues. |
Script and CI status handling config-fermat.sh, .github/workflows/ci.yml, .github/workflows/selftest-large.yml |
The configuration script continues after status 2 and returns other nonzero statuses after processing. CI self-test steps capture Mlucas statuses, accept 0 and 2, and fail for other statuses. |
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Bug fix
Possibly related issues
- primesearch/Mlucas#224 — The issue connects this pull request to self-test coverage and status-gating changes.
Possibly related PRs
- primesearch/Mlucas#365 — Both pull requests rely on status 2 being accepted for self-tests in CI.
- primesearch/Mlucas#263 — Both pull requests change status propagation for tee pipelines in ASan workflow steps.
Suggested labels: enhancement, github_actions
Merge Risk: 
Minimal · up to c4552
-
The documented exit statuses now match how excessive-roundoff cases are handled. No actionable merge-blocking risk remains after normal checks.
Architecture risk: 
Low · up to faf88
-
The change affects 3 systems.
Changed systems:
config-fermat.sh,help.txt,srcArchitecture concerns
No architecture-level concerns identified.Review details
-
Systems and components
- observed — config-fermat.sh (service) was modified; 1 changed file maps to changed impact.
- observed — help.txt (service) was modified; 1 changed file maps to changed impact.
- observed — src (service) was modified; 1 changed file maps to changed impact.
Before / after behavior
- observed — Modified behavior in help.txt: Adds
-s cover/-s cfor 100- or 1000-iteration coverage of supported FFT lengths lacking built-in reference-exponent rows, specifically the odd-31 and odd-63 families from 992K through 32M. Each length uses the exponent and stored residues of the nearest tabulated length at or below it; the stated runtime is roughly that of-s l. - observed — Modified behavior in help.txt: Documents self-test exit statuses: 0 if all radix sets match references or are skipped for a reason that does not impugn the answer; 1 if any case has a wrong answer, including a residue mismatch, disagreement between radix sets at one length, or an identically zero residue; and 2 if there are no wrong answers but a length has too few usable radix sets to test. A one-line summary is printed at the end.
- observed — Modified behavior in src/Mlucas.c: Interactive self-tests now flag a residue as incorrect when all three Selfridge–Hurwitz residues are zero, except for a Mersenne primality test run for exactly (p-2) iterations. Unlike the removed comparison branches, matching any supplied reference residue no longer clears an existing failure; mismatches still set it.
- observed — Modified behavior in src/Mlucas.c: The Res35m1 reference check now tests for inequality directly; a match no longer clears a failure recorded by an earlier check.
-
Pre-merge checks: 
4
-
Full details: Title check
-
Explanation
The title names the two main changes in the PR: making the self-test able to fail (wrong answers return status 1, untested lengths return status 2) and extending coverage to FFT lengths the self-test never reached through the new -s c/cover sweep. The raw summary and PR objectives both confirm these as the primary changes. The title is 81 characters, which is slightly long but still a single descriptive sentence.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
02718b0 to
6815302
Compare
…ngths it never reached The self-test could not report a failure. Five separate defects, each sufficient on its own to make a wrong answer invisible: 1. main() returned 0 unconditionally from the -s/-iters path, including right after printing "WARNING: 0 of N radix-sets at FFT length L K passed - PLEASE CHECK YOUR BUILD OPTIONS". The four CI self-test steps invoke `Mlucas -s <tier>` and rely on its exit status, so no self-test regression could ever turn CI red. On an AVX2 build of the current tree `-s tt` reports 0 of 2 radix sets passing at both 1K and 2K and still exits 0. Now: exit 1 if any case returned a demonstrably wrong answer, 2 if no wrong answers but some FFT length had too few usable radix sets to be tested at all, 0 otherwise. Splitting the two lets a caller gate on wrong answers alone on a host with a known-marginal FFT length. 2. The new-data consensus anchor test was (!new_res.sh0), which conflates "no anchor yet" with "the anchor is 0". If the first radix set returned Res64 = 0 - the signature of the radix1008/4032 and AVX-512 radix1024 silent-wrong- answer defects - every subsequent radix set re-took the "first" arm and was counted as having succeeded whatever it computed, and a .cfg entry asserting a 0 consensus residue was written. Anchor on the count of radix sets which have reached that point instead. This path covers all 42 reference-free rows of MvecPRP[], the 10000-iter column of the 17 largest MersVec[] rows, and every FFT length absent from the tables. 3. The Selfridge-Hurwitz comparisons in ernstMain's timing-test block set resFlag = 0 on a match, so a Res64 mismatch was erased by a subsequent Res35m1 match. Only ever set it. 4. Nothing rejected an identically-zero residue when no reference was stored - it was silently adopted as the new reference. The roundoff-error check cannot see this: fracmax measures distance from the nearest integer and zero is an integer, so a zeroed residue reports AvgMaxErr = MaxErr = 0. Reject it, with an exemption for the one case where zero is the correct answer (an LL test run to exactly p-2 iterations). 5. The "FFT length not in the reference table" test in the -fft handler was (i != MvecPtr[j].fftLength) evaluated at j == numTest, i.e. against the scratch row into which the -fft argument had just been copied - never true. The auto-pick-an-exponent branch behind it was therefore dead and `-fft L -iters N` for an off-table L exited with "nor user-supplied command-line exponent" instead of running. Test j == numTest, as the equivalent loop 30 lines above already does. Plus new '-s c': a coverage sweep over the supported FFT lengths that have no reference-table row. Every -s tier steps through rows of MersVec[]/MvecPRP[], so the lengths those tables list are the only ones the self-test can reach. A SIMD build supports 153 FFT lengths / 603 (length, radix set) pairs; the tables reach 136 / 564. The 17 missing lengths are exactly the odd-31 and odd-63 families (992, 1008, 1984, 2016, ... 516096), and they are the only place leading radices 63, 992, 1008 and 4032 are ever used - 4 of the 34 leading radices a SIMD build can select had no self-test coverage of any kind. The sweep needs no new reference data: the residue triplet after a fixed number of iterations depends only on the exponent and the iteration count, not on the FFT length, so an uncovered length can borrow both the exponent and the stored residues of the nearest table row at or below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate on the status Follow-on to the previous commit: o Print a one-line "N of M (FFT length, radix set) cases ran and gave the expected residue" summary at the end of every self-test, pass or fail. Without it the only record of a radix set having been skipped is a line buried thousands of lines up in the log - which is how the fact that a stock AVX2 build fails every radix set at both 1K and 2K went unremarked. The counts cross-check against an independent enumeration of get_fft_radices(): -s tt = 26 cases, -s t = 93, -s s = 127 in an AVX2 build. o help.txt: document -s c and the exit-status contract. o ci.yml: the four self-test steps invoked `Mlucas -s <tier>` and relied on its exit status, which was always 0; three of them additionally piped into tee, whose status is the one bash reports for a pipeline, so even a fixed exit status would have been discarded. Capture the real status via PIPESTATUS and fail the job on status 1 (a wrong answer). Status 2 (an FFT length with too few usable radix sets) is tolerated for now because it fires on a stock build at 1K and 2K; the comment says to tighten to 0 once that is fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects in the gating this PR added.
The pipeline form - 'time ./Mlucas -s $s |& tee -a test.log' followed by
'st=${PIPESTATUS[0]}' - runs under set -e with pipefail, so a nonzero status
ends the step on the pipeline itself and the next line never executes. The
whole point of capturing the status was to tolerate 2; instead every job that
hit it died on the first tier of the ladder, before running any of the others.
Capture the status in a '|| st=${PIPESTATUS[0]}' tail, which set -e does not
apply to.
The gate itself was 'st == 1', which passes everything else - including the
statuses the self-test never returns. A SIGSEGV part-way through '-s m' (139)
or an abort (134) counted as a clean ladder. Gate on 'st != 0 && st != 2', so
2 stays tolerated for the stock build's 1K/2K rejections and nothing else does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Callers run this as 'bash -e -o pipefail -- ./config-fermat.sh', so now that Mlucas returns a nonzero status the first affected length ends the script. On ARM that is F18 at 15K, three of whose four radix sets decline Fermat-mod, so F19 through F29 were never configured and never tested - the run looked like it had simply finished. Carry on past status 2, which says only that this build could not write a cfg entry for that length, and exit nonzero at the end for anything else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vmactions/freebsd-vm runs with usesh: true, so that step is /bin/sh, not bash.
${PIPESTATUS[0]} and (( ... )) are bashisms:
sh: ${PIPESTATUS[...}: Bad substitution
##[error]Process completed with exit code 2.
The gate never worked there - previously set -e killed the step on the pipeline
before either bashism was reached, so the breakage was invisible.
'set -o pipefail' is already set at the top of the step and tee always succeeds,
so the pipeline already carries Mlucas's own status and a plain '|| st=$?'
captures it. The arithmetic tests become [ ... ] -ne / -eq.
Checked against a stub binary under a POSIX shell: status 0 and 2 pass the gate,
1 and 139 fail it, and the block parses under ash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…new exit status and removed continue-on-error from the two TSan jobs. All four run the self-test bare under set -e, so with this PR's exit status they would end their jobs on status 2, which a stock build still returns at 1K and 2K. Gate them like the other jobs: 0 and 2 pass, anything else - a wrong answer, a crash - fails. (The rebase onto primesearch#291 also changed the FreeBSD gate: primesearch#291 added FreeBSD 10, whose /bin/sh has no pipefail, so that gate no longer pipes Mlucas into tee. It writes to a file, takes the status directly, and then shows the file and appends it to test.log.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est workflow selftest-large.yml (from primesearch#294) runs '-s l' and '-s h' in plain loops under pipefail, which is right while the self-test always exits 0. With this PR it exits 1 for a wrong answer and 2 when some FFT length had too few usable radix sets, so give those loops the same rule as ci.yml: fail on 1, pass on 2. The FreeBSD job uses the POSIX form, as in ci.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…under a plain 'bash' too
Without pipefail the status of the Mlucas | tee | grep pipeline is grep's, and grep almost always
matches a line of Mlucas's output, so '|| st=${PIPESTATUS[0]}' never ran and a wrong answer or a
crash passed. CI invokes the script with pipefail, but the documented invocation does not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main, where the TSan jobs tee their self-test output into test.log, so their gate reads the self-test's status from PIPESTATUS. Say so in the comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fftLength is an int and len a uint32, which drew -Wsign-compare now that main builds with -Wextra. Table lengths are positive, so the cast changes nothing but the warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each -s argument rewrites start/finish, so '-s t -s c' silently dropped the tiny tier and ran only the coverage sweep, and '-s c -s t' left start at the tiny tier's offset while the sweep set finish = nsweep, so no case ran and the run reported 0 of 0 with exit status 0. The same happened with '-s c -fft 992 -iters 100', where the -fft handling leaves start = numTest. -m and -f point start/finish at the single-case slot too, and -f switches the modulus to Fermat, which the sweep does not support. Allow only one -s per run, and refuse -s c together with -fft, -radset, -m or -f with an explicit message and a nonzero exit, instead of running nothing and reporting success. Assert start == 0 when the sweep sets finish, so a sweep that would run zero cases can no longer pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
primesearch#365 moved the single-threaded jobs' self-tests into parallel steps. Apply the same rule as the other jobs there: 0 and 2 pass, anything else fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1ceddcf replaced the TSan self-test lines with the exit-status gate and took the upload-artifact step after each with them. Put both back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ecc7f99 to
c45526f
Compare
|
I rebased this onto main, only to fix the conflicts in
Everything else is unchanged. |
|
@coderabbitai review |
Action not completed
|
The self-test has a good correctness check and throws its verdict away. This makes it capable of failing, and extends it to the FFT lengths it never reached.
The core defect
Mlucas.c:4853-4855:-sand-fft/-radsetalways exit 0. Measured on a stocknosimdbuild of418f365:And
ci.yml's four self-test steps gate on that status — three of them additionally pipe intotee, so even a corrected status would be swallowed by the pipeline. No self-test regression could ever have turned CI red.Four more, underneath
if(!new_res.sh0). A first-radsetRes64 == 0leaves the anchor falsy, so every radix set is scored as passing whatever it computed. On pristine main,-m 18776473 -fft 992 -iters 100printsRes64: 0000000000000000. AvgMaxErr = 0.000000874and writes that zero intomlucas.cfgas a certified "2 of 2 consensus". Exposure: 42 of 136MvecPRProws, the 10000-iter column of 17MersVecrows, and every off-table-fft.resFlag = 0on a match erases an earlier Res64 mismatch.i != MvecPtr[j].fftLengthis evaluated atj == numTest, against the row just written a few dozen lines earlier, so it is never true.-fft 992 -iters 100refused to run at all.Note that
fracmax/AvgMaxErrcannot substitute for any of this: it measures distance from the nearest integer, and zero is an integer, so an all-zero residue reports perfect numerical health.Coverage, measured rather than assumed
The "56 of 603" figure in circulation compares two different populations — 56 is the nosimd Teensy count, 603 the SIMD total. Enumerated properly via
get_fft_radices()in a harness linked against the real objects: 153 legal lengths / 603 (fftlen, radix_set) pairs under SIMD, 708 under nosimd.-s acovers 454/603 (75%); all tiers together 564/603 (93.5%). Per tier on avx2: tt 26, t 93, s 127, m 145, l 63, h 67, e 43 — reproduced exactly by the new summary line.The gap that matters is not the 6.5%. The 17 uncovered lengths are exactly the 31·2ᵏ and 63·2ᵏ families — the only lengths at which leading radices 63, 992, 1008 and 4032 are ever used, and therefore precisely where every silent-wrong-residue defect found in this campaign lives. All 27 radix sets there fail today in plain AVX2.
What this changes
-s ccoverage sweep over the uncovered lengths. It needs no new reference data:Res64depends only on (exponent, iters), so each length borrows the nearest tabulated row. Verified — p=17551099 givesAB1180428ED65EE0at 896K and at all 12 radsets of 1024K.resFlagfix, the dead-branch fix, an always-printed summary, and CI gating via${PIPESTATUS[0]}so theteeno longer swallows the status. CI fails on status 1 only, since status 2 fires on today's 1K/2K.selftest-large.yml(added since by CI: move the large self-test to its own monthly workflow, and add the huge self-test #294): fail on 1, pass on 2, in POSIX form in the FreeBSD job.Plain cross-radix-set agreement was considered and measured insufficient — at 992K both radsets share leading radix 992 and both return
0000000000000000, so they agree with each other and are both wrong.Validated failing-first
A test that cannot fail is the subject of this PR, so each check was demonstrated to fail before being demonstrated to pass:
Res64 Error, exit 1-fft 8 -radset 1)ERR_CARRY-s t/-s snegative controlsIndependently reproduced here before pushing: stock main
-s ttexits 0 on the 4K/5K/7K/9K/10K warnings above; this branch exits 1 on the identical set.Runtimes (avx2, 4 threads): tt 2.2 s, t 15 s, s 44 s, m 1493 s, and
-s c≈2–4 min once those lengths work.Known blockers for a stricter gate, not fixed here
1K and 2K are unusable in a stock AVX2 build of main — 1K gives ROE ≈0.4993 on iteration 1, and 2K radset 0 hits
ERR_ASSERT"radix < 16 does not support shifted residues" against the self-test's own randomRES_SHIFT. That is why CI gates on status 1 rather than!= 0. AVX-512 under SDE loses 8 of 15 teensy lengths. And-snever exercises Fermat-mod at all.Summary by CodeRabbit
New Features
Bug Fixes