modules: add the L1 layer and six of L2, taking the count from 6 to 24 - #72
Conversation
Changeset 5 continues with L1. Every L1 header depends only on config or minmax, so all twelve build against the modules that already exist. Four headers declared public API the compiler refused to export, and each fix lands here: - math.h marked every function static at namespace scope, which is internal linkage, and left the four constants without inline. It exported 0 of 14 names. - traits.h wrapped the function_traits family in an anonymous namespace, so every translation unit held a different rpp::function_traits. It exported 0 of 8. - timepoint.h left nine namespace-scope constants without inline. The generator already reported this one and named the fix. - type_traits.h needed no change, but the generator dropped its nine variable templates, is_detected_v and is_container among them. The generator mishandled two kinds of declaration: - An out-of-line member definition sits at namespace scope, so delegate.h fed rpp::operator() and rpp::invoke into the export list and the module failed to build. A cursor whose semantic parent is a class no longer counts. - SKIP_KINDS dropped UNEXPOSED_DECL, which is the kind libclang reports for a variable template. A measurement over every header put the cost at nine names, all in type_traits.h, all external linkage. rpp_decls and gen_module_exports each carry a selftest case for the shape they missed, so the gate catches both again. Nine modules take a case in tests/test_modules.cpp, which includes no header of its own. <rpp/tests.h> masks type_traits, source_loc and future_types, so l1_module_only.cpp imports those three with no header at all. Verified: the modules build passes 542/542, the header build passes 528/528, all five gates report no finding, both module-only consumers exit 0, and clang-tidy reports nothing over 54 translation units. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
atomic_timepoint, collections, stack_trace, threads, timer and vec. Every L2
header depends only on modules that already exist, so all eight generate and
compile against the L1 layer.
stack_trace.h marked CALLSTACK_MAX_DEPTH static at namespace scope, which is
internal linkage even beside inline, so the module could not export it. The
generator reported it and named the fix.
sprint and task do not ship. gcc-14 writes a .gcm for each one and no importer
reads it back:
rpp.sprint: error: failed to read compiled module cluster 1510: Bad file data
fatal error: failed to load pendings for 'std::_Mutex_base'
The message names a libstdc++ internal, so this is a compiler defect and not a
wrong export list. Each module fails alone, and a consumer that includes <mutex>
before the import fails the same way. BUGS.md B8 carries the reproducer.
Shipping a module no importer can read would leave the build green while the
module stayed unusable, so both .cppm files wait. sprint blocks the most, because
string_buffer, format and every to_string overload sit in that header. The plan
now names it the step before the L3 layer, ahead of the remaining layers.
tests/test_modules.cpp takes a case per shipped module. l1_module_only.cpp is now
masked_module_only.cpp, which is what <rpp/tests.h> masks rather than one layer.
Verified: the modules build passes 548/548, the header build passes 528/528, all
seven gates report no finding, three module-only consumers exit 0, and clang-tidy
reports nothing over 54 translation units.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe1cfb774a
ℹ️ 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".
Four of them are real, and a measurement backs each one. type_traits.h left nine variable templates without inline, so gcc gave the module importer and the header includer one copy each. A two translation unit probe reported different addresses for is_container, and inline made them one. tests/test_modules_identity.cpp takes the address through the module alone, and test_modules.cpp compares it against the header address. The case fails on the old header and passes on the new one. The generator read every header in the host configuration only, so a bare metal build got an export list naming declarations the header hides. threads.h drops from 7 declarations to 1 under RPP_FREERTOS=1, and type_traits.h drops from 22 to 20. The generator already read two RPP_ENABLE_UNICODE configurations and guarded the difference, so that single macro becomes a guard list and bare metal joins it. Regeneration touched those two module interfaces and no other, which shows no other header hides a declaration this way. <rpp/tests.h> includes math.h, so the math case in test_modules.cpp passed whatever rpp.math exported. masked_module_only.cpp imports it with no header now. The B8 reproducer told a reader to restore rpp-sprint.cppm, which no commit ever carried, and the generator refuses a file without the two markers. The entry writes the skeleton first. Verified: the modules build passes 549/549, the module consumer exits 0 on the modules path, and every generator gate reports no finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed37bf403d
ℹ️ 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".
CI: the one red job is a known flake, not this changeTwo runs, 27 of 28 green each time, and a different job red in each one.
Each job passed on the run where the other one failed, so the failure follows the runner and not the code. I re-ran the failed job. Run 1, gcc: BUGS.md B6All tests passed, and the sanitizer set the exit code: Both reports name Run 2, clang: B6 again, plus B2The TSAN report names The same job also hit a timing assertion: The runner reported 2 usable cores, so the test took the Why this change cannot cause either one
No fix ships here. B6 needs a gcc branch and a libstdc++ pattern, plus a run which proves the suppression hides this race and hides no other. One of the gcc reports frees inside ReCpp code at Generated by Claude Code |
future_types.h declares coro_handle, suspend_never and suspend_always inside #if RPP_HAS_COROUTINES, and the module named all three without a guard. A target whose C++20 toolchain ships no <coroutine> header then fails to compile rpp.future_types. The guard list from the last commit cannot reach this one. The header derives RPP_HAS_COROUTINES from __has_include, and config.h defines RPP_HAS_CXX20 as (__cplusplus >= 202002L) beside a static_assert which demands it, so no define turns either one off. So the generator gains a second kind of guard. TEXT_GUARDS names a macro the generator cannot toggle, and the generator reads the region the header brackets with it. A scan over all 24 module headers matches those three names and nothing else, so the change touches one module interface. The selftest header carries a guarded declaration now, and it pins both directions. A name inside the block takes the #if, and a name outside takes none. Verified: the modules build passes 549/549, the module consumer exits 0 on the modules path, and every generator gate reports no finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c38e4a11c
ℹ️ 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".
The text guard from the last commit searched the guarded body for the name, so a
block which only mentioned a name guarded that name too. A simulation over the
headers which carry coroutine members shows what that costs:
semaphore.h would guard: rpp::parallel_task_detached rpp::semaphore
concurrent_queue.h would guard: rpp::parallel_task_detached rpp::concurrent_queue
Both classes are unconditional, and a guard would hide each whole class wherever
the macro is 0. Neither header carries a module yet, so nothing shipped broken,
and the L3 layer would have hit it.
So declarations() carries the declaration line now, and the guard asks whether
that line sits inside a guard span. The same simulation keeps
parallel_task_detached, which semaphore.h really declares inside a coroutine
block at line 21, and it drops both classes. No module interface changed, which
is the expected result for the 24 that ship.
Three smaller fixes ride along:
- timepoint.h declares `inline namespace duration_literals`, and the generator
marked a namespace inline only when it ended in `::literals`. It reads the
header for the inline keyword now, so `using namespace rpp; 1_s` resolves
through the module. The test asks for it that way.
- TimePoint::now() reads CLOCK_REALTIME, which steps. The elapsed check in
test_modules.cpp takes ClockType::Monotonic.
- The threads case calls six names the module hides on bare metal, so it takes
the same guard. yield() stays outside it.
The generator selftest gains a class whose guarded member names the class, which
is the exact shape the text search got wrong.
Verified: the modules build passes 549/549, the module consumer exits 0 on the
modules path, and all three generator gates report no finding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
A review asked whether rpp::sort ships. It does, and the module was right, but no test proved it. collections.h declares the three sort overloads at lines 532, 538 and 544 as wrappers over rpp::insertion_sort, so rpp.collections carries them. sort.h declares insertion_sort, two concepts and container_element_t, and rpp.sort carries those four. The collections case covered contains, index_of, sum_all, any_of and range, and it now calls both sort overloads. The case includes no rpp header, so the module alone supplies the name. Verified: the modules build passes 549/549. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
An importer of rpp.sort could not call rpp::sort. collections.h declared the three overloads, so the name reached an importer through rpp.collections instead, which is not the module the name suggests. sort.h already carried the concepts the move needs. contiguous_container asks for data(), size() and operator[], and element_range answers all three, so two generic overloads replace the three which named std::vector and element_range one by one. The forwarding reference takes both an lvalue vector and the temporary that rpp::range() returns. Nothing breaks for a consumer of collections. collections.h includes sort.h at line 11, and rpp-collections.cppm already carried `export import rpp.sort;`, so both the include path and the import path still reach rpp::sort. tests/module_consumer/sort_module_only.cpp imports rpp.sort with no header and no rpp.collections beside it, then calls both sort overloads and insertion_sort. The target fails to build if the name ever leaves that module again. Verified: the modules build passes 549/549, RppSortModuleOnly exits 0, the module consumer exits 0 on the modules path, and the include and generator gates report no finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5786e87140
ℹ️ 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".
The move in 5786e87 broke two calls which used to compile. A probe reproduces each one against that commit: rpp::sort<int>(v); // error: no matching function const rpp::element_range<int> r = rpp::range(v); rpp::sort(r, cmp); // error: T = const int, no swap An explicit template argument named the element type of the old vector overload, and the forwarding reference reads it as the container instead. A const range is worse. The old parameter took the range by value, so the copy gave a mutable view over mutable elements, and a reference keeps the view const and makes data() return const T*. Both shapes only ever reached a caller through collections.h, and element_range lives there, so both overloads go back there. sort.h keeps the two generic ones, so `import rpp.sort;` still serves a vector, and the minimal sort header still needs no <vector>. The vector overload is more specialized than the generic one, so a plain rpp::sort(v) stays unambiguous. Six call shapes compile, including the three which krattcam and krattlink use. test_sort.cpp takes three cases, so every header build covers them, not only the two modules jobs. Verified: the modules build passes 552/552, RppSortModuleOnly exits 0, the module consumer exits 0 on the modules path, and the generator gates report no finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4898141536
ℹ️ 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".
The handover state carries what the two layers taught. Six headers declared API the compiler could not export, and type_traits.h is the one worth reading twice. Nine variable templates without inline gave gcc one copy per translation unit, so a module importer and a header includer read different addresses for one trait. The generator learned three configuration rules, each with a selftest. It used to emit an export list for whatever configuration it parsed in, so a declaration a header hides under another configuration became an unguarded export. The table names what each rule reaches. A module can also move a declaration. rpp::sort left collections.h for sort.h, so an importer of rpp.sort can call it, and the two shapes only collections.h can offer stayed there. B6 joins the open questions with its measured cost. It failed 6 of the 40 TSAN jobs this branch ran, and every one passed every test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
The README sort.h table listed 1 of the 4 functions the header declares. It carries all four now, and update_doc_linerefs.py corrected the three new line references itself. --check-undocumented stayed quiet through this, so BUGS.md B9 records why. extract_public_decls returns nothing for 19 of the 48 headers, sort.h among them, so the gate never asked whether README documents them. The entry carries the loop which produced that count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
Changeset 5 of
docs/MODULES_MIGRATION.md. The module count goes from 6 to 24: the wholeL1 layer, and six of the eight in L2.
Every module carries a case in
tests/test_modules.cppthat includes no header of its own,so the module alone has to supply the names.
Six headers declared API the compiler could not export
Writing the
.cppmfiles was the easy half. The layer found real defects:math.hstaticat namespace scope, constants withoutinlinetraits.hfunction_traitsfamily inside an anonymous namespacetimepoint.hinlinetype_traits.hinline, so each importer got its own copystack_trace.hCALLSTACK_MAX_DEPTHmarkedstaticbesideinlinetraits.his worth a second look beyond modules. An anonymous namespace insidenamespace rppmaderpp::function_traitsa different type in every translation unitthat included it. That predates this work and has nothing to do with modules.
type_traits.his the same class of defect one level down. A namespace-scopeconstexprvariable template without
inlinegets one copy per translation unit on gcc, so the moduleimporter and the header includer saw different addresses for the same trait. A probe on
gcc 14.2 reported
plain same=0againstinline same=1, andnmmarks the symbol local.tests/test_modules_identity.cpptakes the address through the module alone, andtest_modules.cppcompares it against the header address.The generator learned three configuration rules
The generator emitted an export list for whatever configuration it happened to parse in, so
any declaration a header hides under a different configuration became an unguarded export.
It now handles three cases, each pinned by a selftest:
RPP_ENABLE_UNICODEand!RPP_BARE_METAL. Thegenerator parses each configuration and guards the difference.
threads.hdrops from 7declarations to 1 under
RPP_FREERTOS=1, andtype_traits.hfrom 22 to 20.RPP_HAS_COROUTINES, which the header derives from__has_include. The generator reads the region the header brackets with it.::literals. That spelling missedrpp::duration_literals, so a module consumer lostusing namespace rpp; 1_s.Rule 2 matches by declaration location, not by name text. A text search also matched a name
a guarded block only mentions, which would have hidden the whole
rpp::semaphoreandrpp::concurrent_queueclasses on a target without<coroutine>. Neither header carries amodule yet, so nothing shipped broken, and the L3 layer would have hit it.
Two more generator defects, each pinned by a selftest
delegate.hdefinesdelegate<...>::operator()and::invokeat namespace scope, so the traversal emittedusing rpp::operator();and the module failed to build. A cursor whose semantic parentis a class no longer counts.
SKIP_KINDSdroppedUNEXPOSED_DECL, which is the kind libclang reports for avariable template, so every variable template was missing from every module. A
measurement over all headers put the cost at nine names, all in
type_traits.h, allexternal linkage.
sprint and task do not ship
gcc-14 writes a
.gcmfor each and no importer reads it back:The message names a libstdc++ internal, so this is a compiler defect and not a wrong export
list. Each module fails alone, and a consumer that includes
<mutex>before the importfails the same way.
BUGS.mdB8 carries the reproducer, which writes the interfaceskeleton first, because no
rpp-sprint.cppmever landed in a commit.Both
.cppmfiles wait, because a module no importer can read would leave the build greenwhile the module stayed unusable.
sprintblocks the most, sincestring_buffer,formatand every
to_stringoverload sit in that header, so the plan now names it the step beforethe L3 layer. Only clang-21 and a newer gcc can answer whether B8 is gcc-only, and this
machine carries clang-18, which cannot build modules at all.
Verified
modules path
26c382e, first attemptTwo TSAN jobs flaked earlier in the branch, on a different job each run, and a re-run cleared
each one. Both are catalogued:
BUGS.mdB6, the exception refcount inside anuninstrumented libstdc++ or libc++abi, and B2, a 5 ms timing budget on a runner which
reports 2 usable cores. Neither is this changeset.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PJQak2qMQ4cXHtqEcpwimN
Generated by Claude Code