Scrap your static_assert: report compile-time checks instead of breaking the build - #89
Open
steve-downey wants to merge 6 commits into
Open
Scrap your static_assert: report compile-time checks instead of breaking the build#89steve-downey wants to merge 6 commits into
steve-downey wants to merge 6 commits into
Conversation
Two header-only components that let a compile-time contract be reported by the test run instead of breaking the build. constant_eval(probe) is consteval, so calling it is an immediate invocation: the probe is evaluated during translation and its result is required to be a constant expression. That answers "is this constant-evaluable?" by construction, with no static_assert, while handing the result back as an ordinary prvalue that CHECK can compare and report. The probe is a plain lambda, so the same body can also be run at runtime -- constant evaluation and ordinary evaluation take different paths through a union-based type. type_name<T>() returns the identity of T, compared with std::is_same_v, so a check is exactly as strict as the static_assert it replaces. The compiler's spelling is consulted only to explain a failure, turning "false" into "const int& == int&". It has two implementations of that spelling: P2996 reflection via std::meta::display_string_of when the compiler offers it, and otherwise recovery from std::source_location::function_name() by calibrating on the signatures of signature<bool> and signature<char>, which differ only where the template argument is named. Nothing about any compiler's format is hard-coded, and self-checks hold whichever implementation is selected to the same contract. Note that __cpp_lib_reflection is a library macro: <version> must be included before testing it, or the reflection path silently never activates.
Selects the P2996 reflection implementation of testing/type_name.hpp, which is otherwise dormant. This belongs in the per-version toolchain rather than cmake/gcc-flags.cmake, which is shared with gcc-12 through gcc-15: -freflection is a gcc-16 feature, and gcc rejects it outright below -std=c++26 rather than warning. gcc-flags.cmake already pins -std=gnu++26, so the requirement is met. It deliberately does not go in CMakePresets.json. The preset toolchains under infra/cmake set CMAKE_CXX_COMPILER to the unversioned g++, so a preset carrying this flag would break the build for anyone whose default compiler is not gcc-16 -- which is nearly everyone today. That leaves the reflection path without CI coverage; see bemanproject#88.
Establishes the convention the rest of the suite follows.
A static_assert is a poor test: a wrong answer is a translation failure, so
the only thing anyone sees is a compiler diagnostic. The test run reports
nothing, the xUnit output is empty, and because the build stops at the first
failing assertion no other test in the file is exercised at all.
Type-level properties become ordinary CHECKs, and type identities become
type_name comparisons, so a mismatch prints "const int& == int&" rather than
"false". The four constexpr cases build their expected inside a self-contained
probe, run it through constant_eval, and compare the resulting state through a
streamable aggregate -- a wrong value now reports "{ error 7 } == { error 3 }"
while the other 274 assertions in the file still run.
Compile-time enforcement is not lost. Whether something can be constant-
evaluated is still answered by the compiler, because constant_eval's call is an
immediate invocation; only the value comparison moved to the test run.
Also fixes the include block to use canonical <beman/expected/...> spellings.
Applies the convention established in expected.test.cpp to the other 17 files, removing the last 283 static_assert declarations from the .test.cpp sources. No live static_assert remains in any test file; the word survives only in comments explaining why a check is now a CHECK. These files are almost entirely type-level, so the bulk of the work is traits and SFINAE concepts becoming CHECK / CHECK_FALSE and is_same_v becoming a type_name comparison. Polarity and operands are preserved throughout; the count of passing tests rises from 1095 to 1172 because assertions that used to be invisible are now reported cases. Two files needed more than the mechanical treatment: expected_std_equivalence.test.cpp generates its assertions from BEMAN_PARITY and BEMAN_RUN macros over a battery of error types. The macros now expand to INFO + CHECK inside a TEST_CASE, so a trait divergence names the exact (T, E) pair it was found on instead of failing to compile. This does mean the trait equivalence target is no longer a translation-time gate. It also means the four pre-existing is_trivially_copyable divergences on clang with libstdc++ -- which are four hard static assertion failures at HEAD -- now report as named test failures rather than breaking the build. expected_ref_constraints.test.cpp brace-scopes each INFO/CHECK pair, because Catch2 INFO messages accumulate for the enclosing scope and a failing check would otherwise print the messages of every sibling that ran before it.
Declare the constexpr construction/equality probes in expected.test.cpp as constexpr objects so they still exercise "usable as a constexpr variable", matching unexpected.test.cpp. Fix a stale comment in expected_review_corrections.test.cpp that described a runtime-checked assignment trait as "rejected at compile time". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nery Add docs/blog/scrap-your-static_assert.org, an org-mode post covering the two techniques the test suite uses to report compile-time facts through the runtime framework: type_name<T>() for type-identity checks and constant_eval for constexpr probes. Code snippets are pulled from the tree by org-transclusion against UUID-anchored regions, so the post cannot drift from the code. Add the anchor comment pairs the post transcludes to type_name.hpp, constant_eval.hpp, and expected.test.cpp. Copy the conversion machinery from the compile-time-scheme repo: .emacs.d/ (init.el plus the orgit-file-transclusion.el adapter, base URLs repointed at this repo) and the blog-md Makefile targets that export docs/blog/*.org to GFM markdown with transclusions resolved. Pinned orgit-file: links resolve against an annotated tag; docs/blog/pins.md records the post-to-tag mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes every live
static_assertfromtests/beman/expected/*.test.cpp, replacing each with a check the test run reports.Why
A
static_assertis a poor test. A wrong answer is a translation failure, so the only thing anyone sees is a compiler diagnostic — the test run reports nothing, the xUnit output is empty, and because the build stops at the first failing assertion, no other test in the file is exercised. A trait that flips from under you takes the whole file with it and tells you about exactly one problem.What replaces it
Two header-only components under
tests/beman/expected/testing/:constant_eval(probe)isconsteval, so calling it is an immediate invocation: the probe is evaluated during translation and its result must be a constant expression. Whether something can be constant-evaluated is therefore still answered by the compiler, with nostatic_assertneeded — only the value comparison moves to the test run:A wrong value now reports
{ error 7 } == { error 3 }and the rest of the file still runs. Because the probe is a plain lambda rather than aconstevalone, the same body covers both evaluation modes — which matters for a union-based type.type_name<T>()returns the identity ofT, compared withstd::is_same_v. The check is exactly as strict as thestatic_assertit replaces; the compiler's spelling is consulted only to explain a failure:It has two implementations of that spelling, selected automatically and holding to the same contract: P2996 reflection via
std::meta::display_string_of(^^T)where available, and otherwise recovery fromstd::source_location::function_name()by calibrating on the signatures ofsignature<bool>andsignature<char>, which differ only where the template argument is named. Nothing about any compiler's format is hard-coded. Both produce byte-identical spellings for every type the suite uses.Result
static_assertdeclarations removed across 17 files, plus 32 in the exemplar. None remain live; the word survives only in comments.ctestgoes from 1095/1095 to 1172/1172 — assertions that were previously invisible are now reported cases.type_nameimplementations, and for the behavioral files againststd::expectedas well asbeman::expected.Two things worth reviewing closely
expected_std_equivalence.test.cppis no longer a translation-time gate. Its assertions are generated byBEMAN_PARITY/BEMAN_RUNover a battery of error types; the macros now expand toINFO+CHECKinside aTEST_CASE, so a divergence names the exact(T, E)pair. A consequence: the four pre-existingis_trivially_copyabledivergences on clang with libstdc++ — which are four hardstatic assertion failederrors at HEAD — now surface as named test failures instead of breaking the build. Pre-existing, and now diagnosable, but it is a real change in failure mode for that target.-freflectionis enabled incmake/gcc-16-toolchain.cmakeonly. It cannot go inCMakePresets.json, because the preset toolchains use the unversionedg++and gcc rejects the flag outright below-std=c++26. That leaves the reflection path without CI coverage — tracked in #88.