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
1 change: 1 addition & 0 deletions .agents/skills
3 changes: 3 additions & 0 deletions .claude/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# ignore memory items that claude might write when you get mad at it
projects/

# i have seen claude put something here when asked to use a worktree
worktrees/

# ignore this high churn file that is written whenever you allow a tool use
settings.local.json

Expand Down
286 changes: 73 additions & 213 deletions AGENTS.md

Large diffs are not rendered by default.

101 changes: 9 additions & 92 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})

option(MX_CORE_DEV "Build the core roundtrip (corert) test binary against the regenerated mx/core." OFF)

option(MX_API "Build the mx::api/mx::impl product library and its tests (Phase 2)." ON)
option(MX_API "Build the mx::api/mx::impl product library and its tests." ON)

option(MX_COVERAGE "Instrument the build for gcov coverage (GCC/Clang only)." OFF)

Expand Down Expand Up @@ -65,9 +65,9 @@ target_include_directories(mx_core PUBLIC ${PRIVATE_DIR})
target_link_libraries(mx_core PUBLIC pugixml)
target_compile_features(mx_core PUBLIC cxx_std_20)

# mx_core unit tests (Gate 4 construction-safety: clamping, factories,
# structural value shapes; W2 adds the grammar-shape tests). Only built once
# the generator has emitted (the tests exercise generated types).
# mx_core unit tests (clamping, factories, structural value shapes,
# grammar-shape tests). Only built once the generator has emitted (the
# tests exercise generated types).
if(MX_CORE_GENERATED_SOURCES)
file(GLOB_RECURSE SRC_MXTEST_CORE ${PRIVATE_DIR}/mxtest/core/*.cpp ${PRIVATE_DIR}/mxtest/core/*.h)
file(GLOB_RECURSE SRC_CPUL_CORE ${PRIVATE_DIR}/cpul/*.cpp ${PRIVATE_DIR}/cpul/*.h)
Expand Down Expand Up @@ -105,7 +105,7 @@ if(MX_CORE_DEV)
target_include_directories(mxtest-core-dev PRIVATE ${PRIVATE_DIR})
target_link_libraries(mxtest-core-dev mx_core ${CMAKE_THREAD_LIBS_INIT})

# mxtest-validate: the permanent Gate-2 tool (mx-core-plan.md §5.2).
# mxtest-validate: parses corpus files and serializes them for xmllint validation.
# Parses every eligible corpus file, serializes the OUTPUT to
# build/validate-out/, plus the default-constructed Document; `make
# validate-cpp` then xmllint-validates every output against the 4.0 XSD.
Expand All @@ -123,90 +123,10 @@ if(MX_CORE_DEV)

endif()

# Phase-2 step-0 baseline tooling (mx-impl-port-plan.md §8): the judge that
# scores old-master api-roundtrip captures with the same normalize/compare
# machinery corert uses. The capture driver next to it compiles only against
# old master (71fc402) and is committed as a record; both are excluded from
# the MX_API globs below.
option(MX_BASELINE_JUDGE "Build the Phase-2 step-0 baseline judge tool." OFF)
if(MX_BASELINE_JUDGE)
add_executable(mxtest-baseline-judge
${PRIVATE_DIR}/mxtest/api/baseline/JudgeMain.cpp
${PRIVATE_DIR}/mxtest/corert/Compare.cpp
${PRIVATE_DIR}/mxtest/corert/Compare.h
${PRIVATE_DIR}/mxtest/corert/Fixer.cpp
${PRIVATE_DIR}/mxtest/corert/Fixer.h
${PRIVATE_DIR}/mxtest/import/Normalize.cpp
${PRIVATE_DIR}/mxtest/import/Normalize.h
${PRIVATE_DIR}/mxtest/import/DecimalFields.h)
target_include_directories(mxtest-baseline-judge PRIVATE ${PRIVATE_DIR})
target_link_libraries(mxtest-baseline-judge pugixml)
endif()

# Phase-2 step-2/3 scratch target (mx-impl-port-plan.md §9): compiles the
# already-ported api/impl translation units as a static archive (an archive
# needs no symbol resolution, so the port can be validated TU by TU before
# all 77 files exist). Grows during the step-3 wave; retired when MX_API
# builds the real `mx` library.
option(MX_IMPL_SLICE "Compile the ported mx::impl slice (Phase-2 scratch target)." OFF)
if(MX_IMPL_SLICE)
add_library(mx_impl_slice STATIC
${PRIVATE_DIR}/mx/api/DocumentManager.cpp
${PRIVATE_DIR}/mx/api/EncodingData.cpp
${PRIVATE_DIR}/mx/api/MarkData.cpp
${PRIVATE_DIR}/mx/api/ScoreData.cpp
${PRIVATE_DIR}/mx/api/SoundID.cpp
${PRIVATE_DIR}/mx/impl/Converter.cpp
${PRIVATE_DIR}/mx/impl/Cursor.cpp
${PRIVATE_DIR}/mx/impl/EncodingFunctions.cpp
${PRIVATE_DIR}/mx/impl/LyricType.cpp
${PRIVATE_DIR}/mx/impl/MetronomeReader.cpp
${PRIVATE_DIR}/mx/impl/NoteReader.cpp
${PRIVATE_DIR}/mx/impl/NoteWriter.cpp
${PRIVATE_DIR}/mx/impl/ScoreConversions.cpp
${PRIVATE_DIR}/mx/impl/AccidentalMarkFunctions.cpp
${PRIVATE_DIR}/mx/impl/ArticulationsFunctions.cpp
${PRIVATE_DIR}/mx/impl/OrnamentsFunctions.cpp
${PRIVATE_DIR}/mx/impl/TechnicalFunctions.cpp
${PRIVATE_DIR}/mx/impl/ArpeggiateFunctions.cpp
${PRIVATE_DIR}/mx/impl/FermataFunctions.cpp
${PRIVATE_DIR}/mx/impl/LayoutFunctions.cpp
${PRIVATE_DIR}/mx/impl/NonArpeggiateFunctions.cpp
${PRIVATE_DIR}/mx/impl/PageTextFunctions.cpp
${PRIVATE_DIR}/mx/impl/SlideFunctions.cpp
${PRIVATE_DIR}/mx/impl/StaffFunctions.cpp
${PRIVATE_DIR}/mx/impl/TimeReader.cpp
${PRIVATE_DIR}/mx/impl/DynamicsReader.cpp
${PRIVATE_DIR}/mx/impl/DynamicsWriter.cpp
${PRIVATE_DIR}/mx/impl/TupletReader.cpp
${PRIVATE_DIR}/mx/impl/NoteFunctions.cpp
${PRIVATE_DIR}/mx/impl/NotationsWriter.cpp
${PRIVATE_DIR}/mx/impl/PropertiesWriter.cpp
${PRIVATE_DIR}/mx/impl/DirectionReader.cpp
${PRIVATE_DIR}/mx/impl/DirectionWriter.cpp
${PRIVATE_DIR}/mx/impl/MeasureReader.cpp
${PRIVATE_DIR}/mx/impl/MeasureWriter.cpp
${PRIVATE_DIR}/mx/impl/PartReader.cpp
${PRIVATE_DIR}/mx/impl/PartWriter.cpp
${PRIVATE_DIR}/mx/impl/ScoreReader.cpp
${PRIVATE_DIR}/mx/impl/ScoreWriter.cpp)
target_include_directories(mx_impl_slice PRIVATE ${PRIVATE_DIR})
target_include_directories(mx_impl_slice PUBLIC $<BUILD_INTERFACE:${PUBLIC_DIR}>)
target_link_libraries(mx_impl_slice PUBLIC mx_core)
endif()

if(MX_API)

# =========================================================================
# Phase 2 (the mx::impl port): the mx::api/mx::impl product library and
# its preserved test suites. These sources are restored from the
# pre-newgen tree and are NOT expected to compile until mx::impl is
# reimplemented against the new mx::core and DocumentManager is rewritten
# on pugixml/mx::core::parse (newgen-integration-plan.md §8). Default OFF;
# this block exists so the port has a build home from day one.
# =========================================================================

message(STATUS "${PROJECT_NAME}: MX_API=ON: Phase-2 mx::api/mx::impl build")
message(STATUS "${PROJECT_NAME}: MX_API=ON: building mx::api/mx::impl")

# PathRoot.h hands the api-test file walker the repo root at configure time.
file(WRITE ${PRIVATE_DIR}/mxtest/file/PathRoot.h
Expand All @@ -227,12 +147,9 @@ if(MX_API)
target_include_directories(mx PUBLIC $<BUILD_INTERFACE:${PUBLIC_DIR}>)
target_link_libraries(mx PUBLIC mx_core)

# The preserved api/impl suites (the Phase-2 acceptance gate) plus the
# harness infrastructure they depend on (file, control, and the old api
# import harness slated for a pugixml rewrite).
# The api/impl suites plus the harness infrastructure they depend on
# (file, control, and the api import harness).
file(GLOB_RECURSE SRC_MXTEST_API ${PRIVATE_DIR}/mxtest/api/*.cpp ${PRIVATE_DIR}/mxtest/api/*.h)
# mxtest/api/baseline/ holds the step-0 capture/judge tooling, not suite code.
list(FILTER SRC_MXTEST_API EXCLUDE REGEX "/mxtest/api/baseline/")
# CorpusRoundtripMain.cpp has its own main(); it is a separate binary.
list(FILTER SRC_MXTEST_API EXCLUDE REGEX "/mxtest/api/CorpusRoundtripMain\\.cpp")
file(GLOB_RECURSE SRC_MXTEST_CONTROL ${PRIVATE_DIR}/mxtest/control/*.cpp ${PRIVATE_DIR}/mxtest/control/*.h)
Expand Down Expand Up @@ -260,7 +177,7 @@ if(MX_API)
target_link_libraries(mxwrite mx ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(mxhide mx ${CMAKE_THREAD_LIBS_INIT})

# Corpus api roundtrip harness (mx-impl-port-plan.md §8).
# Corpus api roundtrip harness.
# Regression mode (CI): mxtest-api-roundtrip regression <dataRoot> <baselineFile>
# Discovery mode (manual): mxtest-api-roundtrip discovery <dataRoot>
add_executable(mxtest-api-roundtrip
Expand Down
22 changes: 9 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
# a dev machine runs natively with the host toolchain instead (used by the
# macOS CI job, which has no Docker). Requires CMake >= 3.13.
#
# The restored mx::api/mx::impl product stack is parked behind the CMake
# option MX_API=OFF until the Phase-2 port (newgen-integration-plan.md §8);
# no Makefile target builds it yet, so no advertised target is broken.
# ============================================================================

CMAKE ?= cmake
Expand Down Expand Up @@ -79,7 +76,7 @@ help:
@echo 'mx targets (see AGENTS.md). All run via the mx-sdk Docker toolchain;'
@echo 'set MX_RUNNING_IN_DOCKER=1 to run natively on the host instead.'
@echo ''
@echo ' mx::api/mx::impl (Phase 2):'
@echo ' mx::api/mx::impl:'
@echo ' make lib Build the mx static library (MX_API=ON).'
@echo ' make dev Build mx + mxtest + examples + api-roundtrip binary.'
@echo ' make test Run the mxtest suite (api/impl/file/control).'
Expand All @@ -92,8 +89,8 @@ help:
@echo ' make core-dev Build mx_core and the corert/unit/validate binaries.'
@echo " make test-core-dev Run the core roundtrip suite. Filter: ARGS='[core-roundtrip] lysuite/*'"
@echo ' make test-cpp-unit Run the mx::core unit tests (values, shapes, rejection suite).'
@echo ' make validate-cpp Gate 2: serialize every corpus file and xmllint-validate the OUTPUT.'
@echo ' make probe-cpp Gate 4: must-NOT-compile probes (invalid construction).'
@echo ' make validate-cpp Serialize every corpus file and xmllint-validate the output.'
@echo ' make probe-cpp Compile-time negative probes (invalid construction must not compile).'
@echo ' make check-core-dev fmt-check + warning-free core-dev build.'
@echo ' make coverage-core-dev Instrumented build, corert + unit suites, gcovr -> $(COV_DIR)/.'
@echo ''
Expand Down Expand Up @@ -215,10 +212,9 @@ test-core-dev: core-dev
test-cpp-unit: core-dev
$(BUILD_ROOT)/core-dev/mxtest-core $(ARGS)

# Gate 2 (docs/ai/design/mx-core-plan.md §5.2), a permanent gate: every
# parsed corpus document is serialized and the OUTPUT is validated against
# the MusicXML 4.0 XSD -- the mechanical proof that import leniency (value
# clamping) still emits only schema-valid XML.
# Serialize every parsed corpus document and xmllint-validate the output
# against the MusicXML 4.0 XSD -- mechanical proof that import leniency
# (value clamping) still emits only schema-valid XML.
validate-cpp: core-dev
rm -rf $(BUILD_ROOT)/validate-out
$(BUILD_ROOT)/core-dev/mxtest-validate $(BUILD_ROOT)/validate-out
Expand All @@ -229,9 +225,9 @@ validate-cpp: core-dev
--schema $(CURDIR)/docs/musicxml-4.0-ed15c23.xsd *.xml 2>/dev/null \
&& echo 'validate-cpp: all outputs are schema-valid.'

# Gate 4's compile-time probes (mx-core-plan.md §5.4): PROBE=0 must
# compile (the control); every numbered probe is an invalid-construction
# attempt that must NOT compile.
# Compile-time negative probes: PROBE=0 must compile (the control);
# every numbered probe is an invalid-construction attempt that must NOT
# compile.
PROBE_COUNT := 7
probe-cpp:
@$(CXX) -std=c++20 -fsyntax-only -I src/private -DPROBE=0 \
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ This project is a C++ library for working with MusicXML.

## Status

The `mx::core` typed model has been replaced by a generated implementation: a language-agnostic
code generator (`gen/`) reads the MusicXML 4.0 XSD and emits `mx::core` (see the
[Code Generation](#code-generation) section). The simplified `mx::api` layer and its `mx::impl`
implementation are fully ported to the new generated core (`docs/ai/design/mx-impl-port-plan.md`,
Phase 2 complete) and build by default.
The `mx::core` typed model is generated: a language-agnostic code generator (`gen/`) reads the
MusicXML 4.0 XSD and emits `mx::core` (see the [Code Generation](#code-generation) section). The
simplified `mx::api` layer and its `mx::impl` implementation are fully ported to the generated core
and build by default.

# Build

Expand Down
25 changes: 25 additions & 0 deletions data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,28 @@ The sidecars encode one uniform leniency policy, shared by every generated targe
false): the lenient parse the deserializer uses keeps the value verbatim, because unlike a
numeric bound there is no canonical replacement for a failed pattern, and round-trip fidelity
wins. No fixup sidecar therefore ever encodes a string substitution.

## Normalization pipeline

Before `corert` compares an expected document against the one `mx` re-serialized, it normalizes
both so the comparison is canonical-against-canonical. The pipeline lives in
`src/private/mxtest/corert/Compare.cpp` and `src/private/mxtest/import/Normalize.cpp`:

1. Pin the root `version` attribute to the harness baseline (`3.0`).
2. Strip whitespace-only text nodes from every element. Pretty-printing indentation is not content
(MusicXML has no mixed content), and the rule is applied to both sides, so it stays symmetric.
3. Strip trailing zeros from decimal fields: `mx` serializes the shortest round-trip form, so a
trailing-zero decimal like `40.00000` in a source file would otherwise mismatch. The field list
lives in `DecimalFields.h`.
4. Sort each element's attributes alphabetically by qualified name (`xlink:href`, not `href`); it
runs last.

Comparison rules that took debugging to get right:

- Compare each element's direct text only, never the subtree concatenation: a
numerically-equivalent leaf reformat would otherwise fail at every ancestor, not only the leaf.
- Compare attributes by qualified name, so a defect that drops a prefix (`xlink:href` -> `href`)
fails instead of sliding by on the local name.
- A document whose root declares a MusicXML version newer than the model's
`SupportedMusicXMLVersion` is skipped, not failed: it may use elements the generated model has no
types for.
2 changes: 1 addition & 1 deletion gen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class Config:
types: dict[str, str] = field(default_factory=dict) # primitive overrides
docs: DocsSection = field(default_factory=DocsSection)
renames: Renames = field(default_factory=Renames)
# The import-policy repair table (mx-core-plan.md §2.4): a required
# The import-policy repair table: a required
# attribute MISSING from a document gets this default injected by the
# parser; a missing required attribute with no entry is a parse error.
# Keyed (complex type wire, attribute wire) -> wire literal.
Expand Down
2 changes: 1 addition & 1 deletion gen/cpp/templates/defaults.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace {{vars.namespace}}
{

/// Gate-4 mechanical probe (plan §5.4): default-construct EVERY generated
/// Mechanical probe: default-construct EVERY generated
/// complex, group, and choice type, serialize it, and re-parse the result
/// with the strict parser. Throws ParseError if any type's natural zero
/// serializes something the strict grammar would reject.
Expand Down
2 changes: 1 addition & 1 deletion gen/ir/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def all_flat_elements(self, ct: ir.ComplexType) -> list[tuple[ir.Element, str]]:
# or a repeated/optional anonymous sequence. Trivial single-element
# groups are spliced inline (they add no structure, only an occurrence
# wrapper); structural groups stay referenced so targets can emit them
# as shared types (mx-core-plan.md §2.9). All of this is neutral schema
# as shared types. All of this is neutral schema
# reasoning -- nothing here knows what any target does with the view.

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion gen/plates/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class StringPlate:
# projected like enum variants (renameable, collision-gated) plus
# whether a non-empty suffix is required. Empty for any other pattern
# shape; see build.prefix_facets. A structural target stores the suffix
# and emits the prefix from the serializer (mx-core-plan.md §2.2).
# and emits the prefix from the serializer.
prefixes: list[Variant] = field(default_factory=list)
multi_prefix: bool = False
suffix_required: bool = False
Expand Down
2 changes: 1 addition & 1 deletion gen/tests/test_plates.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ def test_color_pattern_semantics(self):


class ContentProjection(unittest.TestCase):
"""The grammar-preserving content projection (mx-core-plan.md §2.9):
"""The grammar-preserving content projection:
fields, group/choice plates, synthesized hoisting, and its invisibility
to targets that consume only the flat member view."""

Expand Down
4 changes: 2 additions & 2 deletions src/include/mx/api/DocumentManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ using DocumentPtr = std::shared_ptr<Document>;

namespace api
{
// The mx::api error channel (mx-impl-port-plan.md §3): no exceptions escape
// this boundary; failures speak Result/ApiError, and any exception from
// The mx::api error channel: no exceptions escape this boundary; failures
// speak Result/ApiError, and any exception from
// below becomes ResultCode::internalError.
class DocumentManager
{
Expand Down
4 changes: 2 additions & 2 deletions src/include/mx/api/Result.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace mx
namespace api
{

// The mx::api error vocabulary (mx-impl-port-plan.md §3). mx::api owns its
// own codes: the core-boundary failures are mirrored (public headers never
// The mx::api error vocabulary. mx::api owns its own codes: the core-boundary
// failures are mirrored (public headers never
// include private mx::core headers), and the api adds the codes core has no
// business knowing. No exceptions escape the DocumentManager boundary.
enum class ResultCode
Expand Down
9 changes: 4 additions & 5 deletions src/include/mx/api/ScoreData.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ namespace mx
{
namespace api
{
// Frozen for source compatibility; read-side informational only
// (mx-impl-port-plan.md §4). The reader sets ThreePointZero iff the parsed
// document's declared version string is exactly "3.0". This field no longer
// influences output: mx writes MusicXML 4.0 documents, and the written root
// version attribute is always "4.0".
// Frozen for source compatibility; read-side informational only. The reader
// sets ThreePointZero iff the parsed document's declared version string is
// exactly "3.0". This field no longer influences output: mx writes MusicXML
// 4.0 documents, and the written root version attribute is always "4.0".
enum class MusicXmlVersion
{
unspecified,
Expand Down
14 changes: 6 additions & 8 deletions src/private/mx/api/DocumentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace
{

// mx::api owns its error vocabulary; the core parse codes are mirrored
// one-to-one (mx-impl-port-plan.md §3).
// one-to-one.
ResultCode mirror(core::ErrorCode code)
{
switch (code)
Expand Down Expand Up @@ -74,8 +74,7 @@ std::string fileExtension(const std::string &filePath)
return filePath.substr(dotPos + 1);
}

// The write side always emits version="4.0", unconditionally
// (mx-impl-port-plan.md §4): mx writes MusicXML 4.0 documents; echoing a
// The write side always emits version="4.0" unconditionally: echoing a
// declared "3.0" (or ScoreData::musicXmlVersion) from a 4.0 model was a
// fiction. Enforced here at the write boundary on a copy, so the stored
// document (and the getDocument escape hatch) keeps what was parsed.
Expand Down Expand Up @@ -230,7 +229,7 @@ Result<int> DocumentManager::createFromScore(const ScoreData &score)
catch (const impl::WriteRefusal &refusal)
{
// Refuse, don't drop: the ScoreData describes something the core
// model will not represent (mx-impl-port-plan.md §3).
// model will not represent.
return refusal.error();
}
catch (const std::exception &e)
Expand Down Expand Up @@ -312,10 +311,9 @@ Result<ScoreData> DocumentManager::getData(int documentId) const
return ApiError{ResultCode::badDocumentId, "", "getData: bad document id"};
}

// The old mutate-and-restore dance (convert the stored document to
// partwise, read, convert back) dies: convert into a local partwise
// copy and read that; the stored document is untouched
// (mx-impl-port-plan.md §5).
// Convert into a local partwise copy and read that; the stored
// document is untouched. The old mutate-and-restore dance (convert
// the stored document to partwise, read, convert back) is gone.
if (it->second->isScoreTimewise())
{
const core::ScorePartwise scorePartwise = impl::timewisePartwise(it->second->asScoreTimewise());
Expand Down
Loading
Loading