Conversation
Three things, and the middle one is a bug the other two found.
Ninety of the calls in zu.hpp answer something a caller must not drop
and only three said so. A try_ call answers a std::expected whose error
half IS the failure report, so `conn.try_query(q);` on a line of its own
compiles, throws nothing, returns nothing, and loses the reason. Every
expected return and every factory is [[nodiscard]] now. It caught a real
one in test_conditions.cpp on the first build, which is the point.
Then the bug. zu.hpp read __cpp_lib_expected before anything had defined
it, because <version> was not included and none of the twenty headers
above the check happens to pull that macro in on libstdc++. An undefined
macro is nought in a preprocessor arithmetic expression, so
ZU_HAS_EXPECTED came out 0 on GCC 13 at -std=c++23, where std::expected
has been available since GCC 12. The whole try_ half of the API went
undeclared on a toolchain that has it. Worse than absent: it depended on
include order, so a translation unit that had already included <version>
or <expected> got a different zu.hpp than one that had not, in the same
build.
Nothing said so, because test_expected.cpp is written as one #if and
compiles to a single placeholder case when the flag is off. ctest ran
that placeholder, printed Passed, and eleven cases about the half a
caller who builds without exceptions depends on had not been compiled by
anything. So the floor branch of that file now carries an #error: below
C++23 there is nothing to run and the throwing half is complete on its
own, but at C++23 and above, this half missing is a broken build rather
than a quiet one.
Third, printing. <format> was included at the top of the header and
ZU_HAS_FORMAT was defined beside it, and neither was used anywhere in
the header, the tests, the examples or the README, which is a header
claiming a capability it did not have. There is now a to_string for
every type worth printing and a std::formatter over each, one line
apiece, so the C++20 floor gets the same text as C++23 and the two
cannot come to disagree. Each formatter inherits formatter<string_view>,
which brings the whole standard format spec with it: {:>8} pads a status
the way it pads any other string and none of that had to be written. No
operator<< to go with it, on purpose, because <ostream> would land in
every translation unit that includes this header whether it prints or
not, and a caller who wants a stream writes os << zu::to_string(e).
test_idiom.cpp is the new file and it is mostly static_assert, which is
also the point. A Result is a sized, common, random access range of Row
and is deliberately not a borrowed one, so filtering a temporary result
answers std::ranges::dangling and the mistake is a compile error where
it was written rather than a read of freed memory that works in a debug
build. Handles move and do not copy. Exceptions descend from
std::runtime_error. The value structs are std::regular. The runtime half
runs a real views pipeline and checks the number, so the concepts cannot
be satisfied by something that compiles and does nothing.
41 of 41 green with -Werror, at C++23 and at the C++20 floor.
tamnd
added a commit
to tamnd/zu
that referenced
this pull request
Aug 22, 2026
The largest item on any card here, at 15, and the last big one on zu-c's. The card goes from 50 to 67 of the 90 it is scored out of, since zu-c owes no api-map and no perf. What landed is tamnd/zu-c#9. Ninety [[nodiscard]] attributes on the calls whose return value is the failure report; a to_string and a std::formatter for every type worth printing, which is what ZU_HAS_FORMAT was for and had never been used for; and test_idiom.cpp, which is mostly static_assert on purpose, because a Result being a random access range and deliberately not a borrowed one is a promise the compiler should keep at every call site rather than one a case checked once. The sweep found a bug on its way through. zu.hpp read __cpp_lib_expected before <version> had defined it, so ZU_HAS_EXPECTED came out 0 on GCC 13 at -std=c++23 and the whole try_ half of the API went undeclared on a toolchain that has it. It depended on include order, so two translation units in one build got two different headers. Nothing said so, because the suite for that half is one #if and compiled down to a single placeholder case that passed. That file now carries an #error on the floor branch: at C++23 and above, this half missing is a broken build rather than a quiet one. Left on the card: reference, install and stability, at 10 each.
22 tasks
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.
The
idiomitem on zu-c's scorecard, which clients.toml puts at 15 and is the largest single item on any card: "The language's own shapes: how it iterates, how it releases a resource, how it awaits, what it raises. A binding that reads like the C ABI in another syntax is a binding nobody wants to write against."The header was already idiomatic in shape. What was missing was the part a compiler checks.
A dropped return was a lost failure
Ninety of the calls in
zu.hppanswer something a caller must not drop, and three of them said so. Atry_call answers astd::expectedwhose error half is the failure report, so this compiles, throws nothing, returns nothing, and loses the reason:conn.try_query(q); // the report was the return valueEvery
expectedreturn and every factory is[[nodiscard]]now. It caught a real one intest_conditions.cppon the first build, which is what a gate is for.The bug the sweep found
zu.hppread__cpp_lib_expectedbefore anything had defined it.<version>was not included, and none of the twenty headers above the check happens to pull that macro in on libstdc++. An undefined identifier is nought in a preprocessor arithmetic expression, so:GCC 13 at
-std=c++23, wherestd::expectedhas been available since GCC 12. The wholetry_half of the API went undeclared on a toolchain that has it.Worse than absent, it depended on include order: a translation unit that had already included
<version>or<expected>got a differentzu.hppthan one that had not, in the same build.And nothing said so
test_expected.cppis written as one#ifwith a single placeholder case in the#else, for the C++20 floor where there is genuinely nothing to run. With the flag stuck off, ctest ran the placeholder, printedPassed, and eleven cases about the half of the API a caller who builds without exceptions depends on had not been compiled by anything.That branch now carries an
#error. Below C++23 there is nothing to run and the throwing half is complete on its own; at C++23 and above, this half missing is a broken build rather than a quiet one, and the compiler is the only thing positioned to notice.Printing
<format>was included at the top of the header andZU_HAS_FORMATwas defined beside it. Neither was used anywhere in the header, the tests, the examples or the README, which is a header claiming a capability it did not have.There is now a
to_stringforStatus,Severity,Type,TemporalKind,Position,Node,Rel,Temporal,ErrorandValue, and astd::formatterover each that is one line apiece. The C++20 floor gets the same text as C++23 and the two cannot come to disagree about what a value looks like, for the same reason the throwing andtry_halves are one line over one implementation.Each formatter inherits
formatter<string_view>, so the whole standard format spec arrives with it and a bad spec is rejected at compile time rather than ignored:No
operator<<to go with it, deliberately.<ostream>is one of the heaviest headers in the standard library and it would land in every translation unit that includes this one whether it prints or not, for a wrapper whose first line is that it includeszu.hand calls nothing else. A caller who wants a stream writesos << zu::to_string(e), which is one call, no dependency and the same bytes.Error::report()stays what it was, the three-line spelling with a caret under the column.to_string(Error)is the one-line one, which is what a log wants.test_idiom.cpp
Mostly
static_assert, and that is the point. A concept that holds is a promise the compiler keeps at every call site rather than one a case checked once, and when a change breaks it, it breaks here with the name of the concept in the message instead of three repositories away inside astd::viewsexpression a user wrote.What it pins:
Resultis a sized, common, random access range ofRow, and its iterator is arandom_access_iterator. Random access rather than input is the difference betweenrows[500]costing a bounds check and costing five hundred reads.Resultis deliberately not aborrowed_range. That is the compiler's half of the rule the README states in English: aRowpoints into theResultit came from, soconn.query(q) | std::views::filter(f)over a temporary answers astd::ranges::danglingand the mistake is a compile error where it was written. AResultmarked borrowed would turn that into a use after free that happens to work in a debug build.Connectionwould be two owners of onezu_connand a double close.zu::Exceptionand fromstd::runtime_error, so a host with one catch at the top of a request gets the message and a host that wants the condition class catchesDataError.std::regular, so they go in a container and can be searched for.The runtime half runs a real views pipeline, a real
ranges::count_if, and checks the numbers, so the concepts cannot be satisfied by something that compiles and does nothing.One promise of this item is not testable from inside the language. A case that dropped a
[[nodiscard]]return to prove the warning fires would be a case that fails the build, since CI compiles this repository with-Werror. That one is enforced at every call site by the compiler and is deliberately not a case.Checked
Built and run on a Linux box rather than a laptop, GCC 13.3,
-Werror:41 of 41, every file at C++23 and again at the C++20 floor.
test_expectedwent from 1 case to 11 at C++23, which is the eleven that had not been compiled anywhere.