Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ if(NOT TARGET pugixml)
endif()

# mx_core: the generated C++ typed model plus its hand-written runtime
# (Decimal, Result, Error, Lexical, Token, NameToken, OneOrMore, Xml). The
# generated per-type sources live in src/private/mx/core/generated/ (their
# own directory, so generator-owned code is recognizable at a glance) and
# arrive via sources.cmake (emitted by `python3 -m gen gen/cpp/config.toml`).
# (Decimal, Result, Error, Lexical, ParseContext, Token, NameToken, OneOrMore,
# Xml). The generated per-type sources live in src/private/mx/core/generated/
# (their own directory, so generator-owned code is recognizable at a glance)
# and arrive via sources.cmake (emitted by `python3 -m gen gen/cpp/config.toml`).
set(MX_CORE_RUNTIME_SOURCES
${PRIVATE_DIR}/mx/core/Attribution.cpp
${PRIVATE_DIR}/mx/core/Attribution.h
Expand All @@ -73,6 +73,8 @@ set(MX_CORE_RUNTIME_SOURCES
${PRIVATE_DIR}/mx/core/NameToken.cpp
${PRIVATE_DIR}/mx/core/NameToken.h
${PRIVATE_DIR}/mx/core/OneOrMore.h
${PRIVATE_DIR}/mx/core/ParseContext.cpp
${PRIVATE_DIR}/mx/core/ParseContext.h
${PRIVATE_DIR}/mx/core/Result.h
${PRIVATE_DIR}/mx/core/Token.cpp
${PRIVATE_DIR}/mx/core/Token.h
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,14 @@ attributes. `mx::core` is generated from the MusicXML 4.0 XSD by the generator i
hand-written runtime lives in `src/private/mx/core/`, the generated model in
`src/private/mx/core/generated/` -- the directory is the generated/hand-written boundary).

<!-- TODO: document the unique ID exception -->
The generated model is valid-by-construction: enum wrappers whose named factories are the only
constructors, clamp-on-construct number wrappers, composites with named fields in schema order
(the serializer walks declaration order, so wrong element order is unrepresentable),
`std::variant`-based choice classes, `OneOrMore<T>` for required repeats, and `Result`-returning
bounded appends (e.g. beam <= 8). Value semantics throughout -- no shared pointers. Errors exist
in exactly two places: `mx::core::parse(const pugi::xml_document&) -> Result<Document>` (strict on
in exactly two places:
`mx::core::parse(const pugi::xml_document&, const ParseContext&) -> Result<Document>` (strict on
names and structure, lenient on values) and the bounded `add...` methods. Serialization is
`mx::core::serialize(const Document&, pugi::xml_document&)`. The design and its rationale are
recorded in `docs/ai/design/mx-core-plan.md`.
Expand Down
1 change: 1 addition & 0 deletions docs/ai/design/mx-core-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ named groups onto the Plates is *neutral schema fact* work that serves all targe

## 1. The non-negotiables (owner's cardinal requirements)

<!-- TODO: document the unique ID exception -->
1. **Valid by construction.** It must be impossible to use the public C++ API to produce a
document that is invalid against the MusicXML 4.0 spec. Invalid states are made
*unrepresentable* by the type system wherever possible — including bespoke structural
Expand Down
2 changes: 1 addition & 1 deletion gen/cpp/templates/attr_parse_expr.tmpl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#type_ref.is_value}}{{type_ref.ident}}::parse(a.value()){{/type_ref.is_value}}{{#type_ref.name_token}}{{type_ref.ident}}::parse(a.value()){{/type_ref.name_token}}{{#type_ref.is_primitive_string}}{{^type_ref.name_token}}std::string{a.value()}{{/type_ref.name_token}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}parseInt(a.value()){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}Decimal::parse(a.value()){{/type_ref.is_primitive_decimal}}
{{#type_ref.is_value}}parseValue<{{type_ref.ident}}>(a.value(), context, el, {{name.wire_q}}){{/type_ref.is_value}}{{#type_ref.name_token}}{{#type_ref.unique_id}}parseIdValue<{{type_ref.ident}}>(a.value(), context, el, {{name.wire_q}}){{/type_ref.unique_id}}{{^type_ref.unique_id}}parseValue<{{type_ref.ident}}>(a.value(), context, el, {{name.wire_q}}){{/type_ref.unique_id}}{{/type_ref.name_token}}{{#type_ref.is_primitive_string}}{{^type_ref.name_token}}std::string{a.value()}{{/type_ref.name_token}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}parseIntegerValue(a.value(), context, el, {{name.wire_q}}){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}parseDecimalValue(a.value(), context, el, {{name.wire_q}}){{/type_ref.is_primitive_decimal}}
1 change: 1 addition & 0 deletions gen/cpp/templates/attributes_parse.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
{{#import_default}}
// Configured import repair (plan §2.4): inject the default.
out.set{{name.pascal}}({{>attr_default_expr}});
reportAttributeDefaulted(context, el, {{name.wire_q}}, {{import_default_q}});
{{/import_default}}
{{^import_default}}
throwMissingAttribute(el, {{name.wire_q}});
Expand Down
8 changes: 5 additions & 3 deletions gen/cpp/templates/attrs.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "mx/core/generated/{{name.pascal}}.h"

#include "mx/core/Lexical.h"
#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

#include <utility>
Expand All @@ -11,17 +12,18 @@ namespace {{vars.namespace}}
{

{{>attributes_impl}}
{{ident}} parse{{ident}}(pugi::xml_node el)
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context)
{
{{ident}} out;
{{>attributes_parse}}
parse{{ident}}Content(out, el);
parse{{ident}}Content(out, el, context);
return out;
}

void parse{{ident}}Content({{ident}} &out, pugi::xml_node el)
void parse{{ident}}Content({{ident}} &out, pugi::xml_node el, const ParseContext &context)
{
(void)out;
(void)context;
if (firstElement(el))
{
throwUnknownElement(firstElement(el));
Expand Down
6 changes: 4 additions & 2 deletions gen/cpp/templates/attrs.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class xml_node;
namespace {{vars.namespace}}
{

class ParseContext;

{{#doc_lines}}
/// {{value}}
{{/doc_lines}}
Expand All @@ -37,9 +39,9 @@ private:
{{>attributes_members}}
};

{{ident}} parse{{ident}}(pugi::xml_node el);
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context);

void parse{{ident}}Content({{ident}} &out, pugi::xml_node el);
void parse{{ident}}Content({{ident}} &out, pugi::xml_node el, const ParseContext &context);

void serialize{{ident}}(const {{ident}} &v, pugi::xml_node parent, const char *tag);

Expand Down
11 changes: 6 additions & 5 deletions gen/cpp/templates/choice.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "mx/core/generated/{{name.pascal}}.h"

#include "mx/core/Lexical.h"
#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

#include <utility>
Expand All @@ -26,7 +27,7 @@ namespace {{vars.namespace}}
}

{{/alternatives}}
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor)
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor, const ParseContext &context)
{
{{#alternatives}}
if ({{>field_first_match}})
Expand Down Expand Up @@ -60,21 +61,21 @@ namespace {{vars.namespace}}
{{/is_element}}
{{^is_element}}
{{#is_required}}
return {{type.ident}}::{{ident}}(parse{{type_ref.ident}}(el, cursor));
return {{type.ident}}::{{ident}}(parse{{type_ref.ident}}(el, cursor, context));
{{/is_required}}
{{#is_vector}}
{{#min1}}
OneOrMore<{{type_ref.ident}}> items{parse{{type_ref.ident}}(el, cursor)};
OneOrMore<{{type_ref.ident}}> items{parse{{type_ref.ident}}(el, cursor, context)};
while ({{>field_first_match}})
{
items.add(parse{{type_ref.ident}}(el, cursor));
items.add(parse{{type_ref.ident}}(el, cursor, context));
}
{{/min1}}
{{^min1}}
std::vector<{{type_ref.ident}}> items;
while ({{>field_first_match}})
{
items.push_back(parse{{type_ref.ident}}(el, cursor));
items.push_back(parse{{type_ref.ident}}(el, cursor, context));
}
{{/min1}}
return {{type.ident}}::{{ident}}(std::move(items));
Expand Down
4 changes: 3 additions & 1 deletion gen/cpp/templates/choice.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class xml_node;
namespace {{vars.namespace}}
{

class ParseContext;

{{#doc_lines}}
/// {{value}}
{{/doc_lines}}
Expand Down Expand Up @@ -87,7 +89,7 @@ private:

/// Consumes one alternative starting at `cursor` (which advances); `el` is
/// the enclosing element, for error paths.
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor);
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor, const ParseContext &context);

void serialize{{ident}}(const {{ident}} &v, pugi::xml_node el);

Expand Down
5 changes: 3 additions & 2 deletions gen/cpp/templates/choice_boxed.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "mx/core/generated/{{name.pascal}}.h"

#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

#include <utility>
Expand Down Expand Up @@ -54,12 +55,12 @@ bool {{ident}}::operator==(const {{ident}} &other) const
}

{{/alternatives}}
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor)
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor, const ParseContext &context)
{
{{#alternatives}}
if (cursorIs(cursor, {{tag_q}}))
{
{{type_ref.ident}} value = parse{{type_ref.ident}}(cursor);
{{type_ref.ident}} value = parse{{type_ref.ident}}(cursor, context);
cursor = nextElement(cursor);
return {{type.ident}}::{{ident}}(std::move(value));
}
Expand Down
4 changes: 3 additions & 1 deletion gen/cpp/templates/choice_boxed.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class xml_node;
namespace {{vars.namespace}}
{

class ParseContext;

{{#doc_lines}}
/// {{value}}
{{/doc_lines}}
Expand Down Expand Up @@ -89,7 +91,7 @@ private:

/// Consumes one alternative starting at `cursor` (which advances); `el` is
/// the enclosing element, for error paths.
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor);
{{ident}} parse{{ident}}(pugi::xml_node el, pugi::xml_node &cursor, const ParseContext &context);

void serialize{{ident}}(const {{ident}} &v, pugi::xml_node el);

Expand Down
12 changes: 12 additions & 0 deletions gen/cpp/templates/color.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,16 @@ bool {{ident}}::tryParse(std::string_view text, {{ident}} &out) noexcept
return out;
}

{{ident}} {{ident}}::parse(std::string_view text, ValueParseOutcome &outcome) noexcept
{
{{ident}} out;
if (tryParse(text, out))
{
outcome = ValueParseOutcome::valid;
return out;
}
outcome = ValueParseOutcome::invalid;
return parse(text);
}

} // namespace {{vars.namespace}}
5 changes: 5 additions & 0 deletions gen/cpp/templates/color.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#pragma once

#include "mx/core/Lexical.h"

#include <cstdint>
#include <optional>
#include <string>
Expand Down Expand Up @@ -81,6 +83,9 @@ public:
/// Lenient: malformed text yields opaque black.
static {{ident}} parse(std::string_view text) noexcept;

/// Lenient, and says whether the text had to be repaired.
static {{ident}} parse(std::string_view text, ValueParseOutcome &outcome) noexcept;

bool operator==(const {{ident}} &other) const noexcept = default;

private:
Expand Down
12 changes: 12 additions & 0 deletions gen/cpp/templates/comma_separated_text.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ bool {{ident}}::tryParse(std::string_view text, {{ident}} &out)
return out;
}

{{ident}} {{ident}}::parse(std::string_view text, ValueParseOutcome &outcome)
{
{{ident}} out;
if (tryParse(text, out))
{
outcome = ValueParseOutcome::valid;
return out;
}
outcome = ValueParseOutcome::invalid;
return parse(text);
}

} // namespace {{vars.namespace}}
5 changes: 5 additions & 0 deletions gen/cpp/templates/comma_separated_text.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#pragma once

#include "mx/core/Lexical.h"

#include <span>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -43,6 +45,9 @@ public:
/// Lenient: repairs into the nearest valid list.
static {{ident}} parse(std::string_view text);

/// Lenient, and says whether the text had to be repaired.
static {{ident}} parse(std::string_view text, ValueParseOutcome &outcome);

bool operator==(const {{ident}} &other) const noexcept
{
return m_items == other.m_items;
Expand Down
7 changes: 4 additions & 3 deletions gen/cpp/templates/composite.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "mx/core/generated/{{name.pascal}}.h"

#include "mx/core/Lexical.h"
#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

#include <utility>
Expand All @@ -12,15 +13,15 @@ namespace {{vars.namespace}}

{{>attributes_impl}}
{{>fields_impl}}
{{ident}} parse{{ident}}(pugi::xml_node el)
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context)
{
{{ident}} out;
{{>attributes_parse}}
parse{{ident}}Content(out, el);
parse{{ident}}Content(out, el, context);
return out;
}

void parse{{ident}}Content({{ident}} &out, pugi::xml_node el)
void parse{{ident}}Content({{ident}} &out, pugi::xml_node el, const ParseContext &context)
{
pugi::xml_node cursor = firstElement(el);
{{>fields_parse}}
Expand Down
6 changes: 4 additions & 2 deletions gen/cpp/templates/composite.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class xml_node;
namespace {{vars.namespace}}
{

class ParseContext;

{{#doc_lines}}
/// {{value}}
{{/doc_lines}}
Expand All @@ -49,9 +51,9 @@ private:
{{>fields_members}}
};

{{ident}} parse{{ident}}(pugi::xml_node el);
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context);

void parse{{ident}}Content({{ident}} &out, pugi::xml_node el);
void parse{{ident}}Content({{ident}} &out, pugi::xml_node el, const ParseContext &context);

void serialize{{ident}}(const {{ident}} &v, pugi::xml_node parent, const char *tag);

Expand Down
5 changes: 3 additions & 2 deletions gen/cpp/templates/defaults.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{{#groups}}
#include "mx/core/generated/{{ident}}.h"
{{/groups}}
#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

namespace {{vars.namespace}}
Expand All @@ -19,7 +20,7 @@ void roundTripDefaults()
{
pugi::xml_document doc;
serialize{{ident}}({{ident}}{}, doc, "probe");
parse{{ident}}(doc.document_element());
parse{{ident}}(doc.document_element(), ParseContext{});
}
{{/complex_types}}
{{#groups}}
Expand All @@ -29,7 +30,7 @@ void roundTripDefaults()
serialize{{ident}}({{ident}}{}, el);
if (pugi::xml_node cursor = firstElement(el))
{
parse{{ident}}(el, cursor);
parse{{ident}}(el, cursor, ParseContext{});
}
}
{{/groups}}
Expand Down
5 changes: 3 additions & 2 deletions gen/cpp/templates/derived.cpp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "mx/core/generated/{{name.pascal}}.h"

#include "mx/core/Lexical.h"
#include "mx/core/ParseContext.h"
#include "mx/core/Xml.h"

#include <utility>
Expand All @@ -11,11 +12,11 @@ namespace {{vars.namespace}}
{

{{>attributes_impl}}
{{ident}} parse{{ident}}(pugi::xml_node el)
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context)
{
{{ident}} out;
{{>attributes_parse}}
parse{{base.ident}}Content(out, el);
parse{{base.ident}}Content(out, el, context);
return out;
}

Expand Down
4 changes: 3 additions & 1 deletion gen/cpp/templates/derived.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class xml_node;
namespace {{vars.namespace}}
{

class ParseContext;

{{#doc_lines}}
/// {{value}}
{{/doc_lines}}
Expand All @@ -39,7 +41,7 @@ private:
{{>attributes_members}}
};

{{ident}} parse{{ident}}(pugi::xml_node el);
{{ident}} parse{{ident}}(pugi::xml_node el, const ParseContext &context);

void serialize{{ident}}(const {{ident}} &v, pugi::xml_node parent, const char *tag);

Expand Down
Loading
Loading