Repository navigation
A3: one error model — validate() prechecks, rejections through tap::dsp::raise, every header under -fno-exceptions - #81
Merged
Conversation
…sp::raise, every header under -fno-exceptions Every engine's constructor validated by throwing, so no header compiled under -fno-exceptions, the default of the Pico SDK, Zephyr and most Cortex-M firmware trees, and the Pico firmwares forced exceptions on. This is the family's error model (PLAN.md D17): construction is where an engine rejects, processing stays noexcept, and each engine states the rejection twice in the same words. validate(...) is a noexcept precheck returning the reason as a const char* (nullptr when the converter constructs): async::config::validate(cfg), which now also states what the bank's designer rejected and the FIFO's high-water relation, so a validated config constructs without a throw other than bad_alloc; bridge::basic_converter<S, D, K>::validate(channels, profile) over validate_profile<D, K>; rational::basic_stage<S, R>::validate(channels, profile) — the existing validate(profile) now returns the reason instead of throwing — and basic_chain<...>::validate, the stages' at their design divisors, first reason wins. The constructors raise that reason through tap::dsp::raise (std::invalid_argument; std::runtime_error for a rational spec no length meets, k_unmeetable_spec_reason): a throw where the build has exceptions, std::terminate() where it does not, so such a build calls validate() first. The DspTap pin moves to the tree that carries raise (tap/DspTap#59, merged as 6eaca2d on its main, the identical tree). The three C ABIs validate before constructing and return NULL, and carry their catch (a refused allocation) only where TAP_DSP_HAS_EXCEPTIONS; the Pico firmwares build with the SDK's -fno-exceptions default (-DPICO_CXX_ENABLE_EXCEPTIONS=0: validate, then construct; a refused allocation is the hard fault every allocation's is there) and still with exceptions on, where the 12-channel case keeps its SKIP. Gates: <engine>.Family.HeadersCompileWithoutExceptions (every public header in its own TU under -fno-exceptions, hosted GCC and Clang), the no-exceptions CI job (clang, every engine and the three C ABIs, warnings fatal) and the Pico job's second build of each firmware. Tests: in every engine, validate() agrees with the constructor on every rejection — the same text the exception carries, nullptr exactly where it constructs — and the existing throw tests also pin the texts. Ratchet, measured on M33, M55 and Hexagon against the A1 baselines: bridge and rational exact on every target; async moves by one construction-side constant per target on every row (M55 +37, Hexagon +32 ... +34, M33 +182: the precheck's cost at construction), inside tolerance, not re-recorded. The first tree moved rational's M55 streaming rows +11 ... +37 % and its M33 Q15 rows +16 ... +51 % with no change to the hot path: GCC kept raise out of line and stopped inlining the stage's constructor into main, which lost the channel constant the streaming loops were specialized on; bisected to that one call, and exact again with raise forced inline, which DspTap now does. rational/PLAN.md section 6 records the finding and the lever it points at (a compile-time channel count or 1/2-channel fast paths); a consumer constructing behind a boundary GCC does not inline through runs the un-specialized loops today. Also: the root README's "Errors" paragraph (the shared vocabulary), each engine README's construction sentence, async's PERFORMANCE.md and PLAN.md Hexagon-exceptions entries (deployments validate first; the unwinder stays A2's). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
A3 took test_matrix.cpp's 182 chain instantiations past COFF's 2^16 sections on the Windows MSVC leg (C1128). /bigobj applies to that one source and to MSVC only; every other compiler sees an empty option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
…throws Each engine's ValidateAgreesWith* test catches every constructor rejection to compare its text with validate()'s, and qemu-hexagon's static-musl toolchain cannot unwind a throw (async/docs/PERFORMANCE.md, Known debt): rational's two aborted on the leg. They join the EXPECT_THROW tests in all three engines' Hexagon exclusions; the M33/M55 and host legs run them. The book's Hexagon command gains the same name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
…bare metal Stage.ValidateAgreesWithTheConstructor's last case searched every length on the design grid twice (validate, then the constructor) for a profile no length meets; on the Cortex-M33's soft-float double that outlasted the 20-minute job. The case moves to Stage.ValidateAgreesWithTheConstructorOnAnUnmeetableSpec, which the bare-metal filter excludes by name beside Design.UnmeetableSpecThrows (the same search, excluded for the same reason); the host legs run it, and the Hexagon legs exclude every ValidateAgrees* already. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
tap
pushed a commit
that referenced
this pull request
Oct 12, 2026
Q1: the bench is USB asynchronous mode, with the Pico 2 as the asynchronous-mode device on its own crystal (its USB audio firmware joins A11), and it runs twice, so the timestamp servo is a 1.0 item (B3, C1, the verdict rule). Q2: no outside consumer links a C ABI; MAJOR is the ABI; FetchContent and add_subdirectory are the only supported consumption at 1.0 (C3). Q3: as landed in A1 (#80). Q4: DspTap tagged independently, the minimum asserted (A9). Q5: probably our own Pico 2 firmware, by the maintainer's recollection, unconfirmed; A3 landed regardless (#81). Q6: the host ratchet is a hard gate at ±3 % (A8). Each question stays as asked, with its decision beneath it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
tap
pushed a commit
that referenced
this pull request
Oct 12, 2026
Q1: the bench is USB asynchronous mode, with the Pico 2 as the asynchronous-mode device on its own crystal (its USB audio firmware joins A11), and it runs twice, so the timestamp servo is a 1.0 item (B3, C1, the verdict rule). Q2: no outside consumer links a C ABI; MAJOR is the ABI; FetchContent and add_subdirectory are the only supported consumption at 1.0 (C3). Q3: as landed in A1 (#80). Q4: DspTap tagged independently, the minimum asserted (A9). Q5: probably our own Pico 2 firmware, by the maintainer's recollection, unconfirmed; A3 landed regardless (#81). Q6: the host ratchet is a hard gate at ±3 % (A8). Each question stays as asked, with its decision beneath it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Roadmap A3 (
docs/ROADMAP.md, row P4). Each engine now has anoexceptvalidate(...)that returns the rejection reason as aconst char*, ornullptrwhen the converter will construct. The constructors reject with that same text throughtap::dsp::raise(tap/DspTap#59): it throws where the build has exceptions and callsstd::terminate()where it does not. Every public header now compiles under-fno-exceptions. The family's error model is written down asPLAN.mdD17.config::validate(cfg). It also covers what the bank's designer rejects and the FIFO's high-water relation, so a config that validates constructs with no throw other thanbad_alloc.polyphase_filter.hrejects throughraise.basic_converter<S, D, K>::validate(channels, profile), built onvalidate_profile<D, K>.basic_stage<S, R>::validate(channels, profile)andbasic_chain<...>::validate(channels, profile)(the stages are checked at their design divisors; the first reason wins).design.h'svalidate(profile)now returns the reason instead of throwing. A spec that no filter length meets still raisesstd::runtime_error(k_unmeetable_spec_reason).catch(for a refused allocation) only underTAP_DSP_HAS_EXCEPTIONS.pico2_cyccnt,pico2_dualcore): they call validate first, then construct. They build with the SDK's default-fno-exceptions(-DPICO_CXX_ENABLE_EXCEPTIONS=0) and still build with exceptions on.<engine>.Family.HeadersCompileWithoutExceptions: every public header compiled in its own translation unit under-fno-exceptions, on hosted GCC and Clang.no-exceptionsCI job: clang, every engine and the three C ABIs, warnings fatal.Why
Every engine's constructor rejected a bad configuration by throwing, so no header compiled under
-fno-exceptions. That flag is the default of the Pico SDK, Zephyr and most Cortex-M firmware trees, and our own Pico firmwares had to force exceptions on. Firmware needs a way to reject a configuration without exceptions, and hosts should keep the exception they rely on today.Verification
All of this was run locally on this branch's head, which is main 442a09b (A1 merged) plus this PR's commits:
HeadersCompileWithoutExceptionsentries.no-exceptionsjob's configure, run by hand (clang,-fno-exceptions, the three C ABIs on, warnings fatal): builds clean.scripts/check_retired_words.py: clean.validate()agrees with the constructor on every rejection. It returns the same text the exception carries, andnullptrexactly where construction succeeds (asyncValidateAgreesWithTheConstructorOnEveryRejectioncovers 16 rejections; bridge hasConverter.ValidateAgreesWithTheConstructor; rational hasStageandChainversions). The existing throw tests now also pin the texts.scripts/icount.py, gcc-arm-none-eabi 13.2.1 and qemu-system-arm 8.2.2 for M33/M55, CodeLinaro clang 19.1.5 and qemu-hexagon 8.2.2 for Hexagon):Stage.ValidateAgreesWithTheConstructorat 57 s.Notes for the reviewer
tap::sr::rational::validate(profile)used to returnvoidand throw; it now returns the reason and never throws. A caller that relied on it throwing must now test its result. The constructors still throw the same types with the same texts on hosts. Under-fno-exceptionsa rejected construction terminates, so firmware callsvalidate()first.submodules/dsptapnow points at 6eaca2d, expects.h: tap::dsp::raise, the construction-time rejection without exceptions DspTap#59 as merged on DspTap's main, so the pin is already reachable after branch cleanup.rational/PLAN.md§6). The first version of DspTap'sraisewas an out-of-line[[noreturn]]template. That was enough for GCC to stop inliningrational's stage constructor, which then stopped feeding the channel count into the streaming loops as a constant. The cost was +11 to +37 % on the M55 rows and +16 to +51 % on the M33 Q15 rows. Forcingraiseinline (DspTap'sTAP_DSP_RAISE_INLINE) brings every row back to exact. So this engine's ratchet is sensitive to inlining decisions at construction, and future constructor changes should keep that in mind.rational/tests/test_matrix.cppbuilds with/bigobjunder MSVC only. A3's new functions took its 182 instantiations past COFF's section limit (C1128).ValidateAgrees*test, alongside the other throwing tests, because qemu-hexagon's static-musl toolchain cannot unwind a throw.book/src/part4/hexagon.md's ctest command has the same change.Stage.ValidateAgreesWithTheConstructorOnAnUnmeetableSpec, and the bare-metal legs skip it besideDesign.UnmeetableSpecThrows. It searches the design grid for every filter length twice, and on the M33's soft-float double that outlasted the 20-minute job.async/docs/PERFORMANCE.mdandasync/PLAN.md: A3 makes it unreachable in an embedded deployment, and A2's time-boxed unwinder attempt is still open.🤖 Generated with Claude Code
https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf