Skip to content

Create a stdexec module - #2138

Merged
ericniebler merged 12 commits into
NVIDIA:mainfrom
ispeters:modularize
Jul 27, 2026
Merged

Create a stdexec module#2138
ericniebler merged 12 commits into
NVIDIA:mainfrom
ispeters:modularize

Conversation

@ispeters

Copy link
Copy Markdown
Contributor

Summary

This diff starts the process of modularizing stdexec so that consumers can say

import stdexec;

rather than

#include<stdexec/execution.hpp>

The initial module exports nothing except stdexec::just_t so there's a test that confirms consumers can utter stdexec::just_t:

TEST_CASE("I can use ex::just in a modules build", "[modules]")
{
STATIC_REQUIRE(sizeof(ex::just_t) > 0);
}

It builds and passes on my machine.

I've tried to add the above test to CI with the idea that, if this PR is going in the right direction and we merge it with an incomplete module then at least other changes won't break the modularization effort.

Strategy

I'm aping the conditional modules support that the Beman Project's exemplar project scaffolds for new repos. As I understand it, the idea is:

  • There's one main module per "library". I expect it would be useful for stdexec to have stdexec for the contents of execution.hpp and something like stdexec.exec or stdexec.experimental for everything in the exec namespace. This diff introduces only stdexec and it's defined in a new file named modules/stdexec.cppm.
  • Every header has to support being included in both a modules and non-modules context. This diff makes that true for enough headers that the test compiles and runs; I doubt it's all headers.
  • The module definition does a few things:
    • Include non-modularized headers (e.g. std headers that export macros) into the Global Module Fragment (GMF).
    • Declare the module we're exporting
    • #define STDEXEC_IN_MODULE_PURVIEW, which is the signal we're in the "we're defining the module" mode.
    • #include <stdexec/execution.hpp>
  • With the foregoing setup, all headers can now distinguish between:
    • Being included in a traditional non-modules setup (STDEXEC_USE_MODULES() is false)
    • Being included in the module definition (STDEXEC_USE_MODULES() is true and STDEXEC_IN_MODULE_PURVIEW is defined)
    • Being included with modules enabled but not in the module definition (STDEXEC_USE_MODULES() is true and STDEXEC_IN_MODULE_PURVIEW is not defined)
  • If modules are disabled or we're defining the module then a header needs to behave like a traditional header and declare and/or define its symbols.
    • In the case we're defining the module, std includes should not be included because the standard library will already have been imported.
  • If modules are enabled and we're not defining the module then a header should just import stdexec.
  • Throughout the library's headers, certain symbols should be exported from the module. To do this, prefix it with STDEXEC_MODULE_EXPORT; when the header is being included to define the module, that macro will expand to export otherwise it's the empty token otherwise.

Acknowledgements

Claude Sonnet 5 helped me come up with the implementation strategy: https://claude.ai/share/0b35126c-294a-4f5d-844a-3b636381caa8, and @ednolan taught me about The Beman Project's CMake and modules infrastructure.

@copy-pr-bot

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ednolan

Copy link
Copy Markdown
Contributor

Looks good! Thoughts:

  1. This doesn't solve the hard problems that Claude flagged to me such as:

In stdexec, __sexpr — the type of every sender expression (just, let_value, and the sender sendosio::read returns) — is wrapped in
an unnamed namespace (__basic_sender.hpp:344-441), and its template argument is a lambda appearing in a default template argument (STDEXEC_SEXPR_DESCRIPTOR_FN, line 50). Both constructs produce a distinct type per translation unit. That's an intentional stdexec design
choice (it turns silent cross-TU ODR violations into hard errors), and it's fine when everything is header-only — but it's poison for the exemplar model, where stdexec gets included in the global module fragment of sendosio.cppm and its types leak into the module's
exported API.
But from the commit message, that seems intentional, and the idea is to use this as a starting point from which you can go through and fix those problems separately.

  1. In the exemplar-style Beman libraries (which not every Beman library currently uses), instead of a mechanism like STDEXEC_MODULE_EXPORT, we just give up on granular exporting and do what would be analogous to:
export {
#include <stdexec/execution.hpp>
}

Either approach is fine; the export-everything approach is less work but is sloppier. I imagine that your Claude may have come up with a reason to prefer the STDEXEC_MODULE_EXPORT approach.

  1. For this part:
# TODO: this is broken in the modules build
if(STDEXEC_INSTALL)

The place where this logic lives in the Beman project is this file, if you want to refer to it in the future to try to adapt its logic.

@ispeters

ispeters commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Looks good! Thoughts:

Thanks for looking!

  1. This doesn't solve the hard problems that Claude flagged to me such as:

In stdexec, __sexpr — the type of every sender expression (just, let_value, and the sender sendosio::read returns) — is wrapped in
an unnamed namespace (__basic_sender.hpp:344-441), and its template argument is a lambda appearing in a default template argument (STDEXEC_SEXPR_DESCRIPTOR_FN, line 50). Both constructs produce a distinct type per translation unit. That's an intentional stdexec design
choice (it turns silent cross-TU ODR violations into hard errors), and it's fine when everything is header-only — but it's poison for the exemplar model, where stdexec gets included in the global module fragment of sendosio.cppm and its types leak into the module's
exported API.

But from the commit message, that seems intentional, and the idea is to use this as a starting point from which you can go through and fix those problems separately.

Yeah, this is intentional. I don't think modules support is a high-priority need for stdexec so I want @ericniebler to have a chance to weigh in on the design, direction, etc. before I invest a whole lot of time in this. Once we agree on a plan, I'll extend things to support more of the library. Regarding that specific issue that Claude raised, I'm hoping that by defining the module from within the stdexec project rather than trying to create one non-invasively from outside the problems will be smaller. As it is, the stdexec patch you shared with me that did things like name previously-anonymous types and namespaces isn't necessary in this diff. I'm hoping that's a sign that it's OK for stdexec.cppm to define and use things with static linkage as long as those things are not exported. We'll see.

  1. In the exemplar-style Beman libraries (which not every Beman library currently uses), instead of a mechanism like STDEXEC_MODULE_EXPORT, we just give up on granular exporting and do what would be analogous to:
export {
#include <stdexec/execution.hpp>
}

Either approach is fine; the export-everything approach is less work but is sloppier. I imagine that your Claude may have come up with a reason to prefer the STDEXEC_MODULE_EXPORT approach.

I came up with the idea to try this approach in hopes that it would allow me to keep the various anonymous things in stdexec anonymous as long as they remain reachable-but-not-visible (I think that's the modules term for things declared in the module definition unit but not exported from it). When I asked Claude if it was a viable strategy, it claimed that it's the strategy Microsoft took in modularizing their stdlib, which I take as a good sign.

  1. For this part:
# TODO: this is broken in the modules build
if(STDEXEC_INSTALL)

The place where this logic lives in the Beman project is this file, if you want to refer to it in the future to try to adapt its logic.

Thanks! I'll take a look.

@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test 3a4ccc4

@ispetersispeters mentioned this pull request Jul 24, 2026
@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test 782a69d

-DCMAKE_CXX_STANDARD:STRING=${{ matrix.cxxstd }} \
-DCMAKE_CXX_EXTENSIONS:BOOL=OFF \
-DSTDEXEC_BUILD_TESTS:BOOL=ON \
-DSTDEXEC_BUILD_EXAMPLES:BOOL=${{ !contains(matrix.name, 'modules') }} \

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

As of this commit, the examples don't build with modules enabled, so disable them for now.

-DSTDEXEC_BUILD_TESTS:BOOL=ON \
-DSTDEXEC_BUILD_EXAMPLES:BOOL=${{ !contains(matrix.name, 'modules') }} \
-DSTDEXEC_BUILD_MODULES:BOOL=${{ contains(matrix.name, 'modules') }} \
-DCMAKE_CXX_STDLIB_MODULES_JSON=/usr/lib/llvm-22/lib/libc++.modules.json \

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is clearly wrong in the long term, but I think it'll unblock CI for now.

@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test f98255a

This diff doesn't build, but it doesn't build for a good reason: it's
successfully invoking the "build with modules" machinery, which is
exposing implementation gaps.
It looks like the next step is to start updating `stdexec` headers that
include std headers to conditionally rely on `import std` instead, at
least when `STDEXEC_IN_MODULE_PURVIEW` is defined.
This diff doesn't build for a very exciting reson: the new modules-only
test shows that `stdexec::just_t` hasn't been exported from the module.
This diff makes both the modules and non-modules builds pass.
The tests pass in both builds, too!
Locally, this fixes the modules build so I hope CI will work now, too.
I'm not confident I've made the correct change; I suspect I should be
using `rapids_export` somehow, but I don't know how.
 * Tell CMake where the modules.json file is for Clang 22
* Add some std includes to `stdexec.cppm`
This diff disables building the stdexec examples when running the
modules build in CI.
In modules mode, this diff disables the tests that don't build.
@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test a0e87eb

Leave some notes on how to not break the modularized build.
@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test 8cea155

@ispeters

Copy link
Copy Markdown
ContributorAuthor

Before you land this, make sure I haven't disabled the test.exec tests, etc. in the non-modules builds. I think I may have :(

Turns out CMake doesn't understand `if(!FOO)`; the correct syntax is
`if(NOT FOO)`. This diff re-includes the expected build targets when
building a non-modular build.
The module-scanning is pure overhead in non-modular builds.
@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test eb81750

@ericniebler
ericniebler merged commit f0e8ae6 into NVIDIA:mainJul 27, 2026
38 checks passed
@ispeters
ispeters deleted the modularize branch July 28, 2026 02:01
ispeters added a commit to ispeters/sendosio that referenced this pull request Jul 28, 2026
As of NVIDIA/stdexec#2138 and NVIDIA/stdexec#2151, stdexec ships a
`stdexec` module that works for lots of cases. This diff makes Sendosio
consume the `stdexec` module when building as a module, and some of the
tests build, and all of those pass.
ispeters added a commit to ispeters/sendosio that referenced this pull request Jul 28, 2026
As of NVIDIA/stdexec#2138 and NVIDIA/stdexec#2151, stdexec ships a
`stdexec` module that works for lots of cases. This diff makes Sendosio
consume the `stdexec` module when building as a module, and some of the
tests build, and all of those pass.
bjornpagen added a commit to bjornpagen/stdexec that referenced this pull request Aug 9, 2026
f0e8ae6 ("Create a stdexec module (NVIDIA#2138)") changed the ARM branch of
__spin_loop_pause from `static` to `inline` so the function has external
linkage in the module purview. But STDEXEC_ATTRIBUTE(always_inline)
already expands to `__attribute__((__always_inline__, ...)) inline`, so
the ARM declaration became `inline inline` -- ill-formed per
[dcl.spec.general]/1 (a decl-specifier shall appear at most once in the
sequence).
GCC rejects this with a hard error ("duplicate 'inline'"), which breaks
every GCC build on arm/aarch64 since that commit. Clang only warns under
-Wduplicate-decl-specifier (not in the CI warning set), which is why the
arm64 CI runners did not catch it.
Dropping the explicit `inline` keeps the linkage intent of NVIDIA#2138: the
attribute macro still supplies `inline`, so the function keeps external
linkage and stays module-purview-safe.
Verified on aarch64-apple-darwin: GCC 16.1 (-std=c++26, with and without
-fno-exceptions) and Clang 22 both compile the header cleanly after this
change; at HEAD, GCC errors and Clang warns.
Note: the x86 and fallback branches still spell `static` (expanding to
`inline static`, i.e. internal linkage); if NVIDIA#2138's purview-linkage
concern applies to them as well they may want the same treatment. Left
untouched here to keep this a pure build fix.
bjornpagen added a commit to bjornpagen/stdexec that referenced this pull request Aug 9, 2026
…64 build
f0e8ae6 ("Create a stdexec module (NVIDIA#2138)") changed the ARM branch of
__spin_loop_pause from `static` to `inline` so the definition is safe to
include in the module purview. But STDEXEC_ATTRIBUTE(always_inline)
already ends in `inline` on GCC and Clang
(STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE in __config.hpp expands to
`__attribute__((__always_inline__, __artificial__)) inline`; Clang adds
`__nodebug__`), so the declaration became `inline inline` -- ill-formed
per [dcl.spec.general]/2 ("At most one of each of the decl-specifiers
friend, typedef, or inline shall appear in a decl-specifier-seq").
Both compilers diagnose it, as conformance requires: GCC rejects with a
hard error --
include/stdexec/__detail/__spin_loop_pause.hpp:42:36: error: duplicate 'inline'
-- which breaks every GCC build on arm/aarch64 since that commit, while
Clang accepts with an on-by-default -Wduplicate-decl-specifier warning.
CI stayed green because no CI leg compiles the ARM branch at all: the
Linux and Windows runners are x86_64, and the macOS jobs run on
macos-26-large runners, which are Intel.
Two changes, which together make ALWAYS_INLINE guarantee inline linkage
on every compiler:
* drop the explicit `inline` at the ARM use site (the macro supplies
it on GCC/Clang, and MSVC's `__forceinline` implies it -- the HEAD
spelling `__forceinline inline` likely draws C4141 there as well);
* give the fallback branch of STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE a
plain `inline`. Compilers that take the fallback branch (nvcc,
NVHPC, and other EDG front ends define neither STDEXEC_CLANG() nor
STDEXEC_GCC()) previously expanded the macro to nothing, so the
explicit `inline` at the ARM site was the only thing keeping this
header-defined function ODR-safe for them; the fallback `inline`
preserves exactly that. (Read-verified against the detection chain
in __config.hpp; not run against nvcc/NVHPC.)
Site survey: the ARM site was the only
`STDEXEC_ATTRIBUTE(always_inline) inline` pairing among all 81 uses of
the attribute. The two `STDEXEC_ATTRIBUTE(always_inline) static` sites
(the x86 and fallback-architecture branches of this same function)
become `inline static` on GCC/Clang, which is well-formed and still
internal linkage.
Verified on aarch64-apple-darwin: GCC 16.1 (-std=c++26, with and
without -fno-exceptions) and Clang 22 compile the header cleanly after
this change; at HEAD, GCC errors and Clang warns. With this fix the
full default test suite builds and passes with GCC 16.1 on this host
(963/968; the 5 relacy binaries fail identically with and without this
change -- environmental on aarch64-apple-darwin -- and cannot be built
at all at HEAD, where no test TU that reaches this header compiles).
The -fno-exceptions test configuration from the CI matrix also builds
and passes (879/879).
Note: the x86 and fallback-architecture branches still spell `static`;
if NVIDIA#2138's module-purview concern applies to them as well they may want
the same treatment. Left untouched here to keep this a pure build fix.
bjornpagen added a commit to bjornpagen/stdexec that referenced this pull request Aug 9, 2026
…64 build
f0e8ae6 ("Create a stdexec module (NVIDIA#2138)") changed the ARM branch of
__spin_loop_pause from `static` to `inline` so the definition is safe to
include in the module purview. But STDEXEC_ATTRIBUTE(always_inline)
already ends in `inline` on GCC and Clang
(STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE in __config.hpp expands to
`__attribute__((__always_inline__, __artificial__)) inline`; Clang adds
`__nodebug__`), so the declaration became `inline inline` -- ill-formed
per [dcl.spec.general]/2 ("At most one of each of the decl-specifiers
friend, typedef, or inline shall appear in a decl-specifier-seq").
Both compilers diagnose it, as conformance requires: GCC rejects with a
hard error --
include/stdexec/__detail/__spin_loop_pause.hpp:42:36: error: duplicate 'inline'
-- which breaks every GCC build on arm/aarch64 since that commit, while
Clang accepts with an on-by-default -Wduplicate-decl-specifier warning.
CI stayed green because no CI leg compiles the ARM branch at all: the
Linux and Windows runners are x86_64, and the macOS jobs run on
macos-26-large runners, which are Intel.
Two changes, which together make ALWAYS_INLINE guarantee inline linkage
on every compiler:
* drop the explicit `inline` at the ARM use site (the macro supplies
it on GCC/Clang, and MSVC's `__forceinline` implies it -- the HEAD
spelling `__forceinline inline` likely draws C4141 there as well);
* give the fallback branch of STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE a
plain `inline`. Compilers that take the fallback branch (nvcc,
NVHPC, and other EDG front ends define neither STDEXEC_CLANG() nor
STDEXEC_GCC()) previously expanded the macro to nothing, so the
explicit `inline` at the ARM site was the only thing keeping this
header-defined function ODR-safe for them; the fallback `inline`
preserves exactly that. (Read-verified against the detection chain
in __config.hpp; not run against nvcc/NVHPC.)
Site survey: the ARM site was the only
`STDEXEC_ATTRIBUTE(always_inline) inline` pairing among all 81 uses of
the attribute. The two `STDEXEC_ATTRIBUTE(always_inline) static` sites
(the x86 and fallback-architecture branches of this same function)
become `inline static` on GCC/Clang, which is well-formed and still
internal linkage.
Verified on aarch64-apple-darwin: GCC 16.1 (-std=c++26, with and
without -fno-exceptions) and Clang 22 compile the header cleanly after
this change; at HEAD, GCC errors and Clang warns. With this fix the
full default test suite builds and passes with GCC 16.1 on this host
(963/968; the 5 relacy binaries fail identically with and without this
change -- environmental on aarch64-apple-darwin -- and cannot be built
at all at HEAD, where no test TU that reaches this header compiles).
The -fno-exceptions test configuration from the CI matrix also builds
and passes (879/879).
Note: the x86 and fallback-architecture branches still spell `static`;
if NVIDIA#2138's module-purview concern applies to them as well they may want
the same treatment. Left untouched here to keep this a pure build fix.
bjornpagen added a commit to bjornpagen/stdexec that referenced this pull request Aug 9, 2026
…64 build
f0e8ae6 ("Create a stdexec module (NVIDIA#2138)") changed the ARM branch of
__spin_loop_pause from `static` to `inline` so the definition is safe to
include in the module purview. But STDEXEC_ATTRIBUTE(always_inline)
already ends in `inline` on GCC and Clang
(STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE in __config.hpp expands to
`__attribute__((__always_inline__, __artificial__)) inline`; Clang adds
`__nodebug__`), so the declaration became `inline inline` -- ill-formed
per [dcl.spec.general]/2 ("At most one of each of the decl-specifiers
friend, typedef, or inline shall appear in a decl-specifier-seq").
Both compilers diagnose it, as conformance requires: GCC rejects with a
hard error --
include/stdexec/__detail/__spin_loop_pause.hpp:42:36: error: duplicate 'inline'
-- which breaks every GCC build on arm/aarch64 since that commit, while
Clang accepts with an on-by-default -Wduplicate-decl-specifier warning.
CI stayed green because no CI leg compiles the ARM branch at all: the
Linux and Windows runners are x86_64, and the macOS jobs run on
macos-26-large runners, which are Intel.
Two changes, which together make ALWAYS_INLINE guarantee inline linkage
on every compiler:
* drop the explicit `inline` at the ARM use site (the macro supplies
it on GCC/Clang, and MSVC's `__forceinline` implies it -- the HEAD
spelling `__forceinline inline` likely draws C4141 there as well);
* give the fallback branch of STDEXEC_ATTRIBUTE_CASE_ALWAYS_INLINE a
plain `inline`. Compilers that take the fallback branch (nvcc,
NVHPC, and other EDG front ends define neither STDEXEC_CLANG() nor
STDEXEC_GCC()) previously expanded the macro to nothing, so the
explicit `inline` at the ARM site was the only thing keeping this
header-defined function ODR-safe for them; the fallback `inline`
preserves exactly that. (Read-verified against the detection chain
in __config.hpp; not run against nvcc/NVHPC.)
Site survey: the ARM site was the only
`STDEXEC_ATTRIBUTE(always_inline) inline` pairing among all 81 uses of
the attribute. The two `STDEXEC_ATTRIBUTE(always_inline) static` sites
(the x86 and fallback-architecture branches of this same function)
become `inline static` on GCC/Clang, which is well-formed and still
internal linkage.
Verified on aarch64-apple-darwin: GCC 16.1 (-std=c++26, with and
without -fno-exceptions) and Clang 22 compile the header cleanly after
this change; at HEAD, GCC errors and Clang warns. With this fix the
full default test suite builds and passes with GCC 16.1 on this host
(963/968; the 5 relacy binaries fail identically with and without this
change -- environmental on aarch64-apple-darwin -- and cannot be built
at all at HEAD, where no test TU that reaches this header compiles).
The -fno-exceptions test configuration from the CI matrix also builds
and passes (879/879).
Note: the x86 and fallback-architecture branches still spell `static`;
if NVIDIA#2138's module-purview concern applies to them as well they may want
the same treatment. Left untouched here to keep this a pure build fix.
Sign up for freeto 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.

3 participants

@ispeters@ednolan@ericniebler