Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard - #24
Conversation
tap
left a comment
There was a problem hiding this comment.
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 intests/CMakeLists.txt); rootCMakeLists.txtand headers untouched. - The second compile of the reference C is private to tests, no
tap_dsp_warningson third-party C (matches root policy), no PIC needed; #21's bench measures throughtap::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>intests/test_fft_parity_ooura.cppis named in the PR body and in the file, with the targettap::dsp::detail::split_radix_rdft<Sample>and the required surface (ctor-from-size,forward_inplace,inverse_inplace) matching Part 4. tap::dsp::testmatches #21'stap::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-ByandClaude-Sessiontrailers 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
left a comment
There was a problem hiding this comment.
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
db_from_ratiois used by no file in this PR (grep overtests/*.cpp): dead code introduced intosupport/. Drop until a caller exists.sizes_up_to_65536()misnames itself onceTAP_DSP_PARITY_MAX_N < 65536, andMAX_N < 4makes every sweep test vacuously green. Addstatic_assert(TAP_DSP_PARITY_MAX_N >= 4 && (TAP_DSP_PARITY_MAX_N & (TAP_DSP_PARITY_MAX_N - 1)) == 0).- "MSVC's default /fp:precise does not contract" is true for VS 2022 17.0+; before 17.0, x64 with
/arch:AVX2did contract under/fp:precise. State the version assumption in the CMake comment. #pragma GCC diagnostic ignored "-Wmismatched-new-delete"is file-wide from line 69 with no push/pop; wrap only lines 115–191.TYPED_TEST(fft_rt_test, TransformsAreNoexcept)re-evaluates a constexpr alreadystatic_asserted at namespace scope; either say it exists to keep the promise visible in the test list, or drop it.- 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])thendd_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 libC_FLAGS = -O3 -DNDEBUG -ffp-contract=off; parity TU… -std=gnu++20 -ffp-contract=off …; same under Clang (compile_commands).link.txtfortap_dsp_fft_paritylists onlylibtap_dsp_fft_reference_nocontract.a+ gtest, nevertap_dsp_fft. - The flag is load-bearing on this host.
-march=nativebuild, FMA instruction counts byobjdump: libraryfftsg.c.o172,fftsg_float.c.o142; referencefftsg.c.o0,fftsg_float.c.o0; parity TU 0; default-flags parity TU 5; oracle TU 137. Parity gate still green at-march=nativeon both compilers. - Comparison semantics.
memcmpdistinguishes ±0 and equates identical NaN payloads, as intended; the inverse feeds both enginesref_spec, so it is judged independently of the forward.engine_under_testis referenced exactly once (line 241) andbasic_real_fftappears nowhere else in the TU: the single re-point holds.TAP_DSP_PARITY_MAX_Ngates 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(libquadmathcosq/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_ddresidual 3.1e-33; identical under-mfma -ffp-contract=fast. The "~1e-30, needs ~1e-20" claim holds.std::fmaon x86-64 without-mfmais 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 andtest_fft.cpp::SignConventionIsPlusI; the inverse formula matches the readme's IRDFT verbatim. - signals.h.
toneis right at bin 0 (turns = 0 → cos(phase)) and n/2 (turns ∈ {0, ½});bin·jexact below 2⁵³,fmodexact, division by power-of-two n exact.xorshift32(0)guarded.random_signalin [−amp, amp).db_from_ratio(0)= −inf as documented. - Style. No
long double, no wall clock, fixed seeds;k_on namespace-scope constants, bareconstexprlocals, braces everywhere; banner form matchestest_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=native1.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
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
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
Hand-off from #17: the bare-metal registration helper is on
|
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
e97e0ed to
7b46ce4
Compare
Fix pass for reviews A and B — what was applied, what was notHead is now Blocker (A1 / B1) — applied
Should-fix
Nits
Verification of this headGCC 13.3 and Clang 18.1 hosted, 🤖 Generated with Claude Code https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy Generated by Claude Code |
CI follow-up on the rebased head — one new finding from the M55 legThe first real QEMU run of Cause (a defect in Fix in this PR ( 🤖 Generated with Claude Code https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy Generated by Claude Code |
* 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>
…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
26f2393 to
b303e70
Compare
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 momentengine_under_testis re-pointed at the ported engine. Based onclaude/wave1-stage1-legs(#17) because the two PRs sharetests/CMakeLists.txtand the parity targets register through #17's bare-metal helper; retarget tomainafter #17 merges.tests/test_fft_parity_ooura.cpp+ its own targettap_dsp_fft_parity:memcmpbit identity ofbasic_real_fft<double|float>against rawrdft/rdft_ffrom a private reference build offftsg.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:precisehas 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>andengine_under_test<Sample>are each re-pointable in one place.TAP_DSP_PARITY_MAX_N(cache var; default 1048576 hosted, 4096 whenCMAKE_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 (aGTEST_SKIPis 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 (nottap::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, andGTEST_OUTPUT=xml(attached as a ctestENVIRONMENTproperty through aTEST_INCLUDE_FILESscript, since the helper has noEXTRA_ARGSpass-through) writes theRecordPropertyvalues to<build>/parity-ulp.xml. Never fails (Part 6, N3). Where the number is visible: not under plainctest --output-on-failure(passing stdout is hidden);ctest -L parity -R default_flags -Vshows 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(intap_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 viastd::fma, notlong 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 withtest_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(intap_dsp_tests):static_assert(noexcept(...))onforward_inplace,inverse_inplace, same-typeforward/inverse,size,num_binsfor 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: onexorshift32,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: twotarget_sources/target_compile_definitionslines fortap_dsp_tests, the two reference libraries, the two parity targets viatap_dsp_add_gtest_executable, the XML environment script. RootCMakeLists.txt,fft.hand 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 --Werrorandclang-tidy(repo.clang-tidy) clean on the new files.-DTAP_DSP_BARE_METAL=ON -DTAP_DSP_PARITY_MAX_N=4096configures and builds;ctest -Vshows three markers, allTAP_DSP_TESTS_COMPLETE rc=0 … skipped=0:tap_dsp_tests_emulated selected=161(30fft_oracle*and 17fft_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.ctest -L parity -R default_flags -Vprints the 38 table lines, andparity-ulp.xmlcarries 64<property>rows (16 sizes × fwd/inv × 2 precisions).-ffp-contract=offconfirmed inflags.makefor both the reference lib and the parity TU;objdump -dshows 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.-DTAP_DSP_PARITY_MAX_N=4096: the gate binary lists four tests (the 2^20 pair compiled out), all green.|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 ε).cos(w * j)tone generator accumulates ~1e-11 phase error at N = 65536 and fails the double closed-form tests by itself;tone()reducesbin*j mod nexactly 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 noTAP_DSP_FFT_ACCELERATEreaches them and they stay Ooura-vs-Ooura on that leg; the RT guard intap_dsp_testsdoes run against vDSP there), Windows/MSVC (no-ffp-contractflag passed;/W4 /WXon 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 onmain: all three hosted legs plus the M55 compile, drift and clang-tidy green (6 jobs × 2 events).Notes for the reviewer
claude/wave1-stage1-legs(09a584e); the two parity executables register throughtap_dsp_add_gtest_executable(... LABELS parity)(hosted: gtest_main + discovery; bare metal: one-shot main + oneadd_testjudged on the marker);TAP_DSP_PARITY_MAX_Ndefaults to 4096 when cross-compiling and the 2^20 tests are compiled out below it; the oracle sweep is capped by the same knob; Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17's negative filter runs the oracle and RT suites on the four QEMU legs by default. Base isclaude/wave1-stage1-legs; retarget tomainafter Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17 merges.TAP_DSP_PARITY_MAX_N(cache var, cross-compile default 4096, also set by the toolchain files) and the ctest labelparity; the informational number relies on Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17'sctest -L parity -R default_flags -Vstep; the RT guard replaces the globaloperator new/deleteprogram-wide intap_dsp_tests, which every suite in that binary runs through (counting only, no behavioural change). Suggestion for the helper (Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 #17 owner): anEXTRA_ARGS/PROPERTIESpass-through would let the XML environment script collapse to one line.fftsg_float.cas well asfftsg.c; 2c must move it totests/reference/ooura/alongside, or the float half of the gate becomes port-vs-port and has to be deleted. Recorded in the parity file header and the CMake block.engine_under_testintest_fft_parity_ooura.cppis the single alias to change todetail::split_radix_rdft<Sample>at 2a and back tobasic_real_fftat 2b.test_fft_rt.cpp:-Wmismatched-new-deleteis a known false positive (GCC 101480) for replaced allocation functions that forward to malloc/free; push/pop-scoped to the replacement functions, with the reason in the comment.🤖 Generated with Claude Code
https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy