From ca00eacda3e6eb0c2a4e90780e52c5aafe39a9a0 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 27 Jun 2026 19:27:30 +0200 Subject: [PATCH 1/2] fix: remove remaining anonymous namespaces for unity build compat Token.cpp and NameToken.cpp both defined isNameChar in an anonymous namespace inside mx::core; nmtoken_string.cpp.tmpl and smufl_wavy.cpp.tmpl did the same. All four collide in a unity build. Rename the helpers to per-file names (tokenIsNameChar, nameTokenIsNameChar, isNameChar{{ident}}) and drop the anonymous namespaces. Add a coding rule to AGENTS.md prohibiting anonymous namespaces project-wide. --- AGENTS.md | 24 +++++++++++++++++++++++ gen/cpp/templates/nmtoken_string.cpp.tmpl | 11 +++-------- gen/cpp/templates/smufl_wavy.cpp.tmpl | 11 +++-------- src/private/mx/core/NameToken.cpp | 11 +++-------- src/private/mx/core/Token.cpp | 19 +++++++----------- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aa426a1b1..12ed25ca9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,30 @@ serialization/deserialization libraries. See: What you need to know right now is that `gen/cpp` is where our MusicXML types are coming from. Run `make gen-cpp` to regenerate the C++ types. +## C++ coding rules + +Do not use anonymous namespaces (`namespace { }`) anywhere in the codebase. All symbols must be in +a named namespace. Anonymous namespaces give internal linkage, which is correct for a normal +one-TU-per-file build but causes redefinition errors in unity builds (where multiple `.cpp` files +are compiled as a single translation unit). Use a named helper function or per-type name instead: + +- For file-local helper functions: name them after the type or file, e.g. `tokenIsNameChar`, + `clampedTenths`. +- For file-local constants (arrays, string_views): use a per-type prefix, e.g. `kYesNoWire`, + `kSmuflAccidentalGlyphNamePrefix`. +- In code generator templates (`gen/cpp/templates/`): use `{{ident}}` to make names unique, e.g. + `k{{ident}}Wire`, `isNameChar{{ident}}`. + +Unity builds can be tested with CMake's built-in support — no changes to `CMakeLists.txt` are +needed by contributors. To verify, configure with: + +``` +cmake -S . -B build/unity -DCMAKE_UNITY_BUILD=ON -DCMAKE_UNITY_BUILD_BATCH_SIZE=0 +cmake --build build/unity --target mx +``` + +`BATCH_SIZE=0` puts all files in a target into one translation unit, which is the strictest test. + ## Quality gates Run `make fmt` to format. `make check` is the clang-format gate **only** — it builds and tests nothing. diff --git a/gen/cpp/templates/nmtoken_string.cpp.tmpl b/gen/cpp/templates/nmtoken_string.cpp.tmpl index 3573d2d41..26a4c5f04 100644 --- a/gen/cpp/templates/nmtoken_string.cpp.tmpl +++ b/gen/cpp/templates/nmtoken_string.cpp.tmpl @@ -7,18 +7,13 @@ namespace {{vars.namespace}} { -namespace -{ - // The ASCII subset of XML name characters (matches the schema's \c). -bool isNameChar(char c) noexcept +bool isNameChar{{ident}}(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':' || c == '_'; } -} // namespace - {{ident}}::{{ident}}() { repair(); @@ -41,7 +36,7 @@ void {{ident}}::repair() cleaned.reserve(m_value.size()); for (const char c : m_value) { - if (isNameChar(c)) + if (isNameChar{{ident}}(c)) { cleaned.push_back(c); } @@ -61,7 +56,7 @@ bool {{ident}}::tryParse(std::string_view text, {{ident}} &out) } for (const char c : text) { - if (!isNameChar(c)) + if (!isNameChar{{ident}}(c)) { return false; } diff --git a/gen/cpp/templates/smufl_wavy.cpp.tmpl b/gen/cpp/templates/smufl_wavy.cpp.tmpl index d10332026..481d79a02 100644 --- a/gen/cpp/templates/smufl_wavy.cpp.tmpl +++ b/gen/cpp/templates/smufl_wavy.cpp.tmpl @@ -7,22 +7,17 @@ namespace {{vars.namespace}} { -namespace -{ - constexpr std::string_view kWiggle = "wiggle"; constexpr std::string_view kGuitar = "guitar"; constexpr std::string_view kVibratoStroke = "VibratoStroke"; // The ASCII subset of XML name characters the schema's \c denotes. -bool isNameChar(char c) noexcept +bool isNameChar{{ident}}(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':' || c == '_'; } -} // namespace - {{ident}}::{{ident}}() { repair(); @@ -55,7 +50,7 @@ void {{ident}}::repair() cleaned.reserve(m_part.size()); for (const char c : m_part) { - if (isNameChar(c)) + if (isNameChar{{ident}}(c)) { cleaned.push_back(c); } @@ -86,7 +81,7 @@ bool {{ident}}::tryParse(std::string_view text, {{ident}} &out) const auto allNameChars = [](std::string_view s) { for (const char c : s) { - if (!isNameChar(c)) + if (!isNameChar{{ident}}(c)) { return false; } diff --git a/src/private/mx/core/NameToken.cpp b/src/private/mx/core/NameToken.cpp index 56b922020..bc7156e68 100644 --- a/src/private/mx/core/NameToken.cpp +++ b/src/private/mx/core/NameToken.cpp @@ -9,17 +9,12 @@ namespace mx::core { -namespace -{ - -bool isNameChar(char c) noexcept +bool nameTokenIsNameChar(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':' || c == '_'; } -} // namespace - NameToken::NameToken() { repair(); @@ -42,7 +37,7 @@ void NameToken::repair() cleaned.reserve(m_value.size()); for (const char c : m_value) { - if (isNameChar(c)) + if (nameTokenIsNameChar(c)) { cleaned.push_back(c); } @@ -62,7 +57,7 @@ bool NameToken::tryParse(std::string_view text, NameToken &out) } for (const char c : text) { - if (!isNameChar(c)) + if (!nameTokenIsNameChar(c)) { return false; } diff --git a/src/private/mx/core/Token.cpp b/src/private/mx/core/Token.cpp index a8460b478..641ffb32e 100644 --- a/src/private/mx/core/Token.cpp +++ b/src/private/mx/core/Token.cpp @@ -9,24 +9,19 @@ namespace mx::core { -namespace -{ - // The ASCII subset of the XML NCName character classes (the schema's // vocabularies are ASCII; the strict parse is the only consumer, so the // approximation can only under-accept). -bool isNameStart(char c) noexcept +bool tokenIsNameStart(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } -bool isNameChar(char c) noexcept +bool tokenIsNameChar(char c) noexcept { - return isNameStart(c) || (c >= '0' && c <= '9') || c == '-' || c == '.'; + return tokenIsNameStart(c) || (c >= '0' && c <= '9') || c == '-' || c == '.'; } -} // namespace - Token::Token() { repair(); @@ -49,14 +44,14 @@ void Token::repair() cleaned.reserve(m_value.size()); for (const char c : m_value) { - if (isNameChar(c)) + if (tokenIsNameChar(c)) { cleaned.push_back(c); } } // An NCName cannot begin with a digit, '-', or '.'. std::size_t start = 0; - while (start < cleaned.size() && !isNameStart(cleaned[start])) + while (start < cleaned.size() && !tokenIsNameStart(cleaned[start])) { ++start; } @@ -70,13 +65,13 @@ void Token::repair() bool Token::tryParse(std::string_view text, Token &out) { - if (text.empty() || !isNameStart(text.front())) + if (text.empty() || !tokenIsNameStart(text.front())) { return false; } for (const char c : text) { - if (!isNameChar(c)) + if (!tokenIsNameChar(c)) { return false; } From 35ae5e4230f6def12d4f40d4b0fb72d6edaabdff Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 27 Jun 2026 19:27:34 +0200 Subject: [PATCH 2/2] gen(cpp): regenerate nmtoken_string and smufl_wavy without anonymous namespace --- src/private/mx/core/generated/SmuflGlyphName.cpp | 11 +++-------- .../mx/core/generated/SmuflWavyLineGlyphName.cpp | 11 +++-------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/private/mx/core/generated/SmuflGlyphName.cpp b/src/private/mx/core/generated/SmuflGlyphName.cpp index e3191a413..61e16cd06 100644 --- a/src/private/mx/core/generated/SmuflGlyphName.cpp +++ b/src/private/mx/core/generated/SmuflGlyphName.cpp @@ -7,18 +7,13 @@ namespace mx::core { -namespace -{ - // The ASCII subset of XML name characters (matches the schema's \c). -bool isNameChar(char c) noexcept +bool isNameCharSmuflGlyphName(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':' || c == '_'; } -} // namespace - SmuflGlyphName::SmuflGlyphName() { repair(); @@ -41,7 +36,7 @@ void SmuflGlyphName::repair() cleaned.reserve(m_value.size()); for (const char c : m_value) { - if (isNameChar(c)) + if (isNameCharSmuflGlyphName(c)) { cleaned.push_back(c); } @@ -61,7 +56,7 @@ bool SmuflGlyphName::tryParse(std::string_view text, SmuflGlyphName &out) } for (const char c : text) { - if (!isNameChar(c)) + if (!isNameCharSmuflGlyphName(c)) { return false; } diff --git a/src/private/mx/core/generated/SmuflWavyLineGlyphName.cpp b/src/private/mx/core/generated/SmuflWavyLineGlyphName.cpp index 026b991ad..bdaebb54f 100644 --- a/src/private/mx/core/generated/SmuflWavyLineGlyphName.cpp +++ b/src/private/mx/core/generated/SmuflWavyLineGlyphName.cpp @@ -7,22 +7,17 @@ namespace mx::core { -namespace -{ - constexpr std::string_view kWiggle = "wiggle"; constexpr std::string_view kGuitar = "guitar"; constexpr std::string_view kVibratoStroke = "VibratoStroke"; // The ASCII subset of XML name characters the schema's \c denotes. -bool isNameChar(char c) noexcept +bool isNameCharSmuflWavyLineGlyphName(char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':' || c == '_'; } -} // namespace - SmuflWavyLineGlyphName::SmuflWavyLineGlyphName() { repair(); @@ -55,7 +50,7 @@ void SmuflWavyLineGlyphName::repair() cleaned.reserve(m_part.size()); for (const char c : m_part) { - if (isNameChar(c)) + if (isNameCharSmuflWavyLineGlyphName(c)) { cleaned.push_back(c); } @@ -86,7 +81,7 @@ bool SmuflWavyLineGlyphName::tryParse(std::string_view text, SmuflWavyLineGlyphN const auto allNameChars = [](std::string_view s) { for (const char c : s) { - if (!isNameChar(c)) + if (!isNameCharSmuflWavyLineGlyphName(c)) { return false; }