Skip to content

modules: land rpp.sprint and rpp.task, fix the TSAN hook, and move the traits to concepts - #73

Merged
RedFox20 merged 9 commits into
masterfrom
claude/cpp-modules-migration-3d1pu3
Sep 7, 2026
Merged

modules: land rpp.sprint and rpp.task, fix the TSAN hook, and move the traits to concepts#73
RedFox20 merged 9 commits into
masterfrom
claude/cpp-modules-migration-3d1pu3

Conversation

@RedFox20

@RedFox20 RedFox20 commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Three independent changes, plus the review round. Two close an open entry in BUGS.md, and one opens a new one.

1. B6: libtsan.so never read the suppression hook (181035c)

B6 blamed the libc++ spelling of the C15 pattern, and the cause is one attribute. gcc links
libtsan.so, which reads __tsan_default_suppressions through the global dynamic symbol
table. -fvisibility=hidden kept the definition out of it, so dlsym answered with the weak
hook inside libtsan.so, which returns null. Every pattern was dead since C15. clang links
its runtime statically, which is why C15 worked there.

The hook and its patterns moved to tests/test_sanitizers.cpp, beside a test which reads the
string back. On gcc it calls dlsym(RTLD_DEFAULT, ...), and that call fails without the
attribute. clang takes the hook directly, because its static runtime never enters .dynsym.

Both reports on master name test_future::test_except_handler_chaining, which shares one
exception object between two pool workers. The refcount which orders them sits in an
uninstrumented libstdc++.so, so TSAN sees no edge and calls the free a race.

Measured on 2 pinned cores: 3 reports in 120 runs before, 0 in 120 after. Ten full-suite runs
matched no suppression at all, which is what B6 asked for.

One report survived the suppression, and it is a different defect. A pool worker reads its
semaphore after the pool destroyed it, so BUGS.md B10 carries it.

2. B8: two shapes gcc-14 cannot write (7b2db16)

B8 said gcc-14 writes a module for sprint.h and task.h that no importer reads, and it
named a compiler defect with no shape. Both modules ship now. A bisect outside cmake, which
builds every .cppm into one gcm.cache and answers in seconds, found two shapes.

Shape 1 reaches rpp.sprint through rpp.type_traits. An export which names a function
in the std::__cxx11 inline namespace makes the module unreadable. std::to_string and
std::stoi both do it, and std::swap does not. The form does not matter. An is_detected_v
alias, a plain alias template and a C++20 concept all fail the same way, and so does a concept
which calls an unexported helper that names it. Only dropping the export works.

So the generator gains NO_EXPORT, and has_std_to_string leaves the module surface. A
header includer still gets the trait.

Shape 2 reaches rpp.task. A module which includes future_types.h in its global module
fragment and also imports rpp.future_types writes an unreadable .gcm. Either half alone is
fine, and the importer only fails when it also includes <rpp/tests.h>. NO_IMPORT drops
that one re-export.

3. The detection idiom becomes C++20 concepts (21920c4)

ReCpp demands C++20, and config.h carries a static_assert which proves it. So every trait
built on is_detected_v became a concept, and every enable_if_t site became a requires
clause. src/rpp carries neither spelling now.

type_traits.h drops eight expression aliases and turns seven traits into concepts. Each one
keeps the expression it detected before, so has_to_string still reads through ADL alone, and
is_container still lets a string-like type lose to is_stringlike. is_detected and
is_detected_v stay, because a consumer brings its own alias, and krattlink does exactly that
in MessageCodec.h.

sprint.h had two detection aliases inside string_buffer, and a concept cannot live in a
class. Both moved to namespace scope as has_ostream_op and has_member_sbuf_op.
mutex.h, binary_stream.h and sprint.h took a requires clause each. delegate.h took
eight, and its two conditions read as named constants now.

test_modules_identity.cpp lost its anchor, because no variable template is left in
type_traits.h, so it takes the address of rpp::PI instead. That constant is one math.h
shipped without inline, which is the defect the case guards.

4. The review round, and a gate which was checking nothing (b59eb6a, f039f52, 04a55cc, c9497d9)

Codex raised six findings. Five are fixed: the generator now re-raises a parse error the
NO_CONFIG allowlist does not name, C25 is two sentences, the plan counts read 18, the
module-only consumer includes <utility>, and B10 carries its own reproducer. The sixth asks
to remove the test-scoped TSAN pattern, and that thread stays open with the measurement,
because removing it reproduces B6 and no narrower pattern reaches the second report.

Two CI rounds went red on clang-tidy findings in the new test, and chasing them exposed
B11. CXX20=1 mama gcc build clang-tidy test="nogdb -vv", which AGENTS.md names as the
gate, leaves CMAKE_CXX_CLANG_TIDY unset because mama reuses the directory a plain build
configured. It exits 0 having analyzed nothing. Adding configure sets the variable, and then
a gcc-14 build stops, because clang-tidy cannot parse the gcc module flags. The CI gcc-13 jobs
never hit that, because gcc-13 builds no modules.

Verification

Gate Result
modules build, gcc-14 557/557
header build, clang-18 534/534
TSAN build, gcc-14 and clang-18 557/557 and 534/534
module consumer, gcc exits 0 on the modules path
the five generator and include gates no finding
clang-tidy see B11. The local gate analyzes nothing, so CI is the only real check. Each finding in this PR was verified against clang-tidy-18 directly

L2 is complete, so twenty-six modules ship. docs/MODULES_MIGRATION.md revision 10 carries
the handover state.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN

RedFox20 and others added 2 commits September 6, 2026 19:29
B6 blamed the libc++ spelling of the C15 pattern, and the cause is one attribute.
gcc links libtsan.so, which reads __tsan_default_suppressions through the global
dynamic symbol table. -fvisibility=hidden kept the definition out of it, so dlsym
answered with the weak hook inside libtsan.so, which returns null. Every pattern
was dead. clang links its runtime statically, which is why C15 worked there.

The hook and its patterns moved to tests/test_sanitizers.cpp, beside a test which
calls dlsym(RTLD_DEFAULT, ...) and reads the string back. That test fails without
the attribute, and it names the exact property CI lost.

The pattern list grew to cover what the gcc jobs report. Both reports on master
name test_future::test_except_handler_chaining, which shares one exception object
between two pool workers. The refcount which orders them sits in an uninstrumented
libstdc++.so, so TSAN sees no edge and calls the free a race.

Measured on 2 pinned cores: 3 reports in 120 runs before, 0 in 120 after, and the
two patterns matched 3 times each. Ten full-suite runs matched no suppression at
all, which is what B6 asked for.

One report survived the suppression, and it is a different defect. A pool worker
reads its semaphore after the pool destroyed it, so BUGS.md B10 carries it.

Verified: gcc TSAN 553/553, gcc 553/553, clang 532/532, clang-tidy reports nothing,
and all five gates report no finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
B8 said gcc-14 writes a module for sprint.h and task.h that no importer reads,
and it named a compiler defect with no shape. Both modules ship now. A bisect
outside cmake, which builds every .cppm into one gcm.cache and answers in
seconds, found two shapes and one workaround each.

Shape 1 reaches rpp.sprint through rpp.type_traits. An export which names a
function in the std::__cxx11 inline namespace makes the module unreadable.
std::to_string and std::stoi both do it, and std::swap does not. The form does
not matter. An is_detected_v alias, a plain alias template and a C++20 concept
all fail the same way, and so does a concept which calls an unexported helper
that names it. Only dropping the export works.

So the generator gains NO_EXPORT, and has_std_to_string leaves the module
surface. A header includer still gets the trait. std_to_string_expression is
gone, because the concept replaced the alias it fed. The concept keeps the old
expression, so a class with a non-const conversion operator still matches, and
test_sprint.cpp pins all eight cases.

Shape 2 reaches rpp.task. A module which includes future_types.h in its global
module fragment and also imports rpp.future_types writes an unreadable .gcm.
Either half alone is fine, and the importer only fails when it also includes
<rpp/tests.h>. NO_IMPORT drops that one re-export.

Two smaller fixes ride along. The generator read every header in the bare metal
configuration and stopped on a fatal parse, so sprint.h could not generate at
all. A header which does not compile in a configuration now drops that guard,
because no guard per name saves a header a whole configuration rejects.
RppMaskedModuleOnly links the library, because sprint.cpp holds the out-of-line
half of rpp.sprint.

The generator selftest pins both knobs and the export import a working toolchain
writes. test_modules.cpp takes a case per module, and masked_module_only.cpp
drives the sprint surface with no header at all.

Verified: the modules build passes 556/556, the header build passes 533/533, the
TSAN build passes 556/556, the module consumer exits 0 on the modules path, all
five gates report no finding, and clang-tidy reports nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-07T00:15:48.112411Z ac6f145 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b2db16ed1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/gen_module_exports.py Outdated
Comment thread BUGS.md Outdated
Comment thread tests/test_sanitizers.cpp
Comment thread docs/MODULES_MIGRATION.md
ReCpp demands C++20, and config.h carries a static_assert which proves it. So
every trait built on is_detected_v became a concept, and every enable_if_t site
became a requires clause. src/rpp carries neither spelling now.

type_traits.h drops eight expression aliases and turns seven traits into
concepts: has_to_string, has_to_string_memb, has_get_memb, has_set_memb,
is_iterable, is_stringlike and is_container. Each one keeps the expression it
detected before, so has_to_string still reads through ADL alone, and
is_container still lets a string-like type lose to is_stringlike.

is_detected and is_detected_v stay. A consumer brings its own expression alias,
and krattlink does exactly that in MessageCodec.h.

sprint.h had two detection aliases inside string_buffer, and a concept cannot
live in a class. Both moved to namespace scope as has_ostream_op and
has_member_sbuf_op, above a forward declaration of string_buffer.

mutex.h, binary_stream.h and sprint.h took a requires clause each. delegate.h
took eight, and its two conditions read as named constants now, not_copy_ctor
and args_match, so the positive and the negative constructor pair state the same
rule once.

Two tests moved with the traits. masked_module_only.cpp writes its own
expression alias, which proves the idiom rather than one alias ReCpp used to
export. test_modules_identity.cpp lost its anchor, because no variable template
is left in type_traits.h, so it takes the address of rpp::PI instead. That
constant is one math.h shipped without inline, which is the defect the case
guards.

test_sprint.cpp gains a case which pins all eleven traits against the
expressions they name.

Verified: the modules build passes 557/557, the header build passes 534/534, the
TSAN build passes 557/557, the module consumer exits 0 on the modules path, all
five gates report no finding, and clang-tidy reports nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
@RedFox20 RedFox20 changed the title modules: land rpp.sprint and rpp.task, and make the TSAN suppression hook reachable modules: land rpp.sprint and rpp.task, fix the TSAN hook, and move the traits to concepts Sep 6, 2026
RedFox20 and others added 3 commits September 6, 2026 20:58
Five items, and a measurement backs each one.

clang-tidy reported performance-enum-size on the scoped enum in test_sprint.cpp,
which three jobs failed on. The enum takes uint8_t now. A probe reproduces the
warning on the old spelling and reports nothing on the new one.

The two clang TSAN jobs failed on the test this branch added. clang links its
sanitizer runtime statically, so the linker binds __tsan_default_suppressions and
dlsym never sees it. The dlsym probe is a gcc case now, and clang reads the hook
directly. C25 stands: the defect and its fix are both gcc-only.

The generator dropped a guard configuration on any parse error, so a real error
in another header would have produced an unguarded export. NO_CONFIG names the
one header which does not compile bare metal, and every other diagnostic reaches
the caller. The selftest pins both directions.

C25 was several paragraphs, and R9b asks for two sentences. The plan carried two
stale counts, 20 remaining headers and six of eight L2 modules, and both read 18
and eight now.

Verified: the modules build passes 557/557, the header build passes 534/534, both
TSAN builds pass, the module consumer exits 0 on the modules path, clang-tidy
reports nothing on C++20, and all five gates report no finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
clang-tidy on C++23 reported two more findings in the case this branch added.
readability-named-parameter wants the ADL probe argument named, and
performance-enum-size wants a base type on the unscoped enum too. The enum still
promotes to int, so std::to_string takes it and the assertion holds.

The C++20 job stayed quiet on both, which is why the last push missed them. A
standalone probe reports 2 findings on the old spelling and 0 on the new one.

Verified: clang-tidy reports nothing on C++20 and on C++23, the modules build
passes 557/557, the header build passes 534/534, and the TSAN build passes
557/557.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
The command AGENTS.md names as the clang-tidy gate exits 0 on a warm build tree
and reports no finding, while the CI job of the same name fails. Two rounds on
this pull request went red that way, and the commit before this one blamed a
C++20 against C++23 difference. That was wrong. The C++20 job reported both
findings too.

mama reuses the build directory a plain build configured, so
CMAKE_CXX_CLANG_TIDY never lands in the cache and no translation unit is
analyzed. Adding `configure` sets it, and then a gcc-14 build stops, because
clang-tidy cannot parse the gcc module flags. B11 carries the evidence and a
standalone command which does check one finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04a55cce89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/module_consumer/masked_module_only.cpp
Comment thread BUGS.md Outdated
RedFox20 and others added 2 commits September 6, 2026 21:25
…ducer

The module-only consumer names std::declval and included only <string> and
<vector>. libstdc++ declares it through <string>, so the gate passed by luck,
which is what the include-what-you-use rule exists to stop.

B10 told a reader to run the C25 loop, and the R9b cut removed that loop in the
same push. B10 carries its own loop now, which greps for heap-use-after-free
instead of a race.

Verified: the module consumer exits 0 on the modules path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
The concepts commit moved seven delegate declarations from enable_if_t to a
requires clause, and three of them had no test which the constraint could break.
A mutation over each one says which.

Removing not_copy_ctor from the master constructor makes the suite segfault, but
only in the case this commit adds. The unconstrained template beats the copy
constructor for a non-const lvalue, so a delegate wraps itself and recurses.
Every case which ran before that one passed, so the suite never saw it.

Swapping the args_match pair on the two const-instance constructors crashes the
new const case. Removing one constraint of that pair is benign, because C++20
partial ordering prefers the constrained candidate over an unconstrained one.
That is a real difference from the enable_if form, where both were viable.

Two cases close a coverage gap rather than pin a constraint. reset() had no test
at all, and it takes the same constraint as the master constructor, so the case
drives all three dispatch branches plus the two member overloads. operator= had
only the delegate side through copy_operator_lambdas, so the callable side takes
a case of its own.

The const-instance case is the first test to bind a member function through a
const object. `methods` and the decay_adapter cases all pass a non-const
pointer, which the const overload accepts by conversion.

Verified: the modules build passes 561/561, the header build passes 538/538, the
TSAN build passes 561/561, clang-tidy reports nothing over the changed file, and
all five gates report no finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac6f14578a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/MODULES_MIGRATION.md Outdated
The four delegate cases run in both modes, so the handover table reads 561/561
and 538/538 now. The last count refresh landed with the trait cases, one commit
before this one added them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
@RedFox20
RedFox20 merged commit d48d47a into master Sep 7, 2026
28 checks passed
@RedFox20
RedFox20 deleted the claude/cpp-modules-migration-3d1pu3 branch September 7, 2026 00: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