Skip to content

Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard - #24

Merged
tap merged 4 commits into
mainfrom
claude/wave1-stage2a-tests
Sep 17, 2026
Merged

tap merged 4 commits into
mainfrom
claude/wave1-stage2a-tests

Conversation

@tap

@tap tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner

What this changes

Adds the Stage 2a test suite from docs/audit-fft-and-code-smells.md (Part 3 Stage 2a, Part 9), written against the current vendored C so the wave-2 port agent works test-first against a suite that is green today and goes red the moment engine_under_test is re-pointed at the ported engine. Based on claude/wave1-stage1-legs (#17) because the two PRs share tests/CMakeLists.txt and the parity targets register through #17's bare-metal helper; retarget to main after #17 merges.

  • tests/test_fft_parity_ooura.cpp + its own target tap_dsp_fft_parity: memcmp bit identity of basic_real_fft<double|float> against raw rdft/rdft_f from a private reference build of fftsg.c/fftsg_float.c. Both the test TU and the reference C are compiled with -ffp-contract=off (GCC/Clang/AppleClang; MSVC gets nothing — its /fp:precise has not contracted since VS 2022 17.0). Forward and inverse, in place and unnormalized, N = 4 … 65536 plus one run at 2^20, on broadband xorshift, on-bin tone, impulse, DC and full-scale alternating ±1. ooura_ref<Sample> and engine_under_test<Sample> are each re-pointable in one place. TAP_DSP_PARITY_MAX_N (cache var; default 1048576 hosted, 4096 when CMAKE_CROSSCOMPILING, a toolchain's own cache value wins; static_asserted power of two ≥ 4) caps the sweep; the 2^20 case is its own test and is compiled out below the cap (a GTEST_SKIP is rc=1 on the one-shot target).
  • tap_dsp_fft_parity_default_flags: same source at default flags on both sides, against a second private reference build of the same C at default flags (not tap::dsp_fft, which Stage 2c removes and which carries CMSIS objects on the M55). One test for both precisions; prints the measured max-ulp table per N with the worst material, and GTEST_OUTPUT=xml (attached as a ctest ENVIRONMENT property through a TEST_INCLUDE_FILES script, since the helper has no EXTRA_ARGS pass-through) writes the RecordProperty values to <build>/parity-ulp.xml. Never fails (Part 6, N3). Where the number is visible: not under plain ctest --output-on-failure (passing stdout is hidden); ctest -L parity -R default_flags -V shows it, which Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's ci.yml runs as its own step (Part 13), and the XML is there for upload.
  • tests/test_fft_oracle.cpp (in tap_dsp_tests): closed-form vectors with exact answers — impulse → flat, DC → N in slot 0, Nyquist alternation → N in slot 1, on-bin cosine → +N/2 real, on-bin sine → +N/2 imaginary (the +i convention), two-tone — and their unnormalized inverses (gain N/2), swept N = 4 … min(65536, TAP_DSP_PARITY_MAX_N), checked to a tolerance derived from Higham's Theorem 24.2: 4 · ε · log2 N · ‖y‖₂ (derivation, its slack, and its √N looseness per element are in the comment; measured 0.25 / 0.17 of the bound for double / float). Plus a double-double (TwoSum/TwoProd via std::fma, not long double) compensated DFT for N ≤ 256 with double-double twiddles from exact octant reduction, self-checked against libm and by round trip. Typed over float/double; profile<Sample> is the marked extension point for int16/int32 (forward_scale(n), inverse_scale(n), tolerance(n, norm2), k_full_scale). Overlap with test_fft.cpp's closed forms is deliberate (Part 9 lists them in both) and stated in the header; names are distinct.
  • tests/test_fft_rt.cpp (in tap_dsp_tests): static_assert(noexcept(...)) on forward_inplace, inverse_inplace, same-type forward/inverse, size, num_bins for both instantiations; a program-wide counting replacement of all operator new/delete forms showing those four calls allocate nothing at N = 512 and 4096, both on the first call after construction (Ooura's lazy table init, F6) and in steady state; copy, copy-of-unwarmed-source and copy-assignment (over a different geometry) produce bit-identical output. Scope: it counts C++ allocation functions only; malloc/calloc and vendor-internal allocation (vDSP on macOS, CMSIS on the M55) are invisible to it — complete for the Ooura path, wrapper-only for the backends; stated in the header. The float-I/O overloads on the double engine are deliberately not guarded (documented allocating path, F7/D5).
  • tests/support/signals.h: one xorshift32, random_signal<Sample>, tone<Sample>(n, bin, amplitude, phase) (exact modular phase reduction — see Verification). Used only by the new files; migrating the three existing copies is Stage 6.
  • tests/CMakeLists.txt: additive only, one delimited block at the end, on top of Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's file: two target_sources/target_compile_definitions lines for tap_dsp_tests, the two reference libraries, the two parity targets via tap_dsp_add_gtest_executable, the XML environment script. Root CMakeLists.txt, fft.h and every other header untouched.

Why

Bit identity between the C and its C++ transliteration is only meaningful if both are compiled to the same sequence of IEEE operations, and CMake gives neither side an fp-contract setting; the gate has to own its flags (Part 4 fp-contraction policy, N3/N4). Parity alone cannot prove Ooura is a Fourier transform, hence the independent oracle. And "noexcept, allocation-free" has been a docstring claim, not a checked one.

Verification

Built and ran on x86-64 Linux, GCC 13.3 and Clang 18.1, -DCMAKE_BUILD_TYPE=Release -DTAP_DSP_WERROR=ON: 214/214 ctest green on both compilers, zero warnings under -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror. clang-format --dry-run --Werror and clang-tidy (repo .clang-tidy) clean on the new files.

  • Bare-metal one-shot mode on the host: -DTAP_DSP_BARE_METAL=ON -DTAP_DSP_PARITY_MAX_N=4096 configures and builds; ctest -V shows three markers, all TAP_DSP_TESTS_COMPLETE rc=0 … skipped=0: tap_dsp_tests_emulated selected=161 (30 fft_oracle* and 17 fft_rt* [ RUN ] lines under Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's negative filter), tap_dsp_fft_parity_emulated selected=4, tap_dsp_fft_parity_default_flags_emulated selected=1. The real QEMU legs run on this PR's CI.
  • Parity gate runtime: the 4..65536 sweep is 17–27 ms per test; the 2^20 test is 708 ms (double) / 451 ms (float) locally, 0.4–0.5 s on the CI runners — far under the ~20 s budget, so 2^20 stays.
  • Informational target, this host (GCC, default flags): 0 ulp at every N, both precisions — expected, since today both sides are the same Ooura code. Confirmed both channels: ctest -L parity -R default_flags -V prints the 38 table lines, and parity-ulp.xml carries 64 <property> rows (16 sizes × fwd/inv × 2 precisions).
  • -ffp-contract=off confirmed in flags.make for both the reference lib and the parity TU; objdump -d shows 0 FMA instructions in the parity object and both reference objects. That proves little on x86-64 without -march (no FMA to emit), so the reference C was probed at -O3 -march=haswell: gcc 200 / clang 171 FMA instructions by default, 0 with -ffp-contract=off. The recipe is in the test-file header.
  • Hosted -DTAP_DSP_PARITY_MAX_N=4096: the gate binary lists four tests (the 2^20 pair compiled out), all green.
  • Oracle bound margin measured: the largest |error| / (ε · log2 N · ‖y‖₂) over the whole file is 0.25 (double) and 0.17 (float) against the derived constant 4. Self-check maxima: twiddles vs libm 6.4e-16 (2.9 ε, dominated by rounding the double argument 2π·m/n; bound 3.5 ε, constant 4 ε), round trip 1.1e-16 (0.5 ε, constant 8 ε).
  • Found and fixed along the way: a naive cos(w * j) tone generator accumulates ~1e-11 phase error at N = 65536 and fails the double closed-form tests by itself; tone() reduces bin*j mod n exactly before forming the angle.

Not verified here, CI is the gate: macOS (AppleClang flag handling; the parity executables link the reference C directly and not tap::dsp, so no TAP_DSP_FFT_ACCELERATE reaches them and they stay Ooura-vs-Ooura on that leg; the RT guard in tap_dsp_tests does run against vDSP there), Windows/MSVC (no -ffp-contract flag passed; /W4 /WX on the new TUs, including the global operator new/delete replacement), and the four QEMU legs (real emulation of the three one-shot binaries; the arm toolchain and qemu are not in the development container). First revision on main: all three hosted legs plus the M55 compile, drift and clang-tidy green (6 jobs × 2 events).

Notes for the reviewer

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Hostile review B (process / downstream)

Scope: ownership, truthfulness of the PR body, Part 9 / Part 12 compliance, cross-PR hazards, downstream hand-offs, commit hygiene. Numerics are reviewer A's. Everything below was checked against 4ddff35 and the other seven wave-1 heads as of this review; local build in a private scratch dir, GCC 13.3, -DCMAKE_BUILD_TYPE=Release -DTAP_DSP_WERROR=ON: 215/215 green, 0 warnings, -DTAP_DSP_PARITY_MAX_N=4096 runs 6 and skips the two 2^20 tests as described.

Findings

1. Blocker (cross-PR). Merged next to #17, this PR breaks all four QEMU legs, and neither PR says who owns the fix.
Evidence: a trial merge of #24 into claude/wave1-stage1-legs conflicts in tests/CMakeLists.txt (the Stage 2a block anchors on the gtest_discover_tests(tap_dsp_tests …) line that #17 moves inside an else()). With the obvious resolution (keep #17's if/else, append the block), cmake -DTAP_DSP_BARE_METAL=ON fails at configure:

CMake Error at tests/CMakeLists.txt:116 (gtest_discover_tests):
  Unknown CMake command "gtest_discover_tests".

because include(GoogleTest) now lives in #17's else() branch. Hoisting the include does not save it: gtest_discover_tests runs the cross-compiled tap_dsp_fft_parity under CMAKE_CROSSCOMPILING_EMULATOR at build time with --gtest_list_tests, #17's harness states argv does not reach the target, both new binaries link GTest::gtest_main (not bare_metal_main.cpp), have no completion marker / PASS_REGULAR_EXPRESSION, and TAP_DSP_PARITY_MAX_N stays at 2^20 because no toolchain file sets 4096 (4 × 8 MB vectors in a 4 MB data region). #24's body says the block "rebases trivially"; #17's body and its measured-run comment never mention tap_dsp_fft_parity. Part 9 puts the parity gate on the four legs at N ≤ 4096 and Part 10 item 3 puts it in the harness filter, which assumes one executable; this PR made it a separate one, so #17's positive filter cannot reach it at all.
Fix (in this PR, since the block is yours): rebase onto #17; put include(GoogleTest) inside the block; register the two executables as

if(TAP_DSP_BARE_METAL)
    add_test(NAME tap_dsp_fft_parity_emulated COMMAND tap_dsp_fft_parity)
    set_tests_properties(tap_dsp_fft_parity_emulated PROPERTIES
        PASS_REGULAR_EXPRESSION "\\[  PASSED  \\]" FAIL_REGULAR_EXPRESSION "\\[  FAILED  \\]" TIMEOUT 1800)
    # default_flags target: same, or skip on bare metal with a stated reason
else()
    gtest_discover_tests(...)
endif()

and default TAP_DSP_PARITY_MAX_N to 4096 under CMAKE_CROSSCOMPILING (see 4). If you choose the minimal if(NOT TAP_DSP_BARE_METAL) guard instead, say in the PR body that Part 9's "parity on the four legs" is deferred and name the owner. Either way, both PR bodies must state the ownership.

2. Should-fix. "Rebases trivially" is false; here is the exact conflict picture and the order.
Evidence (trial merges): #24 vs #17: conflict tests/CMakeLists.txt. #24 vs #22 (3a), #19 (5), #21 (1b), #23 (capi), #18 (docs), #20 (0): clean. #22 vs #19: conflict with each other (both append one line after test_yin.cpp); #17 vs #22 / #19 / #21: clean.
Order that minimizes churn: #20#17#21#24 (rebased per finding 1) → #22#19 (one-line re-touch) → #18#23, i.e. Part 12's left-to-right table. Replace the "rebases trivially" sentence with this.

3. Should-fix. The informational ulp table is invisible in CI, and the request to the ci.yml owner is recorded nowhere.
Evidence: all three hosted job logs show only fft_parity_ooura_default_flags.ReportsMaxUlpVersusOouraDouble … Passed 0.23 sec; ctest swallows stdout of passing tests. My plain ctest log contains zero [ ulp ] lines; ctest -R default_flags -V shows the full table (0 ulp at every N, both precisions, confirming the PR-body claim). RecordProperty reaches JUnit XML only under --gtest_output=xml, which nothing passes, and ctest's --output-junit does not carry gtest properties. The test-file comment "Printed to stdout so it is in every CI log" is therefore false as configured, and Part 6 N3's "put the number on the record per platform" is unmet. No PR-body request, no issue, no comment on #17.
Fix: correct the comment; post on #17 (or open an issue) asking for a ctest --test-dir build -L parity -R default_flags -V step on the three hosted legs, so the per-platform number is in the log the day the port lands.

4. Should-fix. TAP_DSP_PARITY_MAX_N and LABELS parity are not where the legs owner will find them, and nothing sets 4096.
Evidence: the two hooks are documented only in the block comment and the test header. #17's four toolchain files and ci.yml never set TAP_DSP_PARITY_MAX_N; README's build/test section is untouched; no comment on #17. The block comment says "emulated targets set 4096 (Part 10)" as if it were done.
Fix inside your own block: if(CMAKE_CROSSCOMPILING) set(_tap_dsp_parity_max_n_default 4096) else() 1048576 endif() as the cache default, so the legs owner needs nothing; and record both knobs in the PR body's "Notes for the reviewer" as the hand-off (README is a contention file, so route the README line through its owner or docs/fft-design.md in #18, which already has an fp-contraction section at lines 130–163).

5. Should-fix. The oracle and RT suites never run on any QEMU leg, silently, and nobody has been asked to add them.
Evidence: #17's tests/bare_metal_main.cpp is a positive filter naming real_fft_test/0.*, CertifiedGeometries/*, etc.; fft_oracle_test, fft_oracle_self_check, fft_rt_test, fft_rt_guard are absent, and the ≥ 60 selected-tests floor will not notice. The RT guard also replaces the global operator new/delete program-wide in tap_dsp_tests, which every other suite on the target will run through; the legs owner should know that before it lands in their binary.
Fix: comment on #17 requesting fft_oracle_test/*.*:fft_oracle_self_check.*:fft_rt_test/*.*:fft_rt_guard.* in the filter with the measured emulation delta (the closed-form sweep to N = 65536 in soft-float double on the M4 legs is the cost to watch; if it breaks the 5-minute budget, cap closed_form_sizes() by the same TAP_DSP_PARITY_MAX_N).

6. Nit (truthfulness / reproducibility). The "gcc 200 / clang 171 FMA at -O3 -march=haswell, 0 with -ffp-contract=off" figure reproduces exactly here (GCC 13.3.0, clang 18.1.3) but exists only in the PR body; the repo carries no command line for it. Put the one-line objdump -d … | grep -c vfmadd recipe in the test-file comment so the next reviewer does not have to rediscover it. "12 checks green" is 6 jobs × 2 events (push + pull_request); fine, but say so.

7. Nit (duplication claim). fft_oracle_test.ImpulseHasFlatSpectrum reuses the exact name of real_fft_test.ImpulseHasFlatSpectrum (tests/test_fft.cpp:83), and DcLandsAsNInSlotZero / NyquistAlternationLandsAsNInSlotOne / OnBinCosine… / OnBinSine… restate DcAndNyquistPacking and SignConventionIsPlusI at N = 64/128. Part 9 sanctions the overlap (both files list the closed forms), so the value here is the N sweep and the derived tolerance, but "does not duplicate" is imprecise. One sentence in the oracle header relating the two, and a distinct name for the impulse test, so the ctest listing does not carry two ImpulseHasFlatSpectrum<double> rows.

8. Nit (tolerance rule). k_higham_constant = 4.0 is a round number defended as a derived bound with measured ratios 0.25/0.17 — acceptable for an oracle. The self-check constants 4.0 * eps and 8.0 * eps have no stated measurement; add the measured ratio or say why a bound suffices there.

9. Nit (Stage 2c hazard, record it). The block hard-codes ${PROJECT_SOURCE_DIR}/third_party/ooura/fftsg.c and fftsg_float.c. Stage 2c moves fftsg.c to tests/reference/ooura/ and deletes fftsg_float.c, so the float reference side of this gate disappears at 2c while the test header promises "the reference stays the C even after the library stops linking it (Stage 2c)". Either 2c keeps fftsg_float.c under tests/reference/ or float parity becomes port-vs-port; state which, for the 2c owner.

10. Nit (MSVC claim). "MSVC's default /fp:precise does not contract" holds for VS 2022 17.0+ (/fp:contract became opt-in); earlier MSVC contracted under /fp:precise on x64 /arch:AVX2 and ARM64. windows-latest is fine; qualify the comment.

Checked, no finding

  • Ownership: exactly the allowed files (tests/test_fft_parity_ooura.cpp, test_fft_oracle.cpp, test_fft_rt.cpp, tests/support/signals.h, additive delimited block in tests/CMakeLists.txt); root CMakeLists.txt and headers untouched.
  • The second compile of the reference C is private to tests, no tap_dsp_warnings on third-party C (matches root policy), no PIC needed; #21's bench measures through tap::dsp, so no collision. Note for the root owner in wave 2: after 2b the bench has no C target unless one like this is hoisted to root (Part 11 "port-vs-C pairs").
  • Wave-2 hand-off: engine_under_test<Sample> in tests/test_fft_parity_ooura.cpp is named in the PR body and in the file, with the target tap::dsp::detail::split_radix_rdft<Sample> and the required surface (ctor-from-size, forward_inplace, inverse_inplace) matching Part 4.
  • tap::dsp::test matches #21's tap::dsp::bench; existing tests use anonymous namespaces only; STYLE.md has no rule. No new third-party dependency. Fixed seeds, no wall clock, no filesystem.
  • Commit: one commit, message accurate, Co-Authored-By and Claude-Session trailers present, PR footer present, all four template sections filled with the verification gaps stated.

Verdict

Not mergeable alongside #17 as-is: rebase onto #17 and resolve finding 1 (bare-metal registration + include(GoogleTest) + cross-compile default for TAP_DSP_PARITY_MAX_N), and record the three hand-offs (3, 4, 5) on #17 or as issues; the rest are nits.


Generated by Claude Code

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Hostile review A (correctness / numerics)

Built at 4ddff35 in a private scratch dir with -DCMAKE_BUILD_TYPE=Release -DTAP_DSP_WERROR=ON on GCC 13.3, Clang 18.1, and GCC -march=native (host has FMA): 215/215 green on all three, zero warnings. Evidence for each finding is inline.

Blocker

1. Merged with #17 (claude/wave1-stage1-legs), the tree does not configure in bare-metal mode, and the new suites never reach the QEMU legs. Scratch merge of both branches (only tests/CMakeLists.txt conflicts; resolved as legs-file + Stage 2a block appended, exactly the intended rebase), then cmake -DTAP_DSP_BARE_METAL=ON:

CMake Error at tests/CMakeLists.txt:115 (gtest_discover_tests):
  Unknown CMake command "gtest_discover_tests".

#17 puts include(GoogleTest) inside its else() (hosted) branch; this block calls gtest_discover_tests for both new executables unconditionally. Even with the include hoisted, discovery would launch qemu-system-arm … --gtest_list_tests at build time (CMAKE_CROSSCOMPILING_EMULATOR is prepended) against a binary with no argv, i.e. run the whole sweep at the default TAP_DSP_PARITY_MAX_N=2^20 (five 8 MB buffers + tables) on an MPS3 model. And #17's positive filter in bare_metal_main.cpp names none of fft_oracle_test, fft_oracle_self_check, fft_rt_test, fft_rt_guard, so on the four QEMU legs the oracle and the RT guard are silently not run, contrary to Part 9 ("size-limited to N ≤ 4096, on the four QEMU legs"). Fix in this block: if(NOT TAP_DSP_BARE_METAL) around include(GoogleTest) + both gtest_discover_tests; default TAP_DSP_PARITY_MAX_N to 4096 when TAP_DSP_BARE_METAL (or CMAKE_CROSSCOMPILING); an add_test(… PASS_REGULAR_EXPRESSION …) one-shot registration for tap_dsp_fft_parity in that mode; and whichever PR lands second adds the four suites to the bare-metal filter. Either PR can carry it, but it must land with the wave.

Should-fix

2. The informational target's numbers reach nobody. CI runs ctest --output-on-failure (ci.yml:32). Locally: ctest -L parity --output-on-failure | grep -c '[ ulp ]' = 0; with -V = 38. All three CI job logs contain no [ ulp ] line. The RecordProperty calls are correct (static call inside a TEST body attaches to that test) but only land in XML under --gtest_output=xml:…, which nothing passes (0 hits in the generated tap_dsp_fft_parity_default_flags[1]_tests.cmake). So "puts the number on the record for each platform" is currently false. Fix: gtest_discover_tests(… EXTRA_ARGS --gtest_output=xml:${CMAKE_BINARY_DIR}/parity-ulp.xml) plus an upload-artifact step, or a dedicated ctest -L parity -V step in ci.yml.

3. The informational target links tap::dsp_fft, which Stage 2c removes (it survives only under TAP_DSP_FFT_CMSIS), so this block breaks at 2c; on the M55 CMSIS leg it would also be linking CMSIS objects into a test that says "Ooura". Add a second private reference library (tap_dsp_fft_reference_default: the same two C files, no flag) and link that. Then neither parity target depends on how the library builds the C, which is the stated intent of the gate.

4. profile<Sample> will not take Q15/Q31 as written, so "nothing else changes" is overstated. k_forward_scale is a static constexpr double, but Part 7's fixed scaling is X/N (a function of N) and BFP carries an exponent; tolerance() reads profile<Sample>::k_epsilon directly instead of dispatching to the profile; and every closed-form input is full-scale 1.0 with expected N or N/2, which Q15 cannot represent (max 1 − 2⁻¹⁵) and whose X/N-scaled expectation of exactly 1.0 saturates. Cheap now: static double forward_scale(std::size_t n), inverse_scale(n), tolerance(n, norm2), and a k_full_scale amplitude in the profile applied to the closed-form vectors.

5. The RT guard observes C++ operator new only; a backend allocating through malloc/calloc or inside vDSP is invisible. The claim is precise for today's Ooura path: makewt/makect write into the preallocated m_w/m_ip (fftsg.c 655–713), and the guard's self-test (CountsAVectorAllocation) proves the counter is live. But on the macOS leg the float engine is vDSP and the file says "proved". State the limit in the header, or on glibc legs additionally interpose malloc (#ifdef __GLIBC__, forward to __libc_malloc). Program-wide replacement is otherwise sound: allocation_count is constant-initialized, atomic, all 8 allocating / 12 deallocating forms replaced consistently (nothrow, aligned, sized), so no mismatched pair; gtest's own allocations only ever move the counter outside the two-read window.

6. Higham derivation: the premise "Ooura's tables are computed from libm … so μ ≤ u" is false. makewt derives half its entries arithmetically (w[2] = 0.5/cos(2δ), w[nw1+2] = 0.5/wk1r, …) and the float instantiation forms delta*j in float, so μ is a small multiple of u. The constant still holds: I reproduced the margins independently (patched copy of the TU printing |err|/(ε·log2N·‖y‖₂)): 0.2500 double, 0.1748 float on GCC 13 -O3, i.e. 16×/23× inside 4, so the 4 is derived-with-slack, not fitted. Two things to state: μ ≈ few·u (the 2u "split-radix margin" is really absorbing this), and that using the 2-norm as a per-element bound is loose by up to √N, which is why the test cannot see single-bin errors at large N (float, N=65536, DC: tol ≈ 0.5 on a value of 65536) and why the DFT comparison is confined to N ≤ 256. Fine for an oracle, wrong as written.

Nits

  1. db_from_ratio is used by no file in this PR (grep over tests/*.cpp): dead code introduced into support/. Drop until a caller exists.
  2. sizes_up_to_65536() misnames itself once TAP_DSP_PARITY_MAX_N < 65536, and MAX_N < 4 makes every sweep test vacuously green. Add static_assert(TAP_DSP_PARITY_MAX_N >= 4 && (TAP_DSP_PARITY_MAX_N & (TAP_DSP_PARITY_MAX_N - 1)) == 0).
  3. "MSVC's default /fp:precise does not contract" is true for VS 2022 17.0+; before 17.0, x64 with /arch:AVX2 did contract under /fp:precise. State the version assumption in the CMake comment.
  4. #pragma GCC diagnostic ignored "-Wmismatched-new-delete" is file-wide from line 69 with no push/pop; wrap only lines 115–191.
  5. TYPED_TEST(fft_rt_test, TransformsAreNoexcept) re-evaluates a constexpr already static_asserted at namespace scope; either say it exists to keep the promise visible in the test list, or drop it.
  6. The "compensated" inverse oracle forms (a[0] ± a[1]) as a plain double add before × 0.5 (line 339): one uncompensated rounding in a double-double reference. two_sum(a[0], ±a[1]) then dd_mul_d(·, 0.5). Immaterial against the tolerance; inconsistent with the header claim.

Checked and found correct (no action)

  • Flags reach both sides. flags.make: reference lib C_FLAGS = -O3 -DNDEBUG -ffp-contract=off; parity TU … -std=gnu++20 -ffp-contract=off …; same under Clang (compile_commands). link.txt for tap_dsp_fft_parity lists only libtap_dsp_fft_reference_nocontract.a + gtest, never tap_dsp_fft.
  • The flag is load-bearing on this host. -march=native build, FMA instruction counts by objdump: library fftsg.c.o 172, fftsg_float.c.o 142; reference fftsg.c.o 0, fftsg_float.c.o 0; parity TU 0; default-flags parity TU 5; oracle TU 137. Parity gate still green at -march=native on both compilers.
  • Comparison semantics. memcmp distinguishes ±0 and equates identical NaN payloads, as intended; the inverse feeds both engines ref_spec, so it is judged independently of the forward. engine_under_test is referenced exactly once (line 241) and basic_real_fft appears nowhere else in the TU: the single re-point holds. TAP_DSP_PARITY_MAX_N gates both the sweep and the 2^20 test (GTEST_SKIP). 2^20 peak memory ≈ 50 MB (five 8 MB buffers + tables), no concern on the Windows runner.
  • Double-double twiddles. Against __float128 (libquadmath cosq/sinq, correctly-rounded π): max abs error 1.3e-34 (n=4), 2.6e-32 (256), 3.4e-32 (65536), 4.2e-32 (2^20); Taylor at π/4 4e-33; k_pi_dd residual 3.1e-33; identical under -mfma -ffp-contract=fast. The "~1e-30, needs ~1e-20" claim holds. std::fma on x86-64 without -mfma is a correctly-rounded libm call (software), so TwoProd is exact either way; cost is irrelevant at N ≤ 256. Octant reduction (rotate → (−sin, cos), negate, swap at 2m > n/4) is correct, exact zeros at multiples of π/2.
  • Sign convention. Oracle im = Σ x[j]·sin(+2πjk/N) and closed form "sine → +N/2 in IMAG" match fftsg.c's readme (I[k] = Σ a[j] sin(2πjk/n)), fft.h's docstring and test_fft.cpp::SignConventionIsPlusI; the inverse formula matches the readme's IRDFT verbatim.
  • signals.h. tone is right at bin 0 (turns = 0 → cos(phase)) and n/2 (turns ∈ {0, ½}); bin·j exact below 2⁵³, fmod exact, division by power-of-two n exact. xorshift32(0) guarded. random_signal in [−amp, amp). db_from_ratio(0) = −inf as documented.
  • Style. No long double, no wall clock, fixed seeds; k_ on namespace-scope constants, bare constexpr locals, braces everywhere; banner form matches test_nn.cpp; [[nodiscard]] is unused repo-wide so nothing to align to. Style workflow green.
  • Copy tests vs CopiesAgreeWithTheirSource. Overlap is exactly what the comment says (float forward at three geometries); the new ones add double, inverse, unwarmed-source and assignment-over-different-geometry.
  • CI runtimes (run 35258269936). Sweep tests 0.02–0.03 s each on all hosts. 2^20 double/float: linux 0.41/0.37 s, windows 0.48/0.42 s, macos 0.52/0.51 s. Informational double/float: linux 0.23/0.21, windows 0.27/0.24, macos 0.30/0.26. Parity label total 1.3–1.7 s per host. Local (slower box): gcc 1.91/1.59, clang 0.96/0.80, -march=native 1.10/0.73.

Verdict: the numerics are sound and the gate is what it claims on every compiler I could run, but do not merge alongside #17 until item 1 is guarded, and item 2 means the informational target currently records nothing anywhere.


Generated by Claude Code

tap added a commit that referenced this pull request Sep 17, 2026
Blocker (integration with the Stage 2a tests, PR #24): tests/CMakeLists.txt
now hoists include(GoogleTest) and provides
  tap_dsp_add_gtest_executable(<target> SOURCES ... [MAIN_FILTER <f>] [LABELS ...])
which in hosted mode links gtest_main and discovers tests, and under
TAP_DSP_BARE_METAL links gtest + bare_metal_main.cpp and registers one
CTest test judged on the completion marker. tap_dsp_tests uses it; the
four toolchains default TAP_DSP_PARITY_MAX_N to 4096 so #24's parity
target fits the legs.

- bare_metal_main.cpp: NEGATIVE filter (TAP_DSP_BARE_METAL_FILTER, default
  "*"), so every suite a later PR compiles in runs on target unless
  excluded by name; the marker carries selected= and skipped=, and a skip
  or an empty selection is rc=1. Hard-coded emulation times removed (they
  matched neither run; TCG wall is noise). Banner on lines 1-2.
- cortexm_startup.c: HardFault/Default handlers print TAP_DSP_TESTS_FAULT
  with the IPSR value over semihosting and _exit(2); the marker is a
  FAIL_REGULAR_EXPRESSION and the test TIMEOUT drops to 600 s. MSPLIM
  guard also accepts __ARM_ARCH_8_1M_MAIN__ (clang's M55). irq_lock /
  irq_restore in house case.
- Provenance: platform/README.md (copies of MuTap @ 142361b <- SampleRateTap,
  canonical here, duplication deliberate pending taphouse), SPDX lines in
  the .ld files, the inherited "Ported from RatioTap" line corrected.
- Toolchains: -Wno-psabi on the C++ flags (the ~26 GCC notes per leg).
- ci.yml: runs-on ubuntu-24.04; apt --no-install-recommends with newlib and
  the C++ runtime named; -DTAP_DSP_WERROR=ON on the embedded configure;
  ctest -V on the emulated step so counts and the marker are in the log;
  size -A alongside the Berkeley totals; checkout@v5; hosted configure
  passes -DTAP_DSP_BUILD_BENCH=ON for the bench PR; a guarded verbose
  `ctest -L parity -R default_flags` step for #24's ulp table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap added a commit that referenced this pull request Sep 17, 2026
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Hand-off from #17: the bare-metal registration helper is on claude/wave1-stage1-legs (09a584e)

Both reviews' blocker (this block does not configure or run on the four QEMU legs next to #17) is resolved on #17's side by a helper in tests/CMakeLists.txt; include(GoogleTest) is hoisted to the top of the file, and the four toolchain files now default TAP_DSP_PARITY_MAX_N to 4096 as a cache entry (your set(... CACHE STRING ...) respects it; -D on the command line still wins). Merge order per Part 12: #17 first, then this PR rebases.

Exact change to make in your block (verified in a scratch merge of 4ddff35 onto 09a584e: host -DTAP_DSP_BARE_METAL=ON -DTAP_DSP_PARITY_MAX_N=4096 configures, builds and runs 3 one-shot tests — tap_dsp_tests_emulated 161 selected / 0 skipped, tap_dsp_fft_parity_emulated 4/0, tap_dsp_fft_parity_default_flags_emulated 2/0; hosted -DTAP_DSP_WERROR=ON 215/215, ctest -N -L parity = 8, the ulp table prints 38 lines under -V):

  1. The three base lines your context anchors on (target_link_libraries(tap_dsp_tests PRIVATE GTest::gtest_main), include(GoogleTest), gtest_discover_tests(tap_dsp_tests ...)) no longer exist on Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17; delete them in the conflict resolution. target_sources(tap_dsp_tests PRIVATE test_fft_oracle.cpp test_fft_rt.cpp) stays as is — with Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's negative filter those suites now run on all four legs by default.

  2. Replace add_executable + GTest::gtest_main + gtest_discover_tests for each parity target with the helper, keeping your other target_* calls:

tap_dsp_add_gtest_executable(tap_dsp_fft_parity
    SOURCES test_fft_parity_ooura.cpp
    LABELS parity
    # The 2^20 tests GTEST_SKIP below TAP_DSP_PARITY_MAX_N, and a skip is a
    # failure on the target (bare_metal_main.cpp reports rc=1 on skipped>0):
    # keep them off the legs by name.
    MAIN_FILTER "*-fft_parity_ooura.LargeTransform*")
target_include_directories(tap_dsp_fft_parity PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_compile_features(tap_dsp_fft_parity PRIVATE cxx_std_20)
target_compile_options(tap_dsp_fft_parity PRIVATE ${_tap_dsp_nocontract_cxx})
target_compile_definitions(tap_dsp_fft_parity PRIVATE TAP_DSP_PARITY_MAX_N=${TAP_DSP_PARITY_MAX_N})
target_link_libraries(tap_dsp_fft_parity PRIVATE
    tap_dsp_fft_reference_nocontract
    tap_dsp_warnings)

tap_dsp_add_gtest_executable(tap_dsp_fft_parity_default_flags
    SOURCES test_fft_parity_ooura.cpp
    LABELS parity)
target_include_directories(tap_dsp_fft_parity_default_flags PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_compile_features(tap_dsp_fft_parity_default_flags PRIVATE cxx_std_20)
target_compile_definitions(tap_dsp_fft_parity_default_flags PRIVATE
    TAP_DSP_PARITY_INFORMATIONAL
    TAP_DSP_PARITY_MAX_N=${TAP_DSP_PARITY_MAX_N})
target_link_libraries(tap_dsp_fft_parity_default_flags PRIVATE
    tap::dsp_fft
    tap_dsp_warnings)

Signature, for the record (documented in the file header on #17):
tap_dsp_add_gtest_executable(<target> SOURCES <src>... [MAIN_FILTER <gtest filter>] [LABELS <label>...]) — hosted: links GTest::gtest_main, gtest_discover_tests(... PROPERTIES TIMEOUT 900 [LABELS ...]); TAP_DSP_BARE_METAL: links GTest::gtest + bare_metal_main.cpp, passes MAIN_FILTER as TAP_DSP_BARE_METAL_FILTER (default "*"), registers one add_test(<target>_emulated) with PASS_REGULAR_EXPRESSION "TAP_DSP_TESTS_COMPLETE rc=0 ", FAIL_REGULAR_EXPRESSION "\[ FAILED \];TAP_DSP_TESTS_FAULT", TIMEOUT 600. Never link gtest_main or call gtest_discover_tests yourself.

Two things for your side that the scratch merge cannot prove (no Arm toolchain here; the legs' CI will):

  • fft_oracle_test's closed-form sweep to N = 65536 now runs in soft-float double on the M4/M33 legs (~4.5 min budget headroom today). If a leg goes over budget, cap closed_form_sizes() by TAP_DSP_PARITY_MAX_N (reviewer B's item 5), which on the legs is 4096.
  • fft_rt_guard replaces global operator new/delete for the whole tap_dsp_tests image on newlib; every other suite on the target then runs through it. Nothing in Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17 objects, just flagging it before it lands in the shared binary.

The hosted CI on #17 also has the ctest -L parity -R default_flags -V step you asked for (guarded on the label existing, shell: bash, --build-config Release), so the per-platform ulp table lands in the logs the day this merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

tap added a commit that referenced this pull request Sep 17, 2026
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
@tap
tap force-pushed the claude/wave1-stage2a-tests branch from e97e0ed to 7b46ce4 Compare September 17, 2026 20:14
@tap
tap changed the base branch from main to claude/wave1-stage1-legs September 17, 2026 20:14

tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Fix pass for reviews A and B — what was applied, what was not

Head is now 7b46ce4, rebased onto claude/wave1-stage1-legs (09a584e), three commits: the original suite, the host-side review fixes (0f6eb93), and the #17 coupling (7b46ce4). PR base changed to claude/wave1-stage1-legs; to be retargeted to main once #17 merges.

Blocker (A1 / B1) — applied

  • Rebased onto Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17. The two parity executables are registered through tap_dsp_add_gtest_executable(... LABELS parity); the block no longer calls include(GoogleTest) or gtest_discover_tests itself, so hosted = discovered, bare metal = one-shot with the marker regexes and no emulator at build time.
  • The 2^20 tests are compiled out (#if TAP_DSP_PARITY_MAX_N >= (1 << 20)), not GTEST_SKIPped, since the one-shot main counts a skip as rc=1.
  • TAP_DSP_PARITY_MAX_N defaults to 4096 under CMAKE_CROSSCOMPILING (plain cache set: the toolchains' own 4096 and any -D win); static_asserted power of two ≥ 4.
  • The oracle's closed-form sweep is capped by the same knob (tap_dsp_tests gets the definition), so no 65536-point double buffers on the MPS2 legs.
  • Verified on the host: -DTAP_DSP_BARE_METAL=ON -DTAP_DSP_PARITY_MAX_N=4096 configures, builds (-Werror), and ctest -V shows three markers, all rc=0 skipped=0: tap_dsp_tests_emulated selected=161 (30 fft_oracle* and 17 fft_rt* [ RUN ] lines under Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's negative filter), tap_dsp_fft_parity_emulated selected=4, tap_dsp_fft_parity_default_flags_emulated selected=1. The real QEMU legs run on this PR's CI now that the head carries Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17.

Should-fix

Nits

  • A7 db_from_ratio — removed until a caller exists (header notes Stage 6).
  • A8 sizes_up_to_65536()sweep_sizes(); static_assert on MAX_N — applied.
  • A9 / B10 MSVC — qualified: /fp:precise has not contracted since VS 2022 17.0 (/fp:contract opt-in); earlier x64 /arch:AVX2 and ARM64 did.
  • A10 pragma — push/pop scoped to the replacement functions.
  • A11 TransformsAreNoexcept — kept, with the reason (a row per profile in the listing) in a comment.
  • A12 inverse edge term — two_sum(a[0], ±a[1]) then exact halving.
  • B6 FMA recipe — the objdump | grep -c one-liner and the measured 200/171 → 0 counts are in the parity file header; "12 checks" was 6 jobs × 2 events, now said so.
  • B7 names — ImpulseHasFlatSpectrumImpulseIsFlatAtEverySize; the header relates this file to test_fft.cpp's closed forms (deliberate overlap per Part 9; the sweep, the inverse forms, the derived tolerance and the DFT are what is new).
  • B8 self-check constants — derived (twiddles: 2ε argument rounding + 1ε libm + 0.5ε dd_round = 3.5ε, constant 4ε; round trip ≈ 1.5ε, constant 8ε) and measured (6.4e-16 = 2.9ε; 1.1e-16 = 0.5ε).
  • B9 Stage 2c hazard — recorded in the parity file header and the CMake block: 2c must move fftsg_float.c to tests/reference/ooura/ with fftsg.c, or the float half of the gate becomes port-vs-port and is deleted.

Verification of this head

GCC 13.3 and Clang 18.1 hosted, -DTAP_DSP_WERROR=ON: 214/214 green, zero warnings; clang-format --dry-run --Werror and clang-tidy clean; hosted -DTAP_DSP_PARITY_MAX_N=4096 lists four gate tests (2^20 absent); host one-shot mode as above. CI on the three hosts plus the four QEMU legs is the gate for the rest.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

CI follow-up on the rebased head — one new finding from the M55 leg

The first real QEMU run of 7b46ce4 was green on cortex-m4-softfp, cortex-m4f, cortex-m33 and the three hosts, and failed on cortex-m55: fft_oracle_test/0.ImpulseIsFlatAtEverySize at N = 4, float returned ~2e-19 for bin 1 and then TAP_DSP_TESTS_FAULT HardFault. The parity targets themselves passed there (selected=4 / selected=1, rc=0).

Cause (a defect in fft.h's CMSIS wrapper, not in the test): on the M55 basic_real_fft<float> routes through CMSIS-DSP, and arm_rfft_fast_init_f32 accepts 32 … 4096 only (third_party/cmsis-dsp/Source/TransformFunctions/arm_rfft_fast_init_f32.c, the switch (fftLen) at the end returns ARM_MATH_ARGUMENT_ERROR otherwise). fft.h ignores that return status, so fft.h's documented "power of two ≥ 4" is undefined behaviour under TAP_DSP_FFT_CMSIS below 32 and above 4096. The existing battery starts at N = 64, which is why nothing showed before. For the fft.h owner: the CMSIS wrapper should reject (or fall back to Ooura for) sizes outside 32…4096, and the header should state the range as a backend limit.

Fix in this PR (26f2393): profile<Sample> now carries the engine's size range (k_min_n/k_max_n: 32…4096 for float under TAP_DSP_FFT_CMSIS, 4…2^20 otherwise) and every oracle sweep is built from it, with the finding recorded in the comment — the honest-limit pattern, not a paper-over. Also fixed: the informational table printed zu for N on newlib-nano (%zu unsupported); now %lu. Hosted GCC/Clang 214/214 -Werror clean; host one-shot mode three markers rc=0, selected=161/4/1 skipped=0. The M55 leg's rerun is the gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

tap added a commit that referenced this pull request Sep 17, 2026
* Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55

Stage 1 (DspTap side) of docs/audit-fft-and-code-smells.md, Part 10: the
embedded legs become real. Until now cmake/arm-cortex-m55-mps3.cmake linked
platform files that only exist in MuTap, and CI built the static library
with tests off.

- platform/: cortexm_startup.c (MuTap's armv8m_startup.c, MSPLIM write
  guarded by __ARM_ARCH_8M_MAIN__ so it also serves the Armv7E-M M4),
  mps2_an505.ld and mps3_an547.ld copied, and a new mps2_an386.ld for
  QEMU's Cortex-M4 board (SSRAM1 4 MB @ 0x0 code, SSRAM2/3 4 MB @
  0x20000000 data, per hw/arm/mps2.c).
- cmake/: arm-cortex-m4-mps2.cmake (TAP_DSP_M4_FPU OFF = soft-float, ON =
  fpv4-sp-d16 hard), arm-cortex-m33-mps2.cmake as MuTap's, the M55 file
  repointed at platform/. All set TAP_DSP_BARE_METAL (renamed from
  MUTAP_BARE_METAL); M4/M33 pin TAP_DSP_FFT_CMSIS OFF (no MVE).
- tests/: one-shot bare-metal mode (gtest without pthreads/POSIX RE/stream
  redirection/filesystem, bare_metal_main.cpp with a positive filter of the
  emulation-sized selection, judged on "TAP_DSP_TESTS_COMPLETE rc=0").
- ci.yml: the compile-only M55 job becomes four QEMU legs, cortex-m4-softfp,
  cortex-m4f, cortex-m33, cortex-m55 (CMSIS Helium backend ON there, so the
  FFT backend parity suite runs on a Helium core for the first time), each
  ending with arm-none-eabi-size on the test binary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

* Record the measured emulation times and .text sizes of the four legs

Comment-only: the budget note in tests/bare_metal_main.cpp and the job
comment in ci.yml carry the numbers from the first green run
(cortex-m4-softfp 88 s, cortex-m4f 62 s, cortex-m33 97 s, cortex-m55 14 s;
.text 912868 / 907468 / 906316 / 965636 bytes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

* Address the hostile reviews on the embedded legs (PR #17)

Blocker (integration with the Stage 2a tests, PR #24): tests/CMakeLists.txt
now hoists include(GoogleTest) and provides
  tap_dsp_add_gtest_executable(<target> SOURCES ... [MAIN_FILTER <f>] [LABELS ...])
which in hosted mode links gtest_main and discovers tests, and under
TAP_DSP_BARE_METAL links gtest + bare_metal_main.cpp and registers one
CTest test judged on the completion marker. tap_dsp_tests uses it; the
four toolchains default TAP_DSP_PARITY_MAX_N to 4096 so #24's parity
target fits the legs.

- bare_metal_main.cpp: NEGATIVE filter (TAP_DSP_BARE_METAL_FILTER, default
  "*"), so every suite a later PR compiles in runs on target unless
  excluded by name; the marker carries selected= and skipped=, and a skip
  or an empty selection is rc=1. Hard-coded emulation times removed (they
  matched neither run; TCG wall is noise). Banner on lines 1-2.
- cortexm_startup.c: HardFault/Default handlers print TAP_DSP_TESTS_FAULT
  with the IPSR value over semihosting and _exit(2); the marker is a
  FAIL_REGULAR_EXPRESSION and the test TIMEOUT drops to 600 s. MSPLIM
  guard also accepts __ARM_ARCH_8_1M_MAIN__ (clang's M55). irq_lock /
  irq_restore in house case.
- Provenance: platform/README.md (copies of MuTap @ 142361b <- SampleRateTap,
  canonical here, duplication deliberate pending taphouse), SPDX lines in
  the .ld files, the inherited "Ported from RatioTap" line corrected.
- Toolchains: -Wno-psabi on the C++ flags (the ~26 GCC notes per leg).
- ci.yml: runs-on ubuntu-24.04; apt --no-install-recommends with newlib and
  the C++ runtime named; -DTAP_DSP_WERROR=ON on the embedded configure;
  ctest -V on the emulated step so counts and the marker are in the log;
  size -A alongside the Berkeley totals; checkout@v5; hosted configure
  passes -DTAP_DSP_BUILD_BENCH=ON for the bench PR; a guarded verbose
  `ctest -L parity -R default_flags` step for #24's ulp table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
tap and others added 4 commits September 17, 2026 22:00
…e, RT guard

Written first, against the vendored C, so the wave-2 port works test-first
(docs/audit-fft-and-code-smells.md, Part 3 Stage 2a and Part 9).

- tests/test_fft_parity_ooura.cpp + target tap_dsp_fft_parity: memcmp bit
  identity of basic_real_fft<double|float> against raw rdft/rdft_f from a
  private reference build of fftsg.c/fftsg_float.c, both sides compiled with
  -ffp-contract=off (MSVC: nothing), forward and inverse, N = 4..65536 plus
  2^20, on five materials. ooura_ref and engine_under_test are each
  re-pointable in one place. TAP_DSP_PARITY_MAX_N (default 2^20) caps the
  sizes for emulated targets. A second target,
  tap_dsp_fft_parity_default_flags, prints the measured max-ulp deviation
  per N at default flags and never fails (Part 6, N3).
- tests/test_fft_oracle.cpp: closed-form vectors (impulse, DC, Nyquist,
  on-bin cosine/sine with the +i convention, two-tone) and their
  unnormalized inverses, to a tolerance derived from Higham's FFT bound
  (4 * eps * log2 N * ||y||_2, measured 0.25/0.17 of it for double/float);
  plus a double-double (TwoSum/TwoProd) compensated DFT for N <= 256, with
  a profile<Sample> extension point for the fixed-point stage.
- tests/test_fft_rt.cpp: static_assert(noexcept) on the four transforms and
  size queries; a global operator new/delete counting guard proving the
  four calls allocate nothing at N = 512 and 4096, on the first call after
  construction and in steady state; copy and copy-assignment bit-identical.
- tests/support/signals.h: one xorshift32, random_signal, tone (exact
  modular phase reduction) and db_from_ratio for the new files.
- tests/CMakeLists.txt: additive, delimited block at the end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Rebased onto claude/wave1-stage1-legs. The two parity executables now go
through tap_dsp_add_gtest_executable, so they are discovered hosted and
one-shot under QEMU like tap_dsp_tests; the block no longer calls
include(GoogleTest) or gtest_discover_tests itself. Consequences:

- The 2^20 tests are compiled out (not GTEST_SKIP'ped) when
  TAP_DSP_PARITY_MAX_N is below 2^20, because the one-shot main counts a
  skip as a failed gate; the knob defaults to 4096 when CMAKE_CROSSCOMPILING
  and the toolchains' own cache value is honoured.
- The oracle's closed-form sweep is capped by the same knob (tap_dsp_tests
  gets the definition), so no 65536-point buffers on the MPS2 legs.
- The JUnit XML for the informational report is attached as a GTEST_OUTPUT
  ENVIRONMENT property via a ctest-time TEST_INCLUDE_FILES script, since the
  helper has no EXTRA_ARGS/PROPERTIES pass-through; hosted only.

Verified on the host: GCC 13 and Clang 18 hosted (214/214, -Werror clean),
-DTAP_DSP_BARE_METAL=ON host run (three one-shot markers, rc=0,
selected=161/4/1, skipped=0, oracle and RT suites running under the
negative filter), -DTAP_DSP_PARITY_MAX_N=4096 hosted (2^20 tests absent).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…e M55

The first QEMU run of the rebased suite HardFaulted in
fft_oracle_test/0.ImpulseIsFlatAtEverySize at N = 4 on the cortex-m55 leg,
the one leg where basic_real_fft<float> routes through CMSIS-DSP:
arm_rfft_fast_init_f32 accepts only 32..4096 and fft.h's wrapper does not
check its return status, so fft.h's ">= 4" contract is undefined behaviour
there below 32 (the existing battery starts at 64, which is why it never
showed). The oracle's profile<Sample> now carries the engine's size range
(k_min_n / k_max_n; 32..4096 for float under TAP_DSP_FFT_CMSIS, 4..2^20
otherwise) and every sweep is built from it. The wrapper defect itself is
recorded for the fft.h owner, not papered over here.

Also: the informational table printed "zu" for N on newlib-nano; %lu now.

Hosted GCC 13 / Clang 18: 214/214, -Werror clean; host one-shot mode:
three markers rc=0, selected=161/4/1, skipped=0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
@tap
tap force-pushed the claude/wave1-stage2a-tests branch from 26f2393 to b303e70 Compare September 17, 2026 22:01
@tap
tap changed the base branch from claude/wave1-stage1-legs to main September 17, 2026 22:01
@tap
tap merged commit 5835df1 into main Sep 17, 2026
9 checks passed
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