Hardening pass: sanitizers, coverage, cross-engine oracle, perf gates#14
Merged
Conversation
Add a BOUND_SANITIZE CMake option (AddressSanitizer + UndefinedBehaviorSanitizer, -fno-sanitize-recover=all so any finding is a hard failure) and a Clang-18 Debug Linux CI cell that builds the whole suite under it. The fast paths lean on hand-rolled 128-bit intermediates, signed-overflow detection and bit-shifts — exactly the UB UBSan catches — yet nothing exercised them in CI before. The sanitized cell additionally runs the property fuzzer under UBSan as a short fixed run (seed 1, 500 iters/property, ~90M checks, ~75s); the long sweep stays on the existing heavy cells. Suite, examples and fuzz are all clean under g++ sanitizers locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the existing -DBOUND_COVERAGE target into CI as its own gcc-14 job that builds everything, runs the coverage target, prints the lcov summary, and uploads the HTML report — coverage was measurable but never tracked. Close two reachable gaps the report surfaced: - wrap on a grid spanning the whole u64 range hits apply_wrap's urange==0 fast path (span wraps to 0 in umax), where the wrap is the identity; asserted as a raw round-trip in test_wrap_overflow.cpp. - a real (double-backed) grid rejects NaN/+-inf with errc::not_finite; pinned in test_real_exact.cpp as a behavioral regression. Line coverage 98.1 -> 98.2%. The remaining lines are defensive traps (shadowed by earlier guards) and other-engine-only branches exercised by the float/CORDIC cells, not the default-engine coverage run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
determinism.md and the cmath engine note guarantee the cordic/dbl/flt transcendental engines agree to within ~one output notch on a shared snap grid, but nothing enforced it — the engine tests only pinned exact special values. Add prop_cross_engine: a randomized sweep asserting cordic agrees with both dbl and flt for sin/cos/tanh/atan and sqrt on [0,4]. The engines genuinely differ (~10% of inputs, measured) with a true spread of exactly one notch, so the property is non-vacuous; the 8-notch tolerance catches gross divergence while absorbing legitimate ±1-notch ties and binary32 rounding. Guarded on !BND_MATH_NO_FP (the fixed engine leaves only cordic — nothing to compare). Also apply the BOUND_SANITIZE flags to single_header_freestanding_smoke (it links no bound::bound, so the interface flags miss it) and run it in the sanitizer cell, so the -fno-exceptions / BND_NO_STRING error path is exercised under ASan+UBSan rather than only compiled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bench harness is build-only — a benchmark has no pass/fail — so nothing caught a perf regression. The library's pitch is zero-overhead, autovec-friendly arithmetic; a silent de-optimization (a fast path falling back to the rational path, or losing vectorization) would pass CI today. Two deterministic gates, both immune to shared-runner noise: * Codegen guard (tests/perf_codegen.cpp + check_codegen.sh): compiles the arithmetic fast paths at -O2 and asserts via objdump that the scalar add stays call-free (no checked/rational/error fallback inlined in) and the loop vectorizes (packed integer add). Verified to fire on both regression modes. Registered as a labelled ctest, x86-64 GNU/Clang; runs across the linux matrix and is pinned in a dedicated perf job. * Cachegrind instruction-count gate (tests/perf_workload.cpp + check_perf.py): runs a small, deterministic, BND_PERF_SCALE-scaled workload under cachegrind and fails a >5% growth in retired instructions vs tests/perf_baseline.json. Bootstraps the baseline on first run (writes + passes, uploads it as an artifact to review and commit); active once the baseline is committed. The check script is validated end-to-end against a fake valgrind shim. Kept lean to respect the CI time budget: cachegrind runs only on the dedicated perf cell, on a ~1e6-op workload, with a 10-minute timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first CI run failed the codegen guard on GCC 14: it doesn't autovectorize the loop at -O2 while GCC 15 (local) does, so asserting a packed SIMD add was compiler-version-flaky. Demote vectorization to an informational note. Keep the robust, compiler-stable invariant — both fast paths must be call-free — and apply it to the loop body too. (Note: bound's rational arithmetic on compile-time grids constexpr-folds to call-free code, so this guards mainly against a fast path calling into the error handler / a non-inlined helper, i.e. "no throw in the inner loop"; the cachegrind instruction-count gate remains the fine-grained perf signal.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captured from the perf cell's first run on this branch (gcc-14, ubuntu-24.04): integer_qformat = 9,793,323 retired instructions (Ir). With the baseline present, check_perf.py now gates — a >5% instruction-count regression fails CI instead of bootstrapping. Refresh with `check_perf.py --update` when a change legitimately moves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A hardening pass on an already-mature, TODO-free codebase: the value here is in CI
coverage and regression nets, not feature work. Four independently-reviewable
commits, no production-header changes.
What's in it
ASan + UBSan in CI (
a09a55c)New
BOUND_SANITIZECMake option and a Clang-18 Debug cell running the suite +a short fuzz run under
-fsanitize=address,undefined -fno-sanitize-recover=all.The fast paths lean on hand-rolled 128-bit intermediates, signed-overflow
detection and bit-shifts — exactly the UB UBSan catches — yet nothing exercised
them before. Clean locally under g++ sanitizers: 374 tests, 36 examples, 92M+
fuzz checks, zero findings.
Coverage job + edge tests (
db9e10e)Wires the existing
-DBOUND_COVERAGEtarget into a dedicated job (lcov summary +HTML artifact). Closes two reachable gaps it surfaced: the wrap-identity
full-
u64-span path (apply_wrapurange==0), and arealgrid rejectingNaN/±inf with
not_finite. Line coverage 98.1 → 98.2%.Cross-engine differential oracle + freestanding sanitize (
01a5886)Enforces the documented guarantee that the cordic/dbl/flt transcendental engines
agree to within ~one output notch (the engines genuinely differ on ~10% of
inputs, measured max spread exactly one notch — so the property is non-vacuous).
Also applies the sanitizer flags to
single_header_freestanding_smokeand runsit, so the
-fno-exceptions/BND_NO_STRINGerror path is exercised underASan+UBSan rather than only compiled.
Performance regression gates (
f106f7c)Two deterministic, runner-noise-immune gates (the
benchharness is build-only,so nothing caught a perf regression before):
-O2and asserts viaobjdump that the scalar add stays call-free and the loop vectorizes. Verified
to fire on both regression modes.
BND_PERF_SCALE-scaled workload under cachegrind and fails a >5% growth vs acommitted baseline. Bootstraps the baseline on first run (writes + passes,
uploads it as an artifact to review/commit); active once committed. The check
script is validated end-to-end against a fake-valgrind shim.
Kept lean to respect CI time: cachegrind runs only on its dedicated cell, on a
~1e6-op workload, with a 10-minute timeout; the sanitized fuzz is a short fixed
run while the long sweep stays on the existing heavy cells.
Notes for the reviewer
perf-baselineartifact from the first run on this branch needsto be downloaded and committed as
tests/perf_baseline.jsonto make that gateactive; until then it bootstraps and passes.
core.hppdispatch plumbing) wasinvestigated and declined:
dispatch_out_of_rangeis already the sharedscaffold,
handle_out_of_rangeisn't triplicated, and theapply_*kernelsare legitimately domain-specific. The only boilerplate (the
&/&&ref-qualifier pairs) can't be collapsed without C++23 deducing-this, which
would break the
-DBOUND_CXX20backport. No net-positive safe refactor.valgrindandclang-18tool invocations run in CI (no sudo locally toinstall them); the CMake/script/parser logic around them is verified.
🤖 Generated with Claude Code