Skip to content

modules: add the L1 layer and six of L2, taking the count from 6 to 24 - #72

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

modules: add the L1 layer and six of L2, taking the count from 6 to 24#72
RedFox20 merged 10 commits into
masterfrom
claude/cpp-modules-migration-3d1pu3

Conversation

@RedFox20

@RedFox20 RedFox20 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Changeset 5 of docs/MODULES_MIGRATION.md. The module count goes from 6 to 24: the whole
L1 layer, and six of the eight in L2.

Layer Modules
L1 bitutils, delegate, endian, future_types, math, predicates, proc_utils, sort, source_loc, timepoint, traits, type_traits
L2 atomic_timepoint, collections, stack_trace, threads, timer, vec

Every module carries a case in tests/test_modules.cpp that 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 .cppm files was the easy half. The layer found real defects:

Header Defect Exported
math.h every function static at namespace scope, constants without inline 0 of 14 → 14
traits.h the function_traits family inside an anonymous namespace 0 of 8 → 8
timepoint.h nine namespace-scope constants without inline 0 → 25
type_traits.h nine variable templates without inline, so each importer got its own copy see below
stack_trace.h CALLSTACK_MAX_DEPTH marked static beside inline 11 → 12

traits.h is worth a second look beyond modules. An anonymous namespace inside
namespace rpp made rpp::function_traits a different type in every translation unit
that included it. That predates this work and has nothing to do with modules.

type_traits.h is the same class of defect one level down. A namespace-scope constexpr
variable template without inline gets one copy per translation unit on gcc, so the module
importer and the header includer saw different addresses for the same trait. A probe on
gcc 14.2 reported plain same=0 against inline same=1, and nm marks the symbol local.
tests/test_modules_identity.cpp takes the address through the module alone, and
test_modules.cpp compares 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:

  1. A macro a define can toggleRPP_ENABLE_UNICODE and !RPP_BARE_METAL. The
    generator parses each configuration and guards the difference. threads.h drops from 7
    declarations to 1 under RPP_FREERTOS=1, and type_traits.h from 22 to 20.
  2. A macro no define reachesRPP_HAS_COROUTINES, which the header derives from
    __has_include. The generator reads the region the header brackets with it.
  3. An inline namespace — read from the header, rather than guessed from a name ending in
    ::literals. That spelling missed rpp::duration_literals, so a module consumer lost
    using 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::semaphore and
rpp::concurrent_queue classes on a target without <coroutine>. Neither header carries a
module yet, so nothing shipped broken, and the L3 layer would have hit it.

Two more generator defects, each pinned by a selftest

  1. An out-of-line member definition reached the export list. delegate.h defines
    delegate<...>::operator() and ::invoke at namespace scope, so the traversal emitted
    using rpp::operator(); and the module failed to build. A cursor whose semantic parent
    is a class no longer counts.
  2. SKIP_KINDS dropped UNEXPOSED_DECL, which is the kind libclang reports for a
    variable 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, all
    external linkage.

sprint and task do not ship

gcc-14 writes a .gcm for each 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, which writes the interface
skeleton first, because no rpp-sprint.cppm ever landed in a commit.

Both .cppm files wait, because a module no importer can read would leave the build green
while the module stayed unusable. sprint blocks the most, since string_buffer, format
and every to_string overload sit in that header, so the plan now names it the step before
the 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 build: 549/549 pass, up from 533
  • header build: 528/528 pass, which is what proves the linkage changes are safe off the
    modules path
  • all three generator gates report no finding, selftests included
  • four module-only consumers exit 0
  • clang-tidy reports nothing over 54 translation units
  • CI: 28 of 28 jobs pass on 26c382e, first attempt

Two TSAN jobs flaked earlier in the branch, on a different job each run, and a re-run cleared
each one. Both are catalogued: BUGS.md B6, the exception refcount inside an
uninstrumented 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

RedFox20 and others added 2 commits September 5, 2026 23:00
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
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 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-06T18:37:22.666984Z 8d04bf6 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: 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".

Comment thread src/rpp/rpp-threads.cppm
Comment thread src/rpp/rpp-type_traits.cppm Outdated
Comment thread BUGS.md Outdated
Comment thread tests/test_modules.cpp
Comment thread src/rpp/rpp-type_traits.cppm
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

@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: 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".

Comment thread src/rpp/rpp-future_types.cppm Outdated

RedFox20 commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

CI: the one red job is a known flake, not this change

Two runs, 27 of 28 green each time, and a different job red in each one.

Run Commit Red job Cause
33999376432 fe1cfb7 ubuntu-cpp23-tsan-gcc13 BUGS.md B6
34000577207 ed37bf4 ubuntu-cpp20-tsan-clang18 BUGS.md B6 and B2

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 B6

All tests passed, and the sanitizer set the exit code:

SUCCESS: All 31 test suites with 528/528 test cases passed! 6.31s
ThreadSanitizer: reported 2 warnings
RppTests failed with exit code 66

Both reports name tests/test_future.cpp:232. Worker T103 reads e.what() inside the except handler. Worker T98 leaves its catch block at future.h:78, and __cxa_end_catch frees the runtime_error string. TSAN prints the read as the earlier access, so the order is correct. Only the happens-before edge is missing, because the exception_ptr refcount sits inside an uninstrumented libstdc++.so. B6 names this race by that test.

Run 2, clang: B6 again, plus B2

The TSAN report names std::range_error::~range_error() in libc++abi.so.1, which is the same shape. C15 suppresses race:std::__1::promise, and that pattern matches neither range_error nor the gcc spelling.

The same job also hit a timing assertion:

FAILED ASSERTION test_threadpool.cpp:357  parallel_elapsed => '0.044880' must be less or equal than '0.042352'
Test System # Max Parallelism: 2

The runner reported 2 usable cores, so the test took the parallelism <= 2 branch and its 5 ms budget. TSAN erased that margin, and the overshoot was 2.5 ms. That is BUGS.md B2.

Why this change cannot cause either one

  1. Every test case passes in both runs. The exit code comes from the sanitizer or from a clock bound.
  2. ubuntu-cpp20-modules-gcc14 and ubuntu-cpp20-modules-clang21, the two jobs this changeset exists for, pass on both commits.
  3. The diff adds .cppm facades, replaces static constexpr with inline constexpr, and adds inline to nine variable templates. None of that creates a data race or slows parallel_for.

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 future.h:78, so a wide pattern would hide real races. B2 needs a bound which waits on an event rather than on the clock. Both are their own change.


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

@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: 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".

Comment thread tests/test_modules.cpp
Comment thread tests/test_modules.cpp Outdated
Comment thread src/rpp/rpp-timepoint.cppm Outdated
Comment thread tools/gen_module_exports.py Outdated
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
Comment thread src/rpp/rpp-sort.cppm
RedFox20 and others added 2 commits September 6, 2026 17:53
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

@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: 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".

Comment thread src/rpp/sort.h
Comment thread src/rpp/sort.h
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

@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: 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".

Comment thread src/rpp/sort.h
RedFox20 and others added 2 commits September 6, 2026 18:28
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
@RedFox20
RedFox20 merged commit e423016 into master Sep 6, 2026
28 checks passed
@RedFox20
RedFox20 deleted the claude/cpp-modules-migration-3d1pu3 branch September 6, 2026 18:48
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