Skip to content

Speak ABI 0.15, and make the three things that were red green - #13

Merged
tamnd merged 6 commits into
mainfrom
engine-reserved-on
Aug 25, 2026
Merged

tamnd merged 6 commits into
mainfrom
engine-reserved-on

Conversation

@tamnd

@tamnd tamnd commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

main has been red since the valgrind and TSan job landed, and three separate things were wrong. This is all of them.

The engine moved to ABI 0.15 and this wrapper still said 0.14, so the check that the two agree failed before anything was built. 0.15 added tag 14 and zu_value_decimal, and a wrapper that claimed the number without reading the value would be claiming a version it does not speak, so the decimal comes with it. zu::Decimal is an unscaled 128 bit integer and a scale, kept as the two halves the ABI hands over because C++ has no standard type that wide, with unscaled() where the compiler has __int128, unscaled64() for the values that fit a plain one, and to_string writing the number out with the point where the scale puts it. Reading a decimal cell as a double is refused rather than answered approximately: Decimal::as_double is the conversion and is spelled as a call so that giving up the digits is something a program is seen to do.

ON is a reserved word now, so RETURN f.on AS on is a syntax error and the alias goes in accent quotes. The property is still named on, because a property is read by name rather than parsed as one.

The std::expected floor build fired its #error on clang, which does not have std::expected at -std=c++23 and never claimed to: clang reports __cpp_concepts as 201907L, libstdc++ gates every C++23 library feature on 202002L, and the library never switches on. The gate was asking whether the standard is C++23 when the bug it guards is about the library, so it now reads __cpp_lib_expected with <version> included on the spot.

And the valgrind job shrank the two fixtures that exist in order to be slow. The statement counts pairs, so a tenth of the rows is a hundredth of the work, and a hundredth slowed by memcheck's forty is shorter than the whole fixture is natively. The progress watcher never fired and the case that asserts it fired is the one that has been failing. The knob goes and the number is written where it is used.

Two more came out from under those. GCC at -Wpedantic -Werror says "ISO C++ does not support __int128" and stops, which clang does not say at all, so the decimal compiled on three of the four compilers. The two 128 bit calls stay and the warning is turned off across their declarations, in the header rather than in this project's flags, because the header is installed and a caller compiles it with theirs.

And with misuse passing, the threads suite ran under memcheck for the first time. Its sharing case asserts the intruding thread made at least one call, which natively it makes thousands of and under valgrind it made none, because valgrind runs one thread at a time and the first thread never blocks. It waits on a condition now.

What is left is the engine's, and it is filed as tamnd/zu#778. Five allocations survive the close that should give them back: the database, the connection, the frame and the statement in the interrupt case, and the whole connection in the sharing case. The misuse suite opens and closes hundreds of the same handles the same way and reports nothing, so this is not closing a connection leaking, it is closing a connection leaking after it has been interrupted or shared. The five are suppressed by name in test/memcheck.supp, one rule each naming the call the block came in through and the case that made it, so that the job can go green over everything else it checks. Not a rule about libzu.so, which would suppress the bug this job exists to catch. They come out when 778 closes.

tamnd added 4 commits August 25, 2026 15:16
The engine reserves ON, so MATCH (f:Flag) RETURN f.on AS on stopped
parsing and two frame tests went red against the engine at HEAD. The
property is still named on and reads fine, because a property is looked
up by name rather than parsed as one. It is the alias that is a name
being written, and the way to write one that is also a keyword is in
accent quotes, which is what the engine's own message says and what its
corpus already covers.
…s C++23

The floor build's #error fired on clang, which does not have std::expected
at -std=c++23 and never claimed to. Clang reports __cpp_concepts as
201907L, libstdc++ gates every C++23 library feature on 202002L, so the
library never switches on. The gate was asking about the standard when
the bug it guards is about the library, so it now reads
__cpp_lib_expected with <version> included on the spot, which is the
only thing that can tell a toolchain without std::expected from a
header that mislaid it.
ABI 0.15 added tag 14 and zu_value_decimal, and this wrapper still said
0.14, so the check that the two agree failed before anything was built.

A decimal is an unscaled integer and a scale, and the integer is 128
bits, which holds the thirty eight digits DECIMAL(p, s) may declare. C++
has no standard type that wide and MSVC has no extension either, so it
is kept as the two halves the ABI hands over. unscaled() rebuilds it
where the compiler has __int128, unscaled64() answers for the values
that fit a plain one, and to_string writes the number out with the point
where the scale puts it and every nought the scale asked for.

Not a double, and reading a decimal cell as one is refused rather than
answered approximately. Decimal::as_double is the conversion, spelled as
a call with a name on it so that giving up the digits is something a
program is seen to do. It takes the magnitude before it adds the halves
up: a negative decimal has a high half of -1 and a low half a hair under
two to the sixty four, and adding those as doubles cancels down to
nothing and takes every digit with it.

Doxygen needed the int128 pair in PREDEFINED. Without it the two members
behind that switch are thrown away before it sees them, so they would be
missing from the reference and from api/surface.txt while being present
in the header on every compiler this builds on.
The valgrind job set ZU_TEST_ROWS to 300, which shrank the two fixtures
that exist in order to be slow, on the reasoning that memcheck runs the
machine forty times slower and that is another way of getting a
statement long enough to be watched and interrupted.

The arithmetic does not work. The statement counts pairs, so a tenth of
the rows is a hundredth of the work, and a hundredth slowed by forty is
four tenths: under memcheck at three hundred rows the statement was
shorter than it is here at three thousand. The progress watcher asks to
be called every millisecond and the statement ended before the thread
carrying it got that far, so it never fired, and the case that asserts
it fired has been red since this job was added. The interrupt case in
the threads suite is the same shape and was only spared by being second
in a loop that stopped at the first failure.

So the knob goes. The number is written where it is used, and the two
cases cost this job the minute they cost it.
@tamnd tamnd changed the title Write an alias that is a reserved word in accent quotes Speak ABI 0.15, and make the three things that were red green Aug 25, 2026
tamnd added 2 commits August 25, 2026 16:04
GCC at -Wpedantic says "ISO C++ does not support __int128", which is
true, and with -Werror on top it says it five times and stops. Clang
says nothing, which is why this passed twice on three of the four
compilers and failed on the fourth.

The two calls stay. The alternative is handing a caller who has a 128
bit type the two halves and letting them shift and or them back
together, which is the work the type exists to save. So the warning is
turned off across those two declarations and back on directly after.

It is turned off in the header rather than in this project's flags,
because this header is installed and a caller compiles it with theirs.
A project that builds at -Wpedantic -Werror, which is a reasonable
thing to do, would otherwise fail on a header it only included. What
the caller writes in their own file is still theirs to hear about: the
pragma covers two declarations and nothing past them, which is what
the check with g++ 16 shows, where the header is clean at both -std=
c++20 and -std=c++23 and a scratch file that writes __int128 itself
still gets told.
…e keeps

The memcheck job ran misuse and stopped, because misuse failed, so the
threads suite has not run under valgrind since the job was added. It
runs now, and it had two things wrong with it.

The first is ours. one_connection_in_two_threads_is_refused_rather_
than_raced asserts the intruding thread made at least one call, and
natively it makes thousands. Under memcheck it made none: valgrind
runs one thread at a time, the first thread never blocks, and it got
through its five hundred statements and set the stop flag before the
thread it started was let go once. The assertion was false about the
scheduler rather than about the engine. So the first thread waits on a
condition the second raises after its first call, which cannot hang,
because the only way past the wait is a call that has already happened.

The second is the engine's. Five allocations survive the close that
should give them back: the database, the connection, the frame and the
statement in the interrupt case, and the whole connection in the
sharing case, holding 14,349 bytes more between them. The misuse suite
opens and closes hundreds of the same handles the same way and reports
nothing at all, so this is not closing a connection leaking. It is
closing a connection leaking after it has been interrupted or shared,
and it wants an unstripped build of the library to say which. That is
tamnd/zu#778.

Until 778 closes, those five are suppressed by name, one rule each,
each naming the call the block came in through and the case that made
it. Not a rule about libzu.so, which would be four lines instead of
forty and would suppress the bug this job exists to catch: a
connection zu-c forgot to close, or a result it forgot to free, is a
different stack and is still an error.

Indirectly lost stops counting as an error along with it, because an
indirectly lost block is one hanging off a definitely lost block, so
counting the root counts the children, and it is what lets the roots
be suppressed by name at all. It is still shown.
@tamnd
tamnd merged commit 553c177 into main Aug 25, 2026
11 checks passed
@tamnd
tamnd deleted the engine-reserved-on branch August 25, 2026 09:18
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