From edf59ee1e3e727382839b001835501d70b1c6e2b Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sat, 11 Jul 2026 06:59:21 +0800 Subject: [PATCH 1/9] wip(modules): C++20 module migration work-in-progress Co-Authored-By: Claude Opus 4.8 (1M context) --- include/smallstring.hpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 48020e4..5af690c 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -13,7 +13,6 @@ */ #pragma once -#include #include #include @@ -31,6 +30,11 @@ #include #include +#ifndef STDB_FMT_IMPORTED +#define STDB_FMT_IMPORTED 1 +import fmt; +#endif + namespace small { #ifndef Assert #define Assert(condition, message) assert((condition) && (message)) @@ -5450,13 +5454,19 @@ template class, class T, class A, bool N, float G> class Buffer, template class Core, class Traits, class Allocator, bool NullTerminated, float Growth> struct fmt::formatter> - : fmt::formatter { - using fmt::formatter::parse; + constexpr auto parse(fmt::format_parse_context& ctx) -> fmt::format_parse_context::iterator { + auto it = ctx.begin(); + const auto end = ctx.end(); + while (it != end && *it != '}') { + ++it; + } + return it; + } auto format(const small::basic_small_string& str, fmt::format_context& ctx) const noexcept { - return fmt::formatter::format({str.data(), str.size()}, ctx); + return fmt::format_to(ctx.out(), "{}", std::string_view{str.data(), str.size()}); } }; From 949aad9e93373dbd371c186ca46e0d497744b536 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 07:34:22 +0800 Subject: [PATCH 2/9] fmt: only import the fmt module when the consumer asked for it smallstring.hpp had an unconditional `import fmt;`, so the header could not be used at all in a build where fmt is an ordinary library rather than a C++20 module: "fatal error: module 'fmt' not found". Guard it with STDB_USE_FMT_MODULE and fall back to , the same way arena already does. --- include/smallstring.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 5af690c..c5523fa 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -30,10 +30,17 @@ #include #include +// fmt comes in as a C++20 module only when the consumer asked for it (STDB_USE_FMT_MODULE, off by +// default). Unconditionally importing it made this header unusable in any build where fmt is an +// ordinary library -- "fatal error: module 'fmt' not found". +#if defined(STDB_USE_FMT_MODULE) #ifndef STDB_FMT_IMPORTED #define STDB_FMT_IMPORTED 1 import fmt; #endif +#else +#include +#endif namespace small { #ifndef Assert From 9dfff7ab7899877fdf31a2af46d0c6a1c2a61d47 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 08:34:27 +0800 Subject: [PATCH 3/9] modules: provide a `smallstring` C++20 module smallstring.hpp is 5.6k lines and reaches nearly every translation unit in ClapDB through PlainString (= small::small_byte_string). Textual, every one of them re-parses it. - smallstring.cppm attaches the header's declarations to module `smallstring`. The header stays the single source of truth; the .cppm only wraps it. - smallstring.hpp gets the standard shim: outside the module the include degrades to `import smallstring;`, so its declarations are never both module-attached and global-module. Gated on SMALLSTRING_USE_MODULE, so a consumer that does not build with modules is unaffected. - The Assert() hook survives modularisation. smallstring calls Assert(), not assert(), precisely so the consumer can substitute its own -- and modules break the old way of doing that: the header is an `import` at the consumer, and smallstring's macros are fixed when its *interface* is compiled, not at the include site. So the interface takes a SMALLSTRING_PRELUDE hook; point it at a header that #define-s Assert. Without it, behaviour is unchanged (plain assert()). - std comes in as `import std.compat`, NOT as textual libstdc++ headers in the global module fragment. Textual there bakes libstdc++'s declarations into the BMI as global-module entities; any consumer that then reads textually -- directly or through -- re-declares basic_string.tcc's explicit instantiations on top of them, and clang rejects it ("explicit instantiation of 'getline' does not refer to a function template"). It reaches consumers that never name smallstring, too, because a module which imports smallstring carries those declarations onward in its own BMI. has to stay textual -- the header specialises fmt::formatter -- and that one is fine. - The unnamed namespace holding kMinAlignSize / AlignUpTo becomes `small::detail`. Entities in an unnamed namespace have internal linkage, and a module interface cannot reference an internal-linkage entity from an exported inline function or template. --- include/smallstring.hpp | 30 ++++++++++++++++++++++++++++-- smallstring.cppm | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 smallstring.cppm diff --git a/include/smallstring.hpp b/include/smallstring.hpp index c5523fa..79a7ec8 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -13,6 +13,22 @@ */ #pragma once +// Owned by module `smallstring` when the consumer builds with modules (SMALLSTRING_USE_MODULE). +// +// Outside that module the include degrades to the import, so these declarations are not ALSO +// re-declared in the global module. A header is textual everywhere or module-owned everywhere, never +// both: mixing the two gives every type here two definitions, and nothing links. +// +// `import` is legal in a global module fragment, so a .cppm that reaches this header from its GMF is +// fine. What is NOT fine is reaching it from inside an `export { }` block for the first time -- put it +// in that module's GMF instead. +#if defined(SMALLSTRING_USE_MODULE) && !defined(SMALLSTRING_MODULE_INTERFACE) + +import smallstring; + +#else + +#ifndef SMALLSTRING_MODULE_INTERFACE #include #include @@ -42,11 +58,16 @@ import fmt; #include #endif +#endif // !SMALLSTRING_MODULE_INTERFACE + namespace small { #ifndef Assert #define Assert(condition, message) assert((condition) && (message)) #endif -namespace { +// Not an unnamed namespace: entities there have internal linkage, and a C++20 module interface +// cannot reference an internal-linkage entity from an exported inline function or template +// ("'kMinAlignSize' has internal linkage and cannot be referenced from an exported ..."). +namespace detail { inline constexpr uint64_t kMinAlignSize = 8; // 64 bits for modern cpu /** * @brief Aligns a value up to the next multiple of N @@ -65,7 +86,10 @@ template return (n + N - 1) & static_cast(-N); } -} // namespace +} // namespace detail + +using detail::AlignUpTo; +using detail::kMinAlignSize; /** * @brief Storage strategy enumeration for small string optimization @@ -5668,3 +5692,5 @@ struct hash in this fragment would bake libstdc++'s declarations into the BMI as +// global-module entities. Any consumer that then reads textually -- directly, or through +// -- re-declares basic_string.tcc's explicit instantiations on top of them and clang +// rejects it: "explicit instantiation of 'getline' does not refer to a function template". It reaches +// consumers that never name smallstring, too, because a module that imports smallstring carries those +// declarations onward in its own BMI. +#include + +// fmt is textual: smallstring.hpp specialises fmt::formatter, and fmt is an ordinary library in this +// build. (It is the one heavy header that has to stay in the fragment; every other module in the tree +// carries it in its own fragment too.) +#include + +export module smallstring; + +import std.compat; + +#define SMALLSTRING_MODULE_INTERFACE 1 +export { +#include "smallstring.hpp" +} +#undef SMALLSTRING_MODULE_INTERFACE From 6d0d6b331b17f47493f16af766ab51d0ad04fcfb Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 10:44:25 +0800 Subject: [PATCH 4/9] fmt: keep the format spec -- the formatter was silently discarding width, fill and alignment The formatter used to derive from fmt::formatter, which parses the spec and applies it. It was replaced by a hand-rolled parse() that just skips to '}' and stores nothing, and a format() that writes through a literal "{}". Every format spec was therefore discarded in silence: fmt::format("{:>5}", small_string("foo")) -> "foo" (should be " foo") fmt::format("{:*^7}", small_string("foo")) -> "foo" (should be "**foo**") regression/string_test.cc covers this. Delegate to the string_view formatter again. --- include/smallstring.hpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 79a7ec8..a7318f3 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -5485,19 +5485,16 @@ template class, class T, class A, bool N, float G> class Buffer, template class Core, class Traits, class Allocator, bool NullTerminated, float Growth> struct fmt::formatter> + : fmt::formatter { - constexpr auto parse(fmt::format_parse_context& ctx) -> fmt::format_parse_context::iterator { - auto it = ctx.begin(); - const auto end = ctx.end(); - while (it != end && *it != '}') { - ++it; - } - return it; - } + // Delegate to the string_view formatter, which parses and applies the format spec. A hand-rolled + // parse() that merely skips to '}' stores no state, so width, alignment, fill and precision are + // silently discarded -- fmt::format("{:>5}", small_string("foo")) would give "foo", not " foo". + using fmt::formatter::parse; auto format(const small::basic_small_string& str, fmt::format_context& ctx) const noexcept { - return fmt::format_to(ctx.out(), "{}", std::string_view{str.data(), str.size()}); + return fmt::formatter::format({str.data(), str.size()}, ctx); } }; From 66f00d4118171b199662c6a552e36d236627b3a6 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 19:48:09 +0800 Subject: [PATCH 5/9] modules: keep fmt visible through the module-mode include From review. This header has always made visible to whoever includes it, and code leans on that -- regression/string_test.cc includes only smallstring.hpp and then calls fmt::format. Under SMALLSTRING_USE_MODULE the include collapsed to a bare `import smallstring;`, which cannot carry those declarations: fmt sits in the module's global module fragment, and a GMF is not re-exported. So turning the module on silently took fmt away from every consumer. Reproduced with a TU that includes only this header and calls fmt::format: error: missing '#include ".../fmt/format.h"'; 'format' must be declared before it is used The module-mode branch now pulls fmt in exactly the way the textual branch does. That costs nothing -- fmt is textual on both sides of the module boundary (or a module on both sides, under STDB_USE_FMT_MODULE), so the declarations are the same entities either way. --- include/smallstring.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index a7318f3..a746332 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -24,6 +24,24 @@ // in that module's GMF instead. #if defined(SMALLSTRING_USE_MODULE) && !defined(SMALLSTRING_MODULE_INTERFACE) +// fmt as well, not just the import. This header has always made visible to whoever +// includes it, and plenty of code leans on that -- regression/string_test.cc includes only +// smallstring.hpp and then calls fmt::format. The module cannot carry those declarations across: fmt +// sits in its global module fragment and a GMF is not re-exported, so `import smallstring;` alone would +// silently take fmt away from every consumer the moment SMALLSTRING_USE_MODULE is turned on. +// +// Keeping the include here costs nothing -- fmt is textual on both sides of the module boundary (or a +// module on both sides, under STDB_USE_FMT_MODULE), so the declarations are the same entities either +// way. +#if defined(STDB_USE_FMT_MODULE) +#ifndef STDB_FMT_IMPORTED +#define STDB_FMT_IMPORTED 1 +import fmt; +#endif +#else +#include +#endif + import smallstring; #else From c55555f3bbf7de81c4c640c3bc1059248f64ae71 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 22:06:33 +0800 Subject: [PATCH 6/9] modules: state the condition the textual fmt include depends on, and check it Review asked the module interface to mirror the STDB_USE_FMT_MODULE branch that the public shim carries -- to `import fmt;` here rather than always include -- on the grounds that otherwise the exported fmt::formatter specialisation attaches to the textual fmt::formatter while consumers format through the imported one. That does not happen, and the suggested change does not compile. Built fmt's module the way our deps build does (FMT_ATTACH_TO_GLOBAL_MODULE), built the smallstring BMI with STDB_USE_FMT_MODULE, and compiled a consumer that goes through the shim -- so `import fmt;` then `import smallstring;` -- and calls fmt::format on a small_string: it compiles, links and runs, and `fmt::format("[{:>8}]", s)` yields "[ hello]", so the specialisation the consumer finds really is the module's, format spec and all. It works because FMT_ATTACH_TO_GLOBAL_MODULE detaches every fmt declaration from module `fmt`; imported and textual fmt::formatter are then the same global-module entity, which is the whole point of that macro. Doing it the other way round is what breaks. `import fmt;` in this unit fails to build in either fmt flavour, because the specialisation derives from fmt::formatter and through an import that base resolves to fmt's primary template -- deleted constructor, no parse(), no format(). It is fmt's behaviour, not ours: a TU containing nothing but `import fmt;` and a mention of fmt::formatter fails identically, while the same TU with a textual compiles. The premise is still worth something, though, because it is exactly right about what the arrangement depends on, and nothing said so. Rebuilding fmt's module without FMT_ATTACH_TO_GLOBAL_MODULE does break the consumer, with "declaration 'basic_appender' attached to named module 'fmt' cannot be attached to other modules" -- an error pointing into fmt's headers, from a translation unit that need not mention smallstring at all. So the condition is now written down where the include is, and an #error asserts it while the interface is being compiled, where the message can name the cause. The comment in the shim claimed fmt was "a module on both sides" under STDB_USE_FMT_MODULE; it never was, and that is corrected too. Default builds are untouched: STDB_USE_FMT_MODULE is off, the #if is dead, and the include is the same one it always was. --- include/smallstring.hpp | 10 +++++++--- smallstring.cppm | 27 ++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index a746332..995d541 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -30,9 +30,13 @@ // sits in its global module fragment and a GMF is not re-exported, so `import smallstring;` alone would // silently take fmt away from every consumer the moment SMALLSTRING_USE_MODULE is turned on. // -// Keeping the include here costs nothing -- fmt is textual on both sides of the module boundary (or a -// module on both sides, under STDB_USE_FMT_MODULE), so the declarations are the same entities either -// way. +// Keeping the include here costs nothing -- fmt is textual on both sides of the module boundary. +// +// Under STDB_USE_FMT_MODULE it is textual on *one* side: the interface unit reads +// textually no matter what, and only the consumer imports. Those still meet, because fmt's module is +// required to be built with FMT_ATTACH_TO_GLOBAL_MODULE, which leaves its declarations attached to +// the global module -- the same entities the interface unit saw. smallstring.cppm #errors if that is +// not so, and explains why it cannot simply `import fmt;` instead. #if defined(STDB_USE_FMT_MODULE) #ifndef STDB_FMT_IMPORTED #define STDB_FMT_IMPORTED 1 diff --git a/smallstring.cppm b/smallstring.cppm index fdf24fe..8077624 100644 --- a/smallstring.cppm +++ b/smallstring.cppm @@ -25,9 +25,30 @@ module; // declarations onward in its own BMI. #include -// fmt is textual: smallstring.hpp specialises fmt::formatter, and fmt is an ordinary library in this -// build. (It is the one heavy header that has to stay in the fragment; every other module in the tree -// carries it in its own fragment too.) +// fmt is textual here, and stays textual even when the consumer imports it (STDB_USE_FMT_MODULE). +// That asymmetry with the shim in smallstring.hpp is deliberate, and it rests on one thing: +// +// fmt's module must be built with FMT_ATTACH_TO_GLOBAL_MODULE. +// +// That macro detaches every fmt declaration from module `fmt` (fmt's own words: "you can mix TUs +// with either importing or #including the {fmt} API"). So the fmt::formatter this fragment sees and +// the fmt::formatter an importing consumer sees are the *same* global-module entity, and the +// fmt::formatter specialisation exported below is the one the consumer finds. +// +// It cannot be done the other way round. Reaching fmt by `import fmt;` here does not compile: the +// specialisation derives from fmt::formatter, and through an import that base +// resolves to fmt's *primary* template -- "no member named 'parse'", deleted constructor. That is a +// property of fmt itself, not of smallstring; a TU that does nothing but `import fmt;` and name +// fmt::formatter fails the same way, while the identical TU with a textual +// compiles. +// +// Without FMT_ATTACH_TO_GLOBAL_MODULE the mix really is unsound -- the consumer's `import fmt;` +// meets the global-module fmt declarations baked into this BMI and clang rejects it, "declaration +// 'basic_appender' attached to named module 'fmt' cannot be attached to other modules" -- so say so +// here rather than let it surface as that error in someone else's translation unit. +#if defined(STDB_USE_FMT_MODULE) && !defined(FMT_ATTACH_TO_GLOBAL_MODULE) +#error "module smallstring needs fmt's module built with FMT_ATTACH_TO_GLOBAL_MODULE: it specialises fmt::formatter through a textual , which only matches an imported fmt when fmt's declarations are attached to the global module." +#endif #include export module smallstring; From 7d2be691dbe71c1be658909abb24a83bbe15eab6 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 22:19:12 +0800 Subject: [PATCH 7/9] fmt: the header-only path must read fmt textually, even when fmt is a module The textual branch of the shim imported fmt whenever STDB_USE_FMT_MODULE was set. That branch is the one that carries the body, so it is the one that *defines* fmt::formatter, and the definition derives from fmt::formatter. Through an import that base is fmt's primary template, so the header simply does not compile: "no member named 'parse' in 'fmt::formatter>'", deleted constructor. Anyone who turned the fmt module on while keeping smallstring header-only hit it. Reproduced exactly that way -- header-only smallstring, fmt reached by import -- and the two errors land on the formatter's parse and format. It is the same failure the module interface already documents, arriving from the other side, and the reason is the same: fmt's formatter specialisation is not reachable through `import fmt;`. So the fix is the same as there. This branch now always includes . The import stays in the SMALLSTRING_USE_MODULE branch, where it is correct and necessary: that branch never parses this body, the specialisation reaches the consumer ready-made from the BMI, and all the consumer needs from fmt is its declarations. Verified that distinction rather than assumed it -- header-only + fmt module now compiles and still honours the spec ("{:>5}" on a small_string gives " foo", and fill, alignment and precision with it), while the module consumer continues to compile, link and run with the format spec applied across the module boundary. --- include/smallstring.hpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 995d541..1e8ebce 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -68,17 +68,20 @@ import smallstring; #include #include -// fmt comes in as a C++20 module only when the consumer asked for it (STDB_USE_FMT_MODULE, off by -// default). Unconditionally importing it made this header unusable in any build where fmt is an -// ordinary library -- "fatal error: module 'fmt' not found". -#if defined(STDB_USE_FMT_MODULE) -#ifndef STDB_FMT_IMPORTED -#define STDB_FMT_IMPORTED 1 -import fmt; -#endif -#else +// fmt is textual here, and stays textual even when the rest of the build has fmt as a C++20 module +// (STDB_USE_FMT_MODULE). This is the branch that carries the *body*, so it is the branch that +// *defines* fmt::formatter -- and that definition derives from +// fmt::formatter. Reached through `import fmt;`, that base resolves to fmt's +// primary template and the header does not compile: "no member named 'parse' in +// 'fmt::formatter>'", deleted constructor. +// +// (The import is fine in the SMALLSTRING_USE_MODULE branch above, and only there, because that +// branch never parses this body -- the specialisation arrives ready-made from the module, and the +// consumer needs nothing from fmt but its declarations.) +// +// Mixing this textual fmt with an imported fmt elsewhere in the program is exactly what +// FMT_ATTACH_TO_GLOBAL_MODULE is for; smallstring.cppm requires it and explains why. #include -#endif #endif // !SMALLSTRING_MODULE_INTERFACE From 39a56469a7c5dd31c5c6e6b871447cabfd576604 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 22:24:32 +0800 Subject: [PATCH 8/9] fmt: never import fmt, and say why -- seastar settles it The last `import fmt;` was in the module-consumer branch of the shim. It worked, but it should not have existed, and keeping it was what kept generating bugs: of the three real problems found in this PR, two were in an fmt-import path (the interface unit, then the header-only path), and both had the same cause. seastar decides the question. ~21 of its headers reach fmt through and will keep doing so, so fmt -- if it is ever built as a module here at all -- has to carry FMT_ATTACH_TO_GLOBAL_MODULE or the tree does not link. That macro leaves fmt's declarations attached to the global module, which is exactly the condition under which a textual read of fmt meets an imported one. So smallstring can simply always read fmt textually, in every configuration, and be correct whether or not anything else in the program imports it. The import bought nothing to weigh against that. Any TU that pulls in seastar already has fmt textually, so there was no parse to save; and it cannot be made to work in the branch that defines fmt::formatter, because through an import the fmt::formatter it derives from resolves to fmt's primary template. So the three-state shim no longer branches on how fmt is consumed at all: , always. What remains of STDB_USE_FMT_MODULE is a single assertion at the top of the header -- if fmt is a module and is not attached to the global module, say so there, by name, instead of letting it surface as "declaration 'basic_appender' attached to named module 'fmt' cannot be attached to other modules" in somebody's unrelated translation unit. Checked all four ways in: header-only with textual fmt, header-only with fmt as a module, module consumer with fmt as a module (compiles, links, runs, "{:>8}" still lands as "[ hello]" across the boundary), and the assertion firing on the one combination that cannot work. Format specs still hold on the textual path -- width, alignment, fill, precision. build.ci's default build is unchanged. --- include/smallstring.hpp | 51 +++++++++++++++++++---------------------- smallstring.cppm | 28 ++++------------------ 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 1e8ebce..0a0ecdf 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -13,6 +13,26 @@ */ #pragma once +// smallstring reads fmt textually, always -- header-only, module consumer, module interface alike. It +// never imports fmt, in any configuration. +// +// It can afford to be that blunt because fmt, if it is a module at all, has to be built with +// FMT_ATTACH_TO_GLOBAL_MODULE: seastar reaches fmt through in ~21 of its headers and +// will keep doing so, so fmt's declarations must stay attached to the global module or nothing in the +// tree links. That macro is precisely what makes textual and imported fmt the *same* entities ("you +// can mix TUs with either importing or #including the {fmt} API" -- fmt's own words), so a textual +// read here meets an imported fmt anywhere else in the program. +// +// The converse does not hold, which is why there is no `import fmt;` branch to balance this one: the +// fmt::formatter specialisation below derives from fmt::formatter, +// and through an import that base resolves to fmt's *primary* template -- "no member named 'parse'", +// deleted constructor. That is fmt's behaviour, not smallstring's (a TU that does nothing but +// `import fmt;` and name fmt::formatter fails identically), and it is why every +// attempt to route this header's fmt through the module has been a bug. +#if defined(STDB_USE_FMT_MODULE) && !defined(FMT_ATTACH_TO_GLOBAL_MODULE) +#error "smallstring reads fmt textually and specialises fmt::formatter, so an fmt built as a C++20 module must be built with FMT_ATTACH_TO_GLOBAL_MODULE -- otherwise its declarations attach to module `fmt` and the textual ones here cannot match them." +#endif + // Owned by module `smallstring` when the consumer builds with modules (SMALLSTRING_USE_MODULE). // // Outside that module the include degrades to the import, so these declarations are not ALSO @@ -30,21 +50,9 @@ // sits in its global module fragment and a GMF is not re-exported, so `import smallstring;` alone would // silently take fmt away from every consumer the moment SMALLSTRING_USE_MODULE is turned on. // -// Keeping the include here costs nothing -- fmt is textual on both sides of the module boundary. -// -// Under STDB_USE_FMT_MODULE it is textual on *one* side: the interface unit reads -// textually no matter what, and only the consumer imports. Those still meet, because fmt's module is -// required to be built with FMT_ATTACH_TO_GLOBAL_MODULE, which leaves its declarations attached to -// the global module -- the same entities the interface unit saw. smallstring.cppm #errors if that is -// not so, and explains why it cannot simply `import fmt;` instead. -#if defined(STDB_USE_FMT_MODULE) -#ifndef STDB_FMT_IMPORTED -#define STDB_FMT_IMPORTED 1 -import fmt; -#endif -#else +// Keeping the include here costs nothing: it is the same the interface unit read, and +// under FMT_ATTACH_TO_GLOBAL_MODULE (see the top of this file) the same entities either way. #include -#endif import smallstring; @@ -68,19 +76,8 @@ import smallstring; #include #include -// fmt is textual here, and stays textual even when the rest of the build has fmt as a C++20 module -// (STDB_USE_FMT_MODULE). This is the branch that carries the *body*, so it is the branch that -// *defines* fmt::formatter -- and that definition derives from -// fmt::formatter. Reached through `import fmt;`, that base resolves to fmt's -// primary template and the header does not compile: "no member named 'parse' in -// 'fmt::formatter>'", deleted constructor. -// -// (The import is fine in the SMALLSTRING_USE_MODULE branch above, and only there, because that -// branch never parses this body -- the specialisation arrives ready-made from the module, and the -// consumer needs nothing from fmt but its declarations.) -// -// Mixing this textual fmt with an imported fmt elsewhere in the program is exactly what -// FMT_ATTACH_TO_GLOBAL_MODULE is for; smallstring.cppm requires it and explains why. +// This is the branch that carries the body, so it is the one that *defines* the formatter +// specialisation -- the case the note at the top of this file is really about. Textual, always. #include #endif // !SMALLSTRING_MODULE_INTERFACE diff --git a/smallstring.cppm b/smallstring.cppm index 8077624..f5a908e 100644 --- a/smallstring.cppm +++ b/smallstring.cppm @@ -25,30 +25,10 @@ module; // declarations onward in its own BMI. #include -// fmt is textual here, and stays textual even when the consumer imports it (STDB_USE_FMT_MODULE). -// That asymmetry with the shim in smallstring.hpp is deliberate, and it rests on one thing: -// -// fmt's module must be built with FMT_ATTACH_TO_GLOBAL_MODULE. -// -// That macro detaches every fmt declaration from module `fmt` (fmt's own words: "you can mix TUs -// with either importing or #including the {fmt} API"). So the fmt::formatter this fragment sees and -// the fmt::formatter an importing consumer sees are the *same* global-module entity, and the -// fmt::formatter specialisation exported below is the one the consumer finds. -// -// It cannot be done the other way round. Reaching fmt by `import fmt;` here does not compile: the -// specialisation derives from fmt::formatter, and through an import that base -// resolves to fmt's *primary* template -- "no member named 'parse'", deleted constructor. That is a -// property of fmt itself, not of smallstring; a TU that does nothing but `import fmt;` and name -// fmt::formatter fails the same way, while the identical TU with a textual -// compiles. -// -// Without FMT_ATTACH_TO_GLOBAL_MODULE the mix really is unsound -- the consumer's `import fmt;` -// meets the global-module fmt declarations baked into this BMI and clang rejects it, "declaration -// 'basic_appender' attached to named module 'fmt' cannot be attached to other modules" -- so say so -// here rather than let it surface as that error in someone else's translation unit. -#if defined(STDB_USE_FMT_MODULE) && !defined(FMT_ATTACH_TO_GLOBAL_MODULE) -#error "module smallstring needs fmt's module built with FMT_ATTACH_TO_GLOBAL_MODULE: it specialises fmt::formatter through a textual , which only matches an imported fmt when fmt's declarations are attached to the global module." -#endif +// fmt is textual, here and everywhere else in smallstring -- the header we are about to read +// specialises fmt::formatter, and it is the one heavy header that has to stay in this fragment. The +// note at the top of include/smallstring.hpp explains why it is never an `import fmt;`, and asserts +// the FMT_ATTACH_TO_GLOBAL_MODULE that lets a textual read here meet an imported fmt elsewhere. #include export module smallstring; From ddac83dc240fe300b6c9201ee82e52405b59ab11 Mon Sep 17 00:00:00 2001 From: hurricane1026 Date: Sun, 12 Jul 2026 22:56:59 +0800 Subject: [PATCH 9/9] modules: the Assert fallback needs , and only the module path was missing it Building smallstring.cppm without SMALLSTRING_PRELUDE does not compile. The header's `#ifndef Assert` fallback expands to assert(), but the module-interface path skips the whole include block that would have brought in, so every call site fails with "use of undeclared identifier 'assert'". `import std.compat` cannot cover for it: assert is a macro, and macros do not cross a module boundary. Reproduced by precompiling the interface with the prelude left undefined -- the errors land one per Assert(), at smallstring.hpp:602, :1013, :1017, :1043 and on. Only the prelude hides it, which is why it survived this long: clapdb always points SMALLSTRING_PRELUDE at a header that defines Assert, so the fallback never fires there. Anyone building the module the way the comment in this file describes -- "without it, smallstring.hpp falls back to plain assert() as before" -- got a module that does not build. The include has to go here, not in the header. On the module path the header body is read from inside an `export { }` block, and a first-time #include is not allowed there; that is exactly why the include block is guarded out. So the fallback's dependency belongs in the global module fragment, next to the SMALLSTRING_PRELUDE hook it backs up. It costs nothing: assert is a macro expanded while this unit is preprocessed, so it reaches no importer, and is just -- none of the libstdc++ declarations the note below it warns about. Checked that this actually restores assertions rather than just silencing the error: a consumer built against a no-prelude BMI compiles, links, runs, and its object carries a real __assert_fail reference. The prelude path and the STDB_USE_FMT_MODULE path are both unchanged. --- smallstring.cppm | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/smallstring.cppm b/smallstring.cppm index f5a908e..aec5b41 100644 --- a/smallstring.cppm +++ b/smallstring.cppm @@ -15,6 +15,20 @@ module; #include SMALLSTRING_PRELUDE #endif +// And that fallback needs -- which is why this include is here rather than in the header. +// +// smallstring.hpp does include , but only on its textual path: the module-interface path skips +// the whole include block, and it has to, because it is read from inside an `export { }` block where a +// first-time #include is not allowed. Its `#ifndef Assert` fallback still expands to assert(), though, +// so with no SMALLSTRING_PRELUDE to define Assert the interface would not compile -- "use of undeclared +// identifier 'assert'", once per call site. `import std.compat` cannot rescue it either: assert is a +// macro, and macros do not cross a module boundary. +// +// So the fallback's dependency belongs here, next to the hook it backs. It is a macro, expanded while +// this unit is preprocessed, so nothing about it reaches importers; is just , and +// carries none of the libstdc++ declarations the note below is about. +#include + // std comes in as `import std.compat` below, NOT as textual libstdc++ headers here. // // Textual in this fragment would bake libstdc++'s declarations into the BMI as