// mcpp.pm.mangle — multi-version coexistence (Level 1 of the dep
// resolver's three-step strategy).
//
// When the SemVer merger (`try_merge_semver`) fails because two
// transitive consumers of the same package can't share a version,
// the resolver picks one as "primary" (keeps its module name as
// authored) and rewrites the secondary copy under a mangled name so
// both BMIs can coexist in the build graph without ODR clashes.
//
// This module owns the **pure** half of that work:
// * `mangle_name` decides the mangled name format
// * `rewrite_module_decls` does a regex pass over a single .cppm
// file's text, rewriting module / import declarations whose name
// appears in a caller-supplied rename table.
//
// The orchestration half (deciding which package gets mangled,
// staging files into a per-build directory, splicing the staged
// PackageRoot into the resolver output) lives in `cli.cppm`.
export module mcpp.pm.mangle;
import std;
export namespace mcpp::pm {
// Mangled module name format: `__v____mcpp`.
// The double underscores keep the suffix outside the user namespace
// (C++ ABI reserves `__` for the implementation, so users can't
// collide), and the trailing `__mcpp` makes link-error backtraces
// obviously mcpp-generated rather than mistaken for hand-rolled.
//
// Dots in `version` become underscores so the result is a valid
// C++ module identifier (modules allow `.` but using it here would
// confuse partition-style readings later).
std::string mangle_name(std::string_view base, std::string_view version);
// Rewrite a single .cppm file's module / import declarations:
// * `(export )?module N;` → `(export )?module rename[N];`
// * `(export )?module N:P;` → `(export )?module rename[N]:P;`
// * `(export )?import N;` → `(export )?import rename[N];`
// * `(export )?import N:P;` → `(export )?import rename[N]:P;`
//
// Names not present in the `rename` table are left intact. Bare
// partition imports (`import :P;`) and the global module fragment
// (`module ;`) are also left intact — they don't name the enclosing
// module so they need no rewriting.
//
// Single-line comments and string literals are not parsed: the
// matcher requires the keyword (`module` / `import`) to be at the
// start of a logical line (whitespace-only prefix). That covers
// every real-world declaration site without needing a full lexer.
std::string rewrite_module_decls(
std::string_view source,
const std::map& rename);
} // namespace mcpp::pm
namespace mcpp::pm {
std::string mangle_name(std::string_view base, std::string_view version) {
std::string vmangled;
vmangled.reserve(version.size());
for (char c : version) vmangled += (c == '.' ? '_' : c);
return std::format("{}__v{}__mcpp", base, vmangled);
}
namespace {
// Module names are dotted identifiers: [A-Za-z_][A-Za-z0-9_.]*
bool is_name_start(char c) {
return std::isalpha(static_cast(c)) || c == '_';
}
bool is_name_cont(char c) {
return std::isalnum(static_cast(c)) || c == '_' || c == '.';
}
// Skip whitespace (spaces, tabs) in `s` from `i`. Returns new index.
std::size_t skip_ws(std::string_view s, std::size_t i) {
while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
return i;
}
// Try to consume the keyword `kw` at position `i`, requiring word boundary.
// Returns the index past `kw`, or std::string::npos on miss.
std::size_t consume_keyword(std::string_view s, std::size_t i,
std::string_view kw)
{
if (i + kw.size() > s.size()) return std::string::npos;
if (s.substr(i, kw.size()) != kw) return std::string::npos;
std::size_t after = i + kw.size();
if (after < s.size() && is_name_cont(s[after])) return std::string::npos;
return after;
}
// Parse a dotted identifier starting at `i`. Returns end index (first
// position past the name) and the name slice. Returns npos if no name.
std::pair
read_name(std::string_view s, std::size_t i)
{
if (i >= s.size() || !is_name_start(s[i])) return {std::string::npos, {}};
std::size_t start = i;
++i;
while (i < s.size() && is_name_cont(s[i])) ++i;
return {i, s.substr(start, i - start)};
}
} // namespace
std::string rewrite_module_decls(
std::string_view source,
const std::map& rename)
{
if (rename.empty()) return std::string(source);
std::string out;
out.reserve(source.size());
std::size_t i = 0;
while (i < source.size()) {
// We're at the start of a logical line. Capture leading whitespace.
std::size_t lineStart = i;
std::size_t afterWs = skip_ws(source, i);
// Try to recognize `(export )?(module|import) NAME[:PART][; or ws]`.
// On any deviation, copy the line verbatim.
std::size_t cur = afterWs;
bool hasExport = false;
if (auto p = consume_keyword(source, cur, "export"); p != std::string::npos) {
hasExport = true;
cur = skip_ws(source, p);
}
std::string_view kw;
if (auto p = consume_keyword(source, cur, "module"); p != std::string::npos) {
kw = "module"; cur = p;
} else if (auto p = consume_keyword(source, cur, "import"); p != std::string::npos) {
kw = "import"; cur = p;
} else {
// Not a module/import line — emit the rest of the physical line.
std::size_t eol = source.find('\n', lineStart);
if (eol == std::string_view::npos) eol = source.size();
else ++eol;
out.append(source.substr(lineStart, eol - lineStart));
i = eol;
continue;
}
std::size_t afterKw = skip_ws(source, cur);
// Bare partition import / global module fragment: `import :P;` /
// `module ;` — no name to rename, copy verbatim.
if (afterKw < source.size() && (source[afterKw] == ':' || source[afterKw] == ';')) {
std::size_t eol = source.find('\n', lineStart);
if (eol == std::string_view::npos) eol = source.size();
else ++eol;
out.append(source.substr(lineStart, eol - lineStart));
i = eol;
continue;
}
auto [nameEnd, name] = read_name(source, afterKw);
if (nameEnd == std::string::npos) {
// Not a recognized declaration. Verbatim.
std::size_t eol = source.find('\n', lineStart);
if (eol == std::string_view::npos) eol = source.size();
else ++eol;
out.append(source.substr(lineStart, eol - lineStart));
i = eol;
continue;
}
// Look up the name in the rename table.
auto it = rename.find(std::string(name));
if (it == rename.end()) {
// No rewrite needed for this declaration; copy line verbatim.
std::size_t eol = source.find('\n', lineStart);
if (eol == std::string_view::npos) eol = source.size();
else ++eol;
out.append(source.substr(lineStart, eol - lineStart));
i = eol;
continue;
}
// Emit the rewritten prefix:
// (export )?(module|import)
// Keep whatever follows the name (`:P;` / `;` / extras) verbatim.
out.append(source.substr(lineStart, afterWs - lineStart)); // ws
if (hasExport) out.append("export ");
out.append(kw); out.append(" ");
out.append(it->second);
// Append the trailing portion (from nameEnd) up to and including
// the newline.
std::size_t eol = source.find('\n', nameEnd);
if (eol == std::string_view::npos) eol = source.size();
else ++eol;
out.append(source.substr(nameEnd, eol - nameEnd));
i = eol;
}
return out;
}
} // namespace mcpp::pm