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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ if(NOT TARGET pugixml)
endif()

# mx_core: the generated C++ typed model plus its hand-written runtime
# (Decimal, Result, Error, Lexical, ParseContext, Token, NameToken, OneOrMore,
# Xml). The generated per-type sources live in src/private/mx/core/generated/
# (Decimal, Result, Error, Lexical, ParseContext, Token, NameToken, Language,
# 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
Expand All @@ -70,6 +71,8 @@ set(MX_CORE_RUNTIME_SOURCES
${PRIVATE_DIR}/mx/core/Error.h
${PRIVATE_DIR}/mx/core/IdIntegrity.cpp
${PRIVATE_DIR}/mx/core/IdIntegrity.h
${PRIVATE_DIR}/mx/core/Language.cpp
${PRIVATE_DIR}/mx/core/Language.h
${PRIVATE_DIR}/mx/core/Lexical.cpp
${PRIVATE_DIR}/mx/core/Lexical.h
${PRIVATE_DIR}/mx/core/NameToken.cpp
Expand Down
4 changes: 4 additions & 0 deletions gen/cpp/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ date = "std::string"
nmtoken = "NameToken"
id = "Token"
idref = "Token"
# The XML namespace's language tag: a hand-written repairing wrapper
# (Language.h/cpp) enforcing the builtin xs:language pattern, with the empty
# string admitted because the attribute's schema unions the un-declaration in.
language = "Language"
decimal = "Decimal"
integer = "int"
positive_integer = "int"
Expand Down
2 changes: 1 addition & 1 deletion gen/cpp/templates/attr_default_expr.tmpl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#type_ref.is_value}}{{type_ref.ident}}::parse({{import_default_q}}){{/type_ref.is_value}}{{#type_ref.is_primitive_string}}std::string({{import_default_q}}){{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}parseInt({{import_default_q}}){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}Decimal::parse({{import_default_q}}){{/type_ref.is_primitive_decimal}}
{{#type_ref.is_value}}{{type_ref.ident}}::parse({{import_default_q}}){{/type_ref.is_value}}{{#type_ref.wrapper}}{{type_ref.ident}}::parse({{import_default_q}}){{/type_ref.wrapper}}{{#type_ref.is_primitive_string}}{{^type_ref.wrapper}}std::string({{import_default_q}}){{/type_ref.wrapper}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}parseInt({{import_default_q}}){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}Decimal::parse({{import_default_q}}){{/type_ref.is_primitive_decimal}}
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}}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}}
{{#type_ref.is_value}}parseValue<{{type_ref.ident}}>(a.value(), context, el, {{name.wire_q}}){{/type_ref.is_value}}{{#type_ref.wrapper}}{{#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.wrapper}}{{#type_ref.is_primitive_string}}{{^type_ref.wrapper}}std::string{a.value()}{{/type_ref.wrapper}}{{/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}}
4 changes: 2 additions & 2 deletions gen/cpp/templates/attributes_serialize.tmpl
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{{#merged_attributes}}
{{#is_required}}
setAttribute(el, {{name.wire_q}}, {{#type_ref.is_value}}v.{{ident}}().toString(){{/type_ref.is_value}}{{#type_ref.name_token}}v.{{ident}}().toString(){{/type_ref.name_token}}{{#type_ref.is_primitive_string}}{{^type_ref.name_token}}v.{{ident}}(){{/type_ref.name_token}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}formatInt(v.{{ident}}()){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}v.{{ident}}().toString(){{/type_ref.is_primitive_decimal}});
setAttribute(el, {{name.wire_q}}, {{#type_ref.is_value}}v.{{ident}}().toString(){{/type_ref.is_value}}{{#type_ref.wrapper}}v.{{ident}}().toString(){{/type_ref.wrapper}}{{#type_ref.is_primitive_string}}{{^type_ref.wrapper}}v.{{ident}}(){{/type_ref.wrapper}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}formatInt(v.{{ident}}()){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}v.{{ident}}().toString(){{/type_ref.is_primitive_decimal}});
{{/is_required}}
{{#is_optional}}
if (v.{{ident}}())
{
setAttribute(el, {{name.wire_q}}, {{#type_ref.is_value}}v.{{ident}}()->toString(){{/type_ref.is_value}}{{#type_ref.name_token}}v.{{ident}}()->toString(){{/type_ref.name_token}}{{#type_ref.is_primitive_string}}{{^type_ref.name_token}}*v.{{ident}}(){{/type_ref.name_token}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}formatInt(*v.{{ident}}()){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}v.{{ident}}()->toString(){{/type_ref.is_primitive_decimal}});
setAttribute(el, {{name.wire_q}}, {{#type_ref.is_value}}v.{{ident}}()->toString(){{/type_ref.is_value}}{{#type_ref.wrapper}}v.{{ident}}()->toString(){{/type_ref.wrapper}}{{#type_ref.is_primitive_string}}{{^type_ref.wrapper}}*v.{{ident}}(){{/type_ref.wrapper}}{{/type_ref.is_primitive_string}}{{#type_ref.is_primitive_integer}}formatInt(*v.{{ident}}()){{/type_ref.is_primitive_integer}}{{#type_ref.is_primitive_decimal}}v.{{ident}}()->toString(){{/type_ref.is_primitive_decimal}});
}
{{/is_optional}}
{{/merged_attributes}}
3 changes: 3 additions & 0 deletions gen/cpp/templates/attrs.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "mx/core/NameToken.h"
#include "mx/core/Token.h"
{{/any_name_token}}
{{#any_language}}
#include "mx/core/Language.h"
{{/any_language}}

#include <optional>
#include <string>
Expand Down
3 changes: 3 additions & 0 deletions gen/cpp/templates/composite.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "mx/core/NameToken.h"
#include "mx/core/Token.h"
{{/any_name_token}}
{{#any_language}}
#include "mx/core/Language.h"
{{/any_language}}
{{#any_min1}}
#include "mx/core/OneOrMore.h"
{{/any_min1}}
Expand Down
3 changes: 3 additions & 0 deletions gen/cpp/templates/derived.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "mx/core/NameToken.h"
#include "mx/core/Token.h"
{{/any_name_token}}
{{#any_language}}
#include "mx/core/Language.h"
{{/any_language}}

#include <optional>
#include <string>
Expand Down
3 changes: 3 additions & 0 deletions gen/cpp/templates/value_class.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "mx/core/NameToken.h"
#include "mx/core/Token.h"
{{/any_name_token}}
{{#any_language}}
#include "mx/core/Language.h"
{{/any_language}}

#include <optional>
#include <string>
Expand Down
137 changes: 107 additions & 30 deletions gen/ir/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@
"xs:nonNegativeInteger": "non_negative_integer",
"xs:date": "date",
"xs:anyURI": "string",
"xs:language": "token",
# The XML namespace's language tag: a token restricted by the builtin
# pattern [a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})* (XSD 1.0 Part 2). A distinct
# primitive for the same reason the identity tokens are: a target that
# wants validity-by-construction repairs it, and a non-validating target
# maps it straight back to its token spelling (zero output change).
"xs:language": "language",
# NCName is NMTOKEN minus the colon; no MusicXML type rests on it
# directly, but the imported xml:space enumeration does.
"xs:NCName": "nmtoken",
# The identity builtins are NCName-derived: lexically NARROWER than token
# (no whitespace, no empty value, no leading digit or ':' for ID/IDREF;
# ID values are document-unique). Preserved as distinct primitives -- a
Expand All @@ -32,18 +40,17 @@
"xs:IDREF": "idref",
}

# The 10 attribute refs into the imported xml/xlink schemas, resolved to the
# primitive the emitter should use. This is the only place the IR reaches
# outside the main schema.
# The imported xml/xlink attribute refs the IR resolves to a builtin
# primitive instead of following the import: xml:lang's declared type is the
# xs:language BUILTIN (no schema declares it) and href/role/title are
# xs:anyURI/xs:string, which accept nearly any string. Every other external
# ref is lowered from its declaration in the imported schema, which the XSD
# parser has read beside the main one.
_EXTERNAL_ATTR = {
"xml:lang": "token",
"xml:space": "token",
"xml:lang": "language",
"xlink:href": "string",
"xlink:type": "token",
"xlink:role": "string",
"xlink:title": "string",
"xlink:show": "token",
"xlink:actuate": "token",
}

_NUMERIC = {"decimal", "integer", "positive_integer", "non_negative_integer"}
Expand Down Expand Up @@ -82,6 +89,16 @@ def __init__(self, schema: xsd.Schema, source: str):
self.source = source
self.anon_names: dict[int, str] = {} # id(ComplexType) -> synthesized name
self.synth: list[tuple[str, xsd.ComplexType]] = []
# The imported schemas' attribute declarations, indexed by local name
# (the wire ref keeps its prefix).
self.external_attrs: dict[str, list[tuple[str, xsd.Attribute]]] = {}
for namespace, ext in schema.imported.items():
for attr_name, decl in ext.attributes.items():
self.external_attrs.setdefault(attr_name, []).append((namespace, decl))
# Value types lowered out of the imported schemas, by IR name. They
# are picked up when an attribute ref reaches them (so no unreachable
# external type ever enters the IR).
self.external_values: dict[str, ir.ValueType] = {}

# ----- top level ------------------------------------------------------- #

Expand All @@ -90,11 +107,10 @@ def build(self) -> ir.Ir:
reachable = reachable_types(self.schema)

value_types = [
self._value_type(st)
self._value_type(st, self.schema.simple_types)
for name, st in self.schema.simple_types.items()
if name in reachable
]
value_types = self._topo_sort_values(value_types)
groups = [
ir.Group(name, self._particle(g.particle), g.doc)
for name, g in self.schema.groups.items()
Expand All @@ -116,6 +132,11 @@ def build(self) -> ir.Ir:
]
complex_types += [self._complex_type(name, ct) for name, ct in self.synth]

# The attribute refs lowered above may have pulled value types out of
# the imported schemas; they join the emit order deps-first.
value_types += list(self.external_values.values())
value_types = self._topo_sort_values(value_types)

resolver = Resolver(groups, attribute_groups, complex_types)
for ct in complex_types:
ct.deps = sorted(resolver.deps(ct))
Expand Down Expand Up @@ -169,20 +190,25 @@ def _scan(self, particle, qualifier: str, used: set) -> None:

# ----- value types ----------------------------------------------------- #

def _value_type(self, st: xsd.SimpleType) -> ir.ValueType:
def _value_type(self, st: xsd.SimpleType, simple_types: dict, name: str | None = None) -> ir.ValueType:
"""Lower one simple type. `simple_types` is the schema the type is
declared in (the main one, or an imported schema); `name` overrides
the type's own name, for an anonymous type hoisted under a name the
declaring schema gave it only by context."""
type_name = st.name if name is None else name
if isinstance(st.content, xsd.Union):
return self._union(st)
return self._union(type_name, st, simple_types)
if isinstance(st.content, xsd.ListType):
# MusicXML uses no xs:list; represent defensively as a token string.
return ir.StringType(st.name, "token", doc=st.doc)
primitive, facets = self._resolve_restriction(st.name)
return ir.StringType(type_name, "token", doc=st.doc)
primitive, facets = self._restriction(simple_types, st)
if facets.enumerations:
return ir.EnumType(
st.name, primitive, [e.value for e in facets.enumerations], st.doc
type_name, primitive, [e.value for e in facets.enumerations], st.doc
)
if primitive in _NUMERIC:
return ir.NumberType(
st.name,
type_name,
primitive,
facets.min_inclusive,
facets.max_inclusive,
Expand All @@ -191,7 +217,7 @@ def _value_type(self, st: xsd.SimpleType) -> ir.ValueType:
st.doc,
)
return ir.StringType(
st.name,
type_name,
primitive,
list(facets.patterns),
facets.min_length,
Expand All @@ -200,24 +226,24 @@ def _value_type(self, st: xsd.SimpleType) -> ir.ValueType:
st.doc,
)

def _resolve_restriction(self, type_name: str) -> tuple[str, xsd.Facets]:
def _restriction(self, simple_types: dict, st: xsd.SimpleType) -> tuple[str, xsd.Facets]:
"""A restriction's own facets merged onto its base chain."""
primitive, merged = self._resolve_restriction(simple_types, st.content.base)
_merge_facets(merged, st.content.facets)
return primitive, merged

def _resolve_restriction(self, simple_types: dict, type_name: str) -> tuple[str, xsd.Facets]:
"""Collapse a restriction chain to (primitive, merged facets). Child
facets override inherited ones; patterns accumulate."""
st = self.schema.simple_types.get(type_name)
st = simple_types.get(type_name)
if st is None or not isinstance(st.content, xsd.Restriction):
return _primitive(type_name), xsd.Facets()
base = st.content.base
if base in self.schema.simple_types:
primitive, merged = self._resolve_restriction(base)
else:
primitive, merged = _primitive(base), xsd.Facets()
_merge_facets(merged, st.content.facets)
return primitive, merged
return self._restriction(simple_types, st)

def _union(self, st: xsd.SimpleType) -> ir.UnionType:
def _union(self, name: str, st: xsd.SimpleType, simple_types: dict) -> ir.UnionType:
members: list[ir.UnionMember] = []
for m in st.content.member_types:
if m in self.schema.simple_types:
if m in simple_types:
members.append(ir.UnionMember(ir.Ref(m, "value")))
else:
members.append(ir.UnionMember(ir.Ref(_primitive(m), "primitive")))
Expand All @@ -234,13 +260,64 @@ def _union(self, st: xsd.SimpleType) -> ir.UnionType:

def _attr(self, a: xsd.Attribute) -> ir.Attr:
if a.ref:
ref = ir.Ref(_primitive(a.ref), "primitive")
ref = self._external_ref(a.ref)
name = a.ref
else:
ref = self._type_ref(a.type) if a.type else ir.Ref("string", "primitive")
name = a.name or ""
return ir.Attr(name, ref, a.use == "required", a.default, a.fixed, a.doc)

# ----- imported attributes --------------------------------------------- #

def _external_ref(self, ref_name: str) -> ir.Ref:
"""Resolve an imported attribute ref (xml:lang, xlink:type, ...) to the
type its declaration names.

The pinned refs in _EXTERNAL_ATTR never look at the import: their
declared type is an XML-Schema BUILTIN with nothing to read. The rest
are declared in the imported xml/xlink schemas, which the XSD parser
has already read beside the main one.
"""
pinned = _EXTERNAL_ATTR.get(ref_name)
if pinned is not None:
return ir.Ref(pinned, "primitive")
candidates = self.external_attrs.get(ref_name.split(":")[-1], [])
if len(candidates) != 1:
raise ValueError(
f"external attribute ref {ref_name!r} resolves to "
f"{len(candidates)} declarations in the imported schemas"
)
namespace, decl = candidates[0]
return self._external_decl_ref(namespace, decl, ref_name)

def _external_decl_ref(self, namespace: str, decl: xsd.Attribute, ref_name: str) -> ir.Ref:
"""The type an imported attribute declaration carries: a named type
from its own schema (xlink:typeType), or its inline type hoisted under
the qualified ref name (xml:space)."""
if decl.type:
return self._external_type_ref(namespace, decl.type)
if decl.inline_type is not None:
name = ref_name.replace(":", "-")
self._external_value(name, decl.inline_type, namespace)
return ir.Ref(name, "value")
return ir.Ref("string", "primitive")

def _external_type_ref(self, namespace: str, type_name: str) -> ir.Ref:
ext = self.schema.imported[namespace]
name = type_name.split(":")[-1]
st = ext.simple_types.get(name)
if st is None:
return ir.Ref(_primitive(type_name), "primitive")
self._external_value(name, st, namespace)
return ir.Ref(name, "value")

def _external_value(self, name: str, st: xsd.SimpleType, namespace: str) -> None:
"""Lower (once) a value type declared in an imported schema."""
if name not in self.external_values:
self.external_values[name] = self._value_type(
st, self.schema.imported[namespace].simple_types, name=name
)

# ----- complex types --------------------------------------------------- #

def _complex_type(self, name: str, ct: xsd.ComplexType) -> ir.ComplexType:
Expand Down
6 changes: 6 additions & 0 deletions gen/plates/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
# repairs them, a non-validating one ignores the flag.
_PRIM_NAME_TOKEN = {"id", "idref", "idrefs", "nmtoken", "nmtokens"}

# The primitives a validating target maps to a wrapper class rather than a raw
# target string: the name tokens plus the language tag (a builtin lexical
# space, like them, but not a name).
_PRIM_WRAPPER = _PRIM_NAME_TOKEN | {"language"}


def wrap_doc(doc: str | None, width: int) -> list[str]:
"""Greedy word-wrap of raw doc text at `width` (the wrapped TEXT width;
Expand Down Expand Up @@ -512,6 +517,7 @@ def _plate_ref(self, ref: ir.Ref) -> PlateRef:
if ref.name in _PRIM_NUMERIC
else "primitive-string",
name_token=ref.name in _PRIM_NAME_TOKEN,
wrapper=ref.name in _PRIM_WRAPPER,
unique_id=ref.name == "id",
)
if ref.category == "value":
Expand Down
5 changes: 5 additions & 0 deletions gen/plates/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class PlateRef:
# lexically narrower than a free string. A validating target renders a
# repairing wrapper for it; others ignore the flag. Neutral schema fact.
name_token: bool = False
# True when the target maps this primitive to a WRAPPER type whose
# value-style API (parse/toString) the rendered code uses instead of a raw
# target string. A superset of `name_token`: the identity tokens plus the
# language tag. Target binding, like `ident`.
wrapper: bool = False
# True when the referent is the xs:ID primitive, whose value must be unique
# within the document. Neutral schema fact.
unique_id: bool = False
Expand Down
1 change: 1 addition & 0 deletions gen/press/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ def plate_context(plates: Plates, plate) -> dict:
refs += [f.type_ref for f in items]
ctx["any_decimal"] = any(r.kind == "primitive-decimal" for r in refs)
ctx["any_name_token"] = any(r.name_token for r in refs)
ctx["any_language"] = any(r.wire == "language" for r in refs)
ctx["any_min1"] = any(f.min1 for f in items)
ctx["any_bounded"] = any(f.max is not None for f in items)
ctx["any_vector"] = any(f.cardinality == "vector" for f in items)
Expand Down
Loading
Loading