https://github.com/torarnv created
https://github.com/llvm/llvm-project/pull/207515
Add a `requires feature-list { ... }` block form to module maps that makes the
wrapped module declarations conditionally exist based on language/target
features, instead of the hard-error / unimportable behavior of a member-level
`requires`.
When the feature list is unsatisfied, the wrapped modules are simply not
created: an `@import` finds no such module, and a direct `#include` of one of
their headers falls back to a plain textual include (the header never enters
the Headers map). This lets a header opt into being modular only under, e.g.,
`requires cplusplus` while remaining a normal textual include otherwise.
The block is lowered away at parse time onto a new `Guards` field on
`ModuleDecl`, keeping the parser independent of LangOpts/TargetInfo. The
consumer evaluates the guard once, at the top of `handleModuleDecl`, skipping
module creation when any feature is unmet; this covers eager and lazy loads,
submodules, and the include fallback for free. Nesting accumulates guards and
negation rides on the existing `!` feature syntax.
`extern module` inside a `requires` block is rejected for now.
The file-local `hasFeature()` is exposed as a public static
`Module::hasFeature()` for the consumer-side check.
Fixes #163965
From bcfac2946efebd93273334b1ff029ad9b4f56b01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= <[email protected]>
Date: Sat, 4 Jul 2026 15:15:29 +0200
Subject: [PATCH] [modulemap] Support non-hard requirements via `requires`
blocks
Add a `requires feature-list { ... }` block form to module maps that makes
the wrapped module declarations conditionally exist based on language/target
features, instead of the hard-error / unimportable behavior of a member-level
`requires`.
When the feature list is unsatisfied, the wrapped modules are simply not
created: an `@import` finds no such module, and a direct `#include` of one of
their headers falls back to a plain textual include (the header never enters
the Headers map). This lets a header opt into being modular only under, e.g.,
`requires cplusplus` while remaining a normal textual include otherwise.
The block is lowered away at parse time onto a new `Guards` field on
`ModuleDecl`, keeping the parser independent of LangOpts/TargetInfo. The
consumer evaluates the guard once, at the top of `handleModuleDecl`, skipping
module creation when any feature is unmet; this covers eager and lazy loads,
submodules, and the include fallback for free. Nesting accumulates guards and
negation rides on the existing `!` feature syntax.
`extern module` inside a `requires` block is rejected for now.
The file-local `hasFeature()` is exposed as a public static
`Module::hasFeature()` for the consumer-side check.
Fixes #163965
---
clang/docs/Modules.rst | 32 +++++-
clang/docs/ReleaseNotes.md | 5 +
clang/include/clang/Basic/Module.h | 5 +
clang/include/clang/Lex/ModuleMapFile.h | 7 ++
clang/lib/Basic/Module.cpp | 4 +-
clang/lib/Lex/ModuleMap.cpp | 13 +++
clang/lib/Lex/ModuleMapFile.cpp | 108 +++++++++++++++++++-
clang/test/Modules/requires-block-include.c | 30 ++++++
clang/test/Modules/requires-block.m | 72 +++++++++++++
clang/unittests/Lex/ModuleMapTest.cpp | 37 +++++++
10 files changed, 307 insertions(+), 6 deletions(-)
create mode 100644 clang/test/Modules/requires-block-include.c
create mode 100644 clang/test/Modules/requires-block.m
diff --git a/clang/docs/Modules.rst b/clang/docs/Modules.rst
index 379f62ebf548c..0c2b0d0e89518 100644
--- a/clang/docs/Modules.rst
+++ b/clang/docs/Modules.rst
@@ -467,7 +467,11 @@ A module map file consists of a series of module
declarations:
.. parsed-literal::
*module-map-file*:
- *module-declaration**
+ *top-level-declaration**
+
+ *top-level-declaration*:
+ *module-declaration*
+ *requires-block*
Within a module map file, modules are referred to by a *module-id*, which uses
periods to separate each part of a module's name:
@@ -514,6 +518,7 @@ Modules can have a number of different kinds of members,
each of which is descri
*module-member*:
*requires-declaration*
+ *requires-block*
*header-declaration*
*umbrella-dir-declaration*
*submodule-declaration*
@@ -543,6 +548,31 @@ A *requires-declaration* specifies the requirements that
an importing translatio
The requirements clause allows specific modules or submodules to specify that
they are only accessible with certain language dialects, platforms,
environments and target specific features. The feature list is a set of
identifiers, defined below. If any of the features is not available in a given
translation unit, that translation unit shall not import the module. When
building a module for use by a compilation, submodules requiring unavailable
features are ignored. The optional ``!`` indicates that a feature is
incompatible with the module.
+A *requires-declaration* is a "hard" requirement: the module is still created,
but it is marked unimportable when a feature is unsatisfied, so an attempt to
import it produces an error.
+
+Requires block
+~~~~~~~~~~~~~~
+A *requires-block* wraps one or more module declarations in a feature list.
Unlike a *requires-declaration*, the wrapped modules *conditionally exist*:
when a feature is unsatisfied the wrapped modules are simply not created,
rather than being created and marked unimportable.
+
+.. parsed-literal::
+
+ *requires-block*:
+ ``requires`` *feature-list* '{' *module-declaration** '}'
+
+The same *feature-list* grammar and features as a *requires-declaration*
apply. A *requires-block* may appear at the top level of a module map or as a
*module-member* (gating submodules), and blocks may be nested; a module is only
created when the features of every enclosing block are satisfied.
+
+Because an unsatisfied block skips module creation entirely, importing a
wrapped module yields an ordinary "module not found" (rather than "module is
unavailable"), and a direct ``#include`` of one of its headers falls back to a
plain textual include, since the header never enters the module's header map.
This is what allows a header to opt into being modular only under, e.g.,
``requires cplusplus`` while remaining a normal textual include otherwise.
+
+Only *module-declaration*\ s (and nested *requires-block*\ s) may appear
inside a *requires-block*. In particular, ``extern module`` is not permitted
inside a block. For example:
+
+.. parsed-literal::
+
+ requires cplusplus20 {
+ module m {
+ header "m.h"
+ }
+ }
+
The following features are defined:
altivec
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index b7142ecb072ff..2f5748d48a31c 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -349,6 +349,11 @@ latest release, please see the [Clang Web
Site](https://clang.llvm.org) or the
map source locations back to explicit instantiation sites.
- `typeid` on references and pointers of `final` types no longer emits a
vtable lookup at runtime.
+- Module maps now support wrapping module declarations in a `requires` block,
+ e.g. `requires cplusplus { module m { header "m.h" } }`. Unlike a
+ member-level `requires`, an unsatisfied block causes the wrapped modules to
+ not be created at all, so a direct `#include` of one of their headers falls
+ back to a plain textual include instead of producing an error. (#GH163965)
- Updated support for Unicode from 15.1 to 18.0.
- Linux and Windows toolchains now support Clang multilibs using
`-fmultilib-flag=`.
diff --git a/clang/include/clang/Basic/Module.h
b/clang/include/clang/Basic/Module.h
index 453ad2b7e7600..f0ffc18399a70 100644
--- a/clang/include/clang/Basic/Module.h
+++ b/clang/include/clang/Basic/Module.h
@@ -1027,6 +1027,11 @@ class alignas(8) Module {
const LangOptions &LangOpts,
const TargetInfo &Target);
+ /// Determine whether a translation unit built using the current language
+ /// options has the given feature.
+ static bool hasFeature(StringRef Feature, const LangOptions &LangOpts,
+ const TargetInfo &Target);
+
/// Mark this module and all of its submodules as unavailable.
void markUnavailable(bool Unimportable);
diff --git a/clang/include/clang/Lex/ModuleMapFile.h
b/clang/include/clang/Lex/ModuleMapFile.h
index 59389cd85a928..ec56ff171b64b 100644
--- a/clang/include/clang/Lex/ModuleMapFile.h
+++ b/clang/include/clang/Lex/ModuleMapFile.h
@@ -73,6 +73,13 @@ struct ModuleDecl {
ModuleAttributes Attrs;
std::vector<Decl> Decls;
+ /// Features from enclosing `requires` blocks that must all be satisfied for
+ /// this module to exist. Unlike a member-level `requires` (which creates the
+ /// module and marks it unimportable), an unsatisfied guard means the module
+ /// is never created. Nesting accumulates the features of all enclosing
+ /// blocks; negation rides on RequiresFeature::RequiredState.
+ std::vector<RequiresFeature> Guards;
+
LLVM_PREFERRED_TYPE(bool)
unsigned Explicit : 1;
LLVM_PREFERRED_TYPE(bool)
diff --git a/clang/lib/Basic/Module.cpp b/clang/lib/Basic/Module.cpp
index 3e2f66f27a518..5d466728ddc60 100644
--- a/clang/lib/Basic/Module.cpp
+++ b/clang/lib/Basic/Module.cpp
@@ -92,8 +92,8 @@ static bool isPlatformEnvironment(const TargetInfo &Target,
StringRef Feature) {
/// Determine whether a translation unit built using the current
/// language options has the given feature.
-static bool hasFeature(StringRef Feature, const LangOptions &LangOpts,
- const TargetInfo &Target) {
+bool Module::hasFeature(StringRef Feature, const LangOptions &LangOpts,
+ const TargetInfo &Target) {
bool HasFeature = llvm::StringSwitch<bool>(Feature)
.Case("altivec", LangOpts.AltiVec)
.Case("blocks", LangOpts.Blocks)
diff --git a/clang/lib/Lex/ModuleMap.cpp b/clang/lib/Lex/ModuleMap.cpp
index 6c07386f89010..a9ebd3c538b9a 100644
--- a/clang/lib/Lex/ModuleMap.cpp
+++ b/clang/lib/Lex/ModuleMap.cpp
@@ -1833,6 +1833,19 @@ void
ModuleMapLoader::diagnosePrivateModules(SourceLocation StartLoc) {
}
void ModuleMapLoader::handleModuleDecl(const modulemap::ModuleDecl &MD) {
+ // A `requires` block makes the wrapped modules conditionally exist: if any
+ // guarding feature is unsatisfied for this compilation, the module is simply
+ // not created. This differs from a member-level `requires`, which creates
the
+ // module and marks it unimportable. Skipping creation entirely means an
+ // `@import` finds no such module and a direct `#include` of one of its
+ // headers falls back to a plain textual include (the header never enters the
+ // Headers map). Guards accumulate from nested blocks; all must hold.
+ for (const modulemap::RequiresFeature &RF : MD.Guards) {
+ if (Module::hasFeature(RF.Feature, Map.LangOpts, *Map.Target) !=
+ RF.RequiredState)
+ return;
+ }
+
if (MD.Id.front().first == "*")
return handleInferredModuleDecl(MD);
diff --git a/clang/lib/Lex/ModuleMapFile.cpp b/clang/lib/Lex/ModuleMapFile.cpp
index 4ca33cb86ddd3..4560ca41ae7b8 100644
--- a/clang/lib/Lex/ModuleMapFile.cpp
+++ b/clang/lib/Lex/ModuleMapFile.cpp
@@ -113,6 +113,9 @@ struct ModuleMapFileParser {
std::optional<ExportAsDecl> parseExportAsDecl();
std::optional<UseDecl> parseUseDecl();
std::optional<RequiresDecl> parseRequiresDecl();
+ template <typename Container>
+ bool parseRequiresBlock(Container &Parent, ArrayRef<RequiresFeature> Guards,
+ bool TopLevel);
std::optional<HeaderDecl> parseHeaderDecl(MMToken::TokenKind LeadingToken,
SourceLocation LeadingLoc);
std::optional<ExcludeDecl> parseExcludeDecl(clang::SourceLocation
LeadingLoc);
@@ -197,6 +200,21 @@ bool ModuleMapFileParser::parseTopLevelDecls() {
MMF.Decls.push_back(std::move(*MD));
break;
}
+ case MMToken::RequiresKeyword: {
+ // A `requires` block conditionally provides the wrapped modules based on
+ // language/target features. The wrapped modules are flattened into the
+ // top-level decl list, guarded by the block's feature list. A bare
+ // `requires` (no block) remains an error at the top level.
+ std::optional<RequiresDecl> RD = parseRequiresDecl();
+ if (RD && Tok.is(MMToken::LBrace)) {
+ parseRequiresBlock(MMF.Decls, RD->Features, /*TopLevel=*/true);
+ } else {
+ Diags.Report(RD ? RD->Location : Tok.getLocation(),
+ diag::err_mmap_expected_module);
+ HadError = true;
+ }
+ break;
+ }
case MMToken::Comma:
case MMToken::ConfigMacros:
case MMToken::Conflict:
@@ -213,7 +231,6 @@ bool ModuleMapFileParser::parseTopLevelDecls() {
case MMToken::PrivateKeyword:
case MMToken::RBrace:
case MMToken::RSquare:
- case MMToken::RequiresKeyword:
case MMToken::Star:
case MMToken::StringLiteral:
case MMToken::IntegerLiteral:
@@ -372,9 +389,19 @@ std::optional<ModuleDecl>
ModuleMapFileParser::parseModuleDecl(bool TopLevel) {
SubDecl = parseUseDecl();
break;
- case MMToken::RequiresKeyword:
- SubDecl = parseRequiresDecl();
+ case MMToken::RequiresKeyword: {
+ std::optional<RequiresDecl> RD = parseRequiresDecl();
+ if (RD && Tok.is(MMToken::LBrace)) {
+ // A `requires` block gating submodules: flatten the wrapped modules
+ // into this module's decl list, guarded by the block's features.
+ parseRequiresBlock(MDecl.Decls, RD->Features, /*TopLevel=*/false);
+ } else if (RD) {
+ // Member-level `requires`: kept as-is (creates the module and marks it
+ // unimportable when unsatisfied).
+ SubDecl = std::move(*RD);
+ }
break;
+ }
case MMToken::TextualKeyword:
SubDecl = parseHeaderDecl(MMToken::TextualKeyword, consumeToken());
@@ -668,6 +695,81 @@ std::optional<RequiresDecl>
ModuleMapFileParser::parseRequiresDecl() {
return std::move(RD);
}
+/// Parse the body of a `requires` block.
+///
+/// requires-block:
+/// 'requires' feature-list '{' module-declaration* '}'
+///
+/// The feature-list has already been parsed by the caller; the current token
is
+/// the opening brace. Each wrapped module declaration is flattened directly
+/// into \p Parent, stamped with \p Guards (the accumulated feature lists of
all
+/// enclosing blocks) so the consumer can decide whether the module exists.
+/// Nested `requires` blocks accumulate their features onto \p Guards. Only
+/// module declarations may appear in a block; anything else is an error.
+template <typename Container>
+bool ModuleMapFileParser::parseRequiresBlock(Container &Parent,
+ ArrayRef<RequiresFeature> Guards,
+ bool TopLevel) {
+ assert(Tok.is(MMToken::LBrace));
+ SourceLocation LBraceLoc = consumeToken();
+
+ bool Done = false;
+ do {
+ switch (Tok.Kind) {
+ case MMToken::EndOfFile:
+ case MMToken::RBrace:
+ Done = true;
+ break;
+
+ case MMToken::ExplicitKeyword:
+ case MMToken::FrameworkKeyword:
+ case MMToken::ModuleKeyword: {
+ std::optional<ModuleDecl> MD = parseModuleDecl(TopLevel);
+ if (MD) {
+ MD->Guards.assign(Guards.begin(), Guards.end());
+ Parent.push_back(std::move(*MD));
+ }
+ break;
+ }
+
+ case MMToken::RequiresKeyword: {
+ std::optional<RequiresDecl> RD = parseRequiresDecl();
+ if (!RD)
+ break;
+ if (!Tok.is(MMToken::LBrace)) {
+ // A member-level `requires` has no meaning inside a `requires` block;
+ // only module declarations may appear here.
+ Diags.Report(RD->Location, diag::err_mmap_expected_module);
+ HadError = true;
+ break;
+ }
+ std::vector<RequiresFeature> Nested(Guards.begin(), Guards.end());
+ Nested.insert(Nested.end(), RD->Features.begin(), RD->Features.end());
+ parseRequiresBlock(Parent, Nested, TopLevel);
+ break;
+ }
+
+ default:
+ // Notably this rejects `extern module` inside a `requires` block, which
+ // is intentionally unsupported (its declaration does not flow through
the
+ // guard check on the consumer side).
+ Diags.Report(Tok.getLocation(), diag::err_mmap_expected_module);
+ HadError = true;
+ consumeToken();
+ break;
+ }
+ } while (!Done);
+
+ if (Tok.is(MMToken::RBrace))
+ consumeToken();
+ else {
+ Diags.Report(Tok.getLocation(), diag::err_mmap_expected_rbrace);
+ Diags.Report(LBraceLoc, diag::note_mmap_lbrace_match);
+ HadError = true;
+ }
+ return HadError;
+}
+
/// Parse a header declaration.
///
/// header-declaration:
diff --git a/clang/test/Modules/requires-block-include.c
b/clang/test/Modules/requires-block-include.c
new file mode 100644
index 0000000000000..491e80a6b9ade
--- /dev/null
+++ b/clang/test/Modules/requires-block-include.c
@@ -0,0 +1,30 @@
+// Tests that a header which only belongs to a module under an unsatisfied
+// `requires` block falls back to a plain textual #include, rather than being
+// translated into a module import. This is the motivating use case: a header
+// can opt into being modular only under, e.g., `requires cplusplus`, while
+// remaining an ordinary textual include otherwise.
+//
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+//
+// Compiled as C: the 'cxxonly' module is guarded by `requires cplusplus` and
so
+// does not exist. Its header never enters the Headers map, so the #include is
a
+// plain textual include (no include-to-import translation) and succeeds.
+// -Rmodule-include-translation would emit a remark if any translation
happened;
+// expected-no-diagnostics asserts none does.
+// RUN: %clang_cc1 -x c -std=c99 -fmodules -fimplicit-module-maps \
+// RUN: -fmodules-cache-path=%t/cache -I %t -Rmodule-include-translation \
+// RUN: -verify %t/use.c
+
+//--- module.modulemap
+requires cplusplus {
+ module cxxonly { header "cxxonly.h" }
+}
+
+//--- cxxonly.h
+#define CXXONLY_MACRO 42
+
+//--- use.c
+// expected-no-diagnostics
+#include "cxxonly.h"
+int x = CXXONLY_MACRO;
diff --git a/clang/test/Modules/requires-block.m
b/clang/test/Modules/requires-block.m
new file mode 100644
index 0000000000000..c976a4f66ed0e
--- /dev/null
+++ b/clang/test/Modules/requires-block.m
@@ -0,0 +1,72 @@
+// Tests the `requires` block form of module map declarations (as opposed to a
+// member-level `requires`): when a block's feature list is unsatisfied, the
+// wrapped modules are not created at all, so importing one yields an ordinary
+// "module not found" rather than "module is unavailable".
+//
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+//
+// Compiled as Objective-C (no C++): the C++-guarded modules do not exist,
+// while the !cplusplus-guarded module does.
+// RUN: %clang_cc1 -x objective-c -fmodules -fimplicit-module-maps \
+// RUN: -fmodules-cache-path=%t/cache -I %t %t/test.m -verify
+//
+// Compiled as Objective-C++20: the C++-guarded modules exist, while the
+// !cplusplus-guarded module does not.
+// RUN: %clang_cc1 -x objective-c++ -std=c++20 -fmodules
-fimplicit-module-maps \
+// RUN: -fmodules-cache-path=%t/cache -I %t %t/test.m -verify
+
+//--- module.modulemap
+requires cplusplus {
+ module CXXOnly { header "cxxonly.h" }
+}
+
+// Negation: only exists when C++ is *not* available.
+requires !cplusplus {
+ module NotCXX { header "notcxx.h" }
+}
+
+// Multi-feature list: every feature must hold.
+requires cplusplus, cplusplus20 {
+ module CXX20 { header "cxx20.h" }
+}
+
+// Nested blocks accumulate their features.
+requires cplusplus {
+ requires cplusplus20 {
+ module Nested { header "nested.h" }
+ }
+}
+
+// A block inside a module body gates a submodule; the enclosing module is
+// unguarded and always exists.
+module Outer {
+ header "outer.h"
+ requires cplusplus {
+ module Inner { header "inner.h" }
+ }
+}
+
+//--- cxxonly.h
+//--- notcxx.h
+//--- cxx20.h
+//--- nested.h
+//--- outer.h
+//--- inner.h
+
+//--- test.m
+@import Outer; // OK in both dialects.
+
+#ifdef __cplusplus
+@import CXXOnly; // OK
+@import CXX20; // OK
+@import Nested; // OK
+@import Outer.Inner; // OK
+// The negation block is inactive under C++, so its module was never created.
+@import NotCXX; // expected-error {{module 'NotCXX' not found}}
+#else
+@import NotCXX; // OK
+// The C++-guarded blocks are inactive, so their modules were never created.
+// (err_module_not_found is fatal, so this must be the last import.)
+@import CXXOnly; // expected-error {{module 'CXXOnly' not found}}
+#endif
diff --git a/clang/unittests/Lex/ModuleMapTest.cpp
b/clang/unittests/Lex/ModuleMapTest.cpp
index cbbcbd594faa5..f5667bde8a757 100644
--- a/clang/unittests/Lex/ModuleMapTest.cpp
+++ b/clang/unittests/Lex/ModuleMapTest.cpp
@@ -127,5 +127,42 @@ extern module C "../root/C.cppmap"
ASSERT_TRUE(Seen.contains("C.cppmap"));
}
+// A `requires` block makes the wrapped modules conditionally exist based on
+// language/target features. Unlike a member-level `requires` (which creates
the
+// module and marks it unimportable), an unsatisfied block means the module is
+// never created. The fixture uses default (C, non-C++) LangOpts.
+TEST_F(ModuleMapTest, RequiresBlockGatesModuleCreation) {
+ addFile("/root/map.modulemap", R"(
+requires cplusplus {
+ module CXXOnly {}
+}
+requires !cplusplus {
+ module NotCXX {}
+}
+requires cplusplus, cplusplus20 {
+ module CXX20 {}
+}
+module Outer {
+ requires cplusplus {
+ module Inner {}
+ }
+}
+ )");
+
+ ASSERT_FALSE(loadRoot("/root/map.modulemap"));
+
+ // C++-guarded blocks are inactive under C, so their modules don't exist.
+ EXPECT_EQ(Map.findModule("CXXOnly"), nullptr);
+ EXPECT_EQ(Map.findModule("CXX20"), nullptr);
+
+ // The negation block is active under C, so its module exists.
+ EXPECT_NE(Map.findModule("NotCXX"), nullptr);
+
+ // The unguarded enclosing module exists, but its guarded submodule does not.
+ Module *Outer = Map.findModule("Outer");
+ ASSERT_NE(Outer, nullptr);
+ EXPECT_FALSE(Outer->findSubmodule("Inner"));
+}
+
} // namespace
} // namespace clang
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits