From 208dc18e26b92bff348886500a79d70022faa5fc Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 13 Jun 2026 09:09:00 +0200 Subject: [PATCH 01/20] chore: update .claude skills --- .claude/skills/arch/SKILL.md | 144 ++++++++++++++++++++++++++++++ .claude/skills/grill-me/SKILL.md | 26 ++++++ .claude/skills/questions/SKILL.md | 52 +++++++++++ 3 files changed, 222 insertions(+) create mode 100644 .claude/skills/arch/SKILL.md create mode 100644 .claude/skills/grill-me/SKILL.md create mode 100644 .claude/skills/questions/SKILL.md diff --git a/.claude/skills/arch/SKILL.md b/.claude/skills/arch/SKILL.md new file mode 100644 index 000000000..e652f6600 --- /dev/null +++ b/.claude/skills/arch/SKILL.md @@ -0,0 +1,144 @@ +--- +name: architect +description: Prime the model with senior architect principles during design or coding. Invoke when the user says "pay attention to the architecture", "think like an architect", or when working on design, module layout, or API contracts. +--- +# Architect Mode + +When this skill is invoked, shift into the mindset of a senior software architect. Hold these +principles as active constraints while writing code, suggesting approaches, or discussing design. +Flag violations when they appear, propose alternatives, and steer decisions toward good structure. +Be direct and opinionated. + +## mx Project Architecture + +The codegen program at `gen/` should be designed in such a way that novel use-cases and edge cases +in the desired code do not need to edit the python program. The plates layer and configuration layer +should provide enough flexibility to generate just about any code from the MusicXML XSD spec. + +### C++ Goals + +The `mx/core` layer should... + +Cardinal requirements: +- Spec correctness: it should be impossible to use the C++ code to construct a document that is not + valid to the Music XML 4.0 spec. +- Modern, safe C++. Use C++20. No bare pointers, no bare free or malloc. Smart pointers. S Meyers + Effective Modern C+ + +C++ code optimization priorities +1. Compile time +2. Runtime memory usage +3. Runtime speed +4. Binary size + +## Core Principles (in priority order) + +### 1. Domain Boundaries & Separation of Concerns + +Keep domain boundaries clear, concerns properly separated, and API contracts well-defined. When a +design muddles domains together or leaks responsibilities across boundaries, that is the primary +concern to raise before proceeding. + +### 2. Simple, Deep Abstractions with Information Hiding + +Information should be hidden inside simple, deep abstractions. Narrow interfaces with rich internals +are best. Flag leaking internals and wide, shallow interfaces. Prefer introducing abstractions +early, since good abstractions reduce future blast radius. + +### 3. Minimize Blast Radius of Change + +A single change of behavior should not affect many files all over the codebase. Make sure future +behavioral changes will be confined to the module that owns the behavior. Reject approaches that +spread change widely. Good modularity keeps most changes local. + +### 4. Clarity Over Cleverness + +Code must not be hard to understand or exhibit surprising behavior. Naming must reduce cognitive +burden. It should be obvious what something does from its name. Follow local conventions. Push back +hard on anything confusing or unexpected. + +## Architectural Positions + +### Monolith vs Services + +- Monoliths are fine and preferred for smaller teams. +- Tipping point is `~20+`` engineers in one repository. + +### Dependency Injection + +- DI should serve separation of concerns, information hiding, and simplicity. +- DI used solely to support unit testing is not a great use if it increases or cognitive load. +- DI where it genuinely simplifies the design. + +### Testing Strategy + +- Unit tests are table stakes but tell you almost nothing about whether the system works. +- End-to-end tests are the real payoff — test the system the way a customer uses it. +- Design for end-to-end testability as a first-class concern. + +### Event-Driven & Async Patterns + +- Queues, pub/sub, and event sourcing are a necessary evil acceptable when solving a real + architectural need — never because it's fashionable. +- If a simpler procedural approach works, prefer it. + +### Error Handling + +- Prefer explicit error handling (Rust-style Result types) where the ecosystem supports it. +- In languages where exceptions are idiomatic (e.g., Java), grudgingly accept them pragmatically. + +### Performance + +- Correctness and good design come first. +- Performance matters on hot paths but only after correctness is assured. +- A good design can be optimized later. + +### Configuration & Feature Flags + +- Configuration is behavioral surface area. Never expose it unless you must. +- Unnecessary configuration paints you into a corner when customers depend on it. +- Feature flags are a necessary evil for migrations — separate from config, not customer-facing. + +### API Contracts & Code Generation + +- Generate from a single spec whenever possible (OpenAPI, protobuf, XSD, Smithy, etc.). +- A single source of truth for API surfaces is critical. + +### Data Ownership + +- Greenfield: each service owns its data, or merge the systems. +- Legacy: be pragmatic about existing databases. + +### Backwards Compatibility + +- Breaking changes must come with clear migration paths. +- Customers must not be painfully impacted. + +### Shared Libraries + +- Consistent library use across a codebase is preferred for consistency and binary size. + +### Composition vs Inheritance + +- Lean toward composition, but inheritance has excellent use cases. + +### Observability + +- Leave the door open if it doesn't harm the design. Don't compromise design quality for it. + +## When to Flag for Splitting a Module + +- Excessive size +- Interface scope growing too wide +- Internals leaking out +- High code churn (many unrelated changes hitting the same module) + +## Behavior in This Mode + +- Apply these principles as a continuous lens, not a one-shot review. +- When writing or suggesting code, favor the architecturally sound path without being asked. +- When a decision point arises, name the tradeoff and state a recommendation. +- Frame concerns as: "This would [violate principle / increase blast radius / leak internals] + because [reason]. Consider [alternative]." +- If the current direction is already good, say so and proceed — don't invent problems. +- Always ask: "Will this keep changes local and the system understandable as it grows?" diff --git a/.claude/skills/grill-me/SKILL.md b/.claude/skills/grill-me/SKILL.md new file mode 100644 index 000000000..e3d9233bf --- /dev/null +++ b/.claude/skills/grill-me/SKILL.md @@ -0,0 +1,26 @@ +--- +name: grill-me +description: > + Interview the user relentlessly about a plan or design until reaching + shared understanding, resolving each branch of the decision tree. Use + when user wants to stress-test a plan, get grilled on their design, or + mentions "grill me". +argument-hint: "" +disable-model-invocation: false +user-invocable: true +--- + +Never use the `AskUserQuestion` tool. Never render a numbered option picker. Ask every question as +plain text in the chat, then stop and wait for the answer. + +Interview me relentlessly about every aspect of this plan until +we reach a shared understanding. Walk down each branch of the design +tree resolving dependencies between decisions one by one. + +If a question can be answered by exploring the codebase, explore +the codebase instead. + +For each question, provide your recommended answer. + +Use the /questions skill to avoid sending more than one question at +once. diff --git a/.claude/skills/questions/SKILL.md b/.claude/skills/questions/SKILL.md new file mode 100644 index 000000000..31bd459eb --- /dev/null +++ b/.claude/skills/questions/SKILL.md @@ -0,0 +1,52 @@ +--- +name: questions +description: > + Ask the user clarifying questions one at a time to refine a plan or + task. Invoke with `/questions` or automatically when more information + is needed. +--- +# /questions + +## Non-negotiable: plain chat only + +Never use the `AskUserQuestion` tool. Never render a numbered option picker. Ask every question as +plain text in the chat, then stop and wait. + +## The core rule: one question per turn + +Ask exactly **one question**, then stop and wait for the answer. Do not bundle multiple questions +into one turn — not as a numbered list, not as "and also," not as a parenthetical follow-up. The +user answers one question at a time; batching forces them to scroll back and juggle context, and +answers get lost. + +This rule holds even when several questions feel related or obvious. One turn, one question. + +**Wrong:** + +> A few things to clarify: +> +> 1. What's the target platform? +> 2. Should it support offline mode? +> 3. What's the expected user count? + +**Right:** + +> What's the target platform? + +*(wait for answer, then next turn:)* + +> Got it. Does it need to work offline? + +## Usage + +- `/questions` +- `/questions ` — e.g., `/questions about the design of the flubber async module` + +## Flow + +1. Ask one question, grounded in existing context and the optional prompt. If there's no context, + open with "What would you like to work on?" +2. Wait for the answer. Use it to shape the next question. +3. Repeat until the user says to stop. +4. When they stop, produce a plan summarizing their answers. If it's unclear what they want done + with the plan, ask — one question. From a8ad2970677868a6a3dea0b61c3e28f6135ff970 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 13 Jun 2026 09:09:06 +0200 Subject: [PATCH 02/20] docs: vendor MusicXML XSD and sounds specs --- ...{musicxml.xsd => musicxml-3.0-5fd8eb3.xsd} | 8 +- ...icxml-3.1.xsd => musicxml-3.1-8bbe8e5.xsd} | 2 +- docs/{sounds.xml => sounds-3.0-5fd8eb3.xml} | 6 +- docs/sounds-3.1-8bbe8e5.xml | 924 +++++++++++++++++ docs/sounds-4.0-ed15c23.xml | 932 ++++++++++++++++++ 5 files changed, 1864 insertions(+), 8 deletions(-) rename docs/{musicxml.xsd => musicxml-3.0-5fd8eb3.xsd} (98%) mode change 100755 => 100644 rename docs/{musicxml-3.1.xsd => musicxml-3.1-8bbe8e5.xsd} (98%) mode change 100755 => 100644 rename docs/{sounds.xml => sounds-3.0-5fd8eb3.xml} (96%) create mode 100644 docs/sounds-3.1-8bbe8e5.xml create mode 100644 docs/sounds-4.0-ed15c23.xml diff --git a/docs/musicxml.xsd b/docs/musicxml-3.0-5fd8eb3.xsd old mode 100755 new mode 100644 similarity index 98% rename from docs/musicxml.xsd rename to docs/musicxml-3.0-5fd8eb3.xsd index 2e602eadf..76701dd43 --- a/docs/musicxml.xsd +++ b/docs/musicxml-3.0-5fd8eb3.xsd @@ -5,12 +5,12 @@ Version 3.0 -Copyright © 2004-2011 Recordare LLC. -http://www.recordare.com/ +Copyright © 2004-2011 MakeMusic, Inc. +http://www.makemusic.com/ This MusicXML™ work is being provided by the copyright holder under the MusicXML Public License Version 3.0, available from: - http://www.recordare.com/dtds/license.html + http://www.musicxml.org/dtds/license.html This is the W3C XML Schema (XSD) version of the MusicXML 3.0 language. Validation is tightened by moving MusicXML definitions from comments into schema data types and definitions. Character entities and other entity usages that are not supported in W3C XML Schema have been removed. The features of W3C XML Schema make it easier to define variations of the MusicXML format, either via extension or restriction. @@ -18,7 +18,7 @@ This file defines the MusicXML 3.0 XSD, including the score-partwise and score-t - The MusicXML 3.0 DTD has no namespace, so for compatibility the MusicXML 3.0 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "http://www.musicxml.org/xsd/MusicXML" as the namespace. + The MusicXML 3.0 DTD has no namespace, so for compatibility the MusicXML 3.0 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "MusicXML" as the namespace. diff --git a/docs/musicxml-3.1.xsd b/docs/musicxml-3.1-8bbe8e5.xsd old mode 100755 new mode 100644 similarity index 98% rename from docs/musicxml-3.1.xsd rename to docs/musicxml-3.1-8bbe8e5.xsd index 081d9c71c..c10bb3c56 --- a/docs/musicxml-3.1.xsd +++ b/docs/musicxml-3.1-8bbe8e5.xsd @@ -19,7 +19,7 @@ This file defines the MusicXML 3.1 XSD, including the score-partwise and score-t - The MusicXML 3.1 DTD has no namespace, so for compatibility the MusicXML 3.1 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "http://www.musicxml.org/xsd/MusicXML" as the namespace. + The MusicXML 3.1 DTD has no namespace, so for compatibility the MusicXML 3.1 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "MusicXML" as the namespace. diff --git a/docs/sounds.xml b/docs/sounds-3.0-5fd8eb3.xml similarity index 96% rename from docs/sounds.xml rename to docs/sounds-3.0-5fd8eb3.xml index b604c19a8..067ce566f 100644 --- a/docs/sounds.xml +++ b/docs/sounds-3.0-5fd8eb3.xml @@ -6,14 +6,14 @@ Version 3.0 - Copyright © 2004-2011 Recordare LLC. - http://www.recordare.com/ + Copyright © 2004-2011 MakeMusic, Inc. + http://www.makemusic.com/ This MusicXML™ work is being provided by the copyright holder under the MusicXML Public License Version 3.0, available from: - http://www.recordare.com/dtds/license.html + http://www.musicxml.org/dtds/license.html Starting with Version 3.0, the MusicXML format includes a standard set of instrument sounds to identify musical diff --git a/docs/sounds-3.1-8bbe8e5.xml b/docs/sounds-3.1-8bbe8e5.xml new file mode 100644 index 000000000..3191870a5 --- /dev/null +++ b/docs/sounds-3.1-8bbe8e5.xml @@ -0,0 +1,924 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/sounds-4.0-ed15c23.xml b/docs/sounds-4.0-ed15c23.xml new file mode 100644 index 000000000..bb09ef62f --- /dev/null +++ b/docs/sounds-4.0-ed15c23.xml @@ -0,0 +1,932 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fcaa41249efafd59ef6bd2223729d9dcad1d6341 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 13 Jun 2026 10:12:08 +0200 Subject: [PATCH 03/20] gen: code generator --- gen/AGENTS.md | 112 + gen/DESIGN.md | 549 + gen/README.md | 43 + gen/__init__.py | 1 + gen/__main__.py | 280 + gen/config.py | 501 + gen/generate.py | 13370 ---------------- gen/ids.py | 51 - gen/ir/__init__.py | 7 + gen/ir/build.py | 390 + gen/ir/dump.py | 56 + gen/ir/model.py | 212 + gen/ir/resolve.py | 401 + gen/ir/sounds.py | 101 + gen/names.py | 182 + gen/naming.base.toml | 19 + gen/parse.py | 649 - gen/plates/__init__.py | 29 + gen/plates/build.py | 1044 ++ gen/plates/check.py | 216 + gen/plates/model.py | 498 + gen/press/__init__.py | 5 + gen/press/context.py | 262 + gen/press/engine.py | 399 + gen/press/render.py | 235 + gen/press/writer.py | 102 + gen/quality.py | 12 +- gen/schema/config.toml | 35 + gen/schema/out/musicxml.schema.json | 4264 +++++ .../templates/musicxml.schema.json.tmpl | 68 + gen/tests/__init__.py | 0 gen/tests/mustache_spec/README.md | 9 + gen/tests/mustache_spec/comments.json | 106 + gen/tests/mustache_spec/interpolation.json | 422 + gen/tests/mustache_spec/inverted.json | 227 + gen/tests/mustache_spec/partials.json | 153 + gen/tests/mustache_spec/sections.json | 423 + gen/tests/test_agnosticism.py | 71 + gen/tests/test_ir.py | 330 + gen/tests/test_plates.py | 836 + gen/tests/test_press.py | 130 + gen/tests/test_render.py | 321 + gen/tests/test_schema.py | 75 + gen/tests/test_writer.py | 63 + gen/xsd/__init__.py | 6 + gen/xsd/analyze.py | 462 + gen/xsd/model.py | 229 + gen/xsd/parser.py | 258 + 48 files changed, 14138 insertions(+), 14076 deletions(-) create mode 100644 gen/AGENTS.md create mode 100644 gen/DESIGN.md create mode 100644 gen/README.md create mode 100644 gen/__init__.py create mode 100644 gen/__main__.py create mode 100644 gen/config.py delete mode 100644 gen/generate.py delete mode 100644 gen/ids.py create mode 100644 gen/ir/__init__.py create mode 100644 gen/ir/build.py create mode 100644 gen/ir/dump.py create mode 100644 gen/ir/model.py create mode 100644 gen/ir/resolve.py create mode 100644 gen/ir/sounds.py create mode 100644 gen/names.py create mode 100644 gen/naming.base.toml delete mode 100644 gen/parse.py create mode 100644 gen/plates/__init__.py create mode 100644 gen/plates/build.py create mode 100644 gen/plates/check.py create mode 100644 gen/plates/model.py create mode 100644 gen/press/__init__.py create mode 100644 gen/press/context.py create mode 100644 gen/press/engine.py create mode 100644 gen/press/render.py create mode 100644 gen/press/writer.py create mode 100644 gen/schema/config.toml create mode 100644 gen/schema/out/musicxml.schema.json create mode 100644 gen/schema/templates/musicxml.schema.json.tmpl create mode 100644 gen/tests/__init__.py create mode 100644 gen/tests/mustache_spec/README.md create mode 100644 gen/tests/mustache_spec/comments.json create mode 100644 gen/tests/mustache_spec/interpolation.json create mode 100644 gen/tests/mustache_spec/inverted.json create mode 100644 gen/tests/mustache_spec/partials.json create mode 100644 gen/tests/mustache_spec/sections.json create mode 100644 gen/tests/test_agnosticism.py create mode 100644 gen/tests/test_ir.py create mode 100644 gen/tests/test_plates.py create mode 100644 gen/tests/test_press.py create mode 100644 gen/tests/test_render.py create mode 100644 gen/tests/test_schema.py create mode 100644 gen/tests/test_writer.py create mode 100644 gen/xsd/__init__.py create mode 100644 gen/xsd/analyze.py create mode 100644 gen/xsd/model.py create mode 100644 gen/xsd/parser.py diff --git a/gen/AGENTS.md b/gen/AGENTS.md new file mode 100644 index 000000000..f73b9211a --- /dev/null +++ b/gen/AGENTS.md @@ -0,0 +1,112 @@ +# gen/ agent instructions + +The `gen/` directory is a Python code generator (`python3 -m gen`) that reads a MusicXML XSD +and emits typed serialization/deserialization libraries. Four targets: C++ (product, `gen/cpp/`), +Go (`gen/test/go/`), C (`gen/test/c/`), JSON Schema (`gen/schema/`). Generated output is committed +to the tree. For design rationale, see `gen/DESIGN.md`. + +## Commands + +``` +python3 -m gen analyze [xsd] # structural analysis report +python3 -m gen ir [--type NAME] [--resolve] [--config C] # lower XSD to IR, print as JSON +python3 -m gen plates --config C [--type NAME] [--check] # project IR onto target, print +python3 -m gen render --config C --type NAME # render one type to stdout +python3 -m gen # full emit: render the target +``` + +- `--resolve` prints the collapsed IR view (attribute groups flattened, group refs spliced). +- `--config C` applies that target's companion patches before lowering. +- `--check` on `plates` validates renames and detects identifier collisions; exits non-zero -- a CI gate. +- Full emit shortcuts: `make gen-cpp` (C++ only), `make gen` (all targets). + +## Quality gates + +Run all of these after any generator change: + +``` +make test-gen # unit tests + test_agnosticism.py + regen + git diff --exit-code +make gen-check # plates --check for all targets (rename validation, collision detection) +make gen-quality # design-quality scorer (floor enforced; see quality.py) +make gen-lint # pylint (config: .pylintrc) +``` + +After changing a target's `config.toml` or `templates/`, also run that target's corert suite +(`make test-cpp`, `make test-go`, `make test-c`). + +If generated output changes, commit the new generated files alongside the Python change. + +## Cardinal rules + +**1. The generator is language-agnostic.** Adding or modifying a target must not require editing +any `*.py` file outside that target's own directory. Language knowledge lives in the target's +`config.toml` and `templates/` -- never in `*.py`. Enforced by `gen/tests/test_agnosticism.py`. +If a template needs new dispatch, add a discriminant flag or context field in +`gen/press/context.py`; language tables or per-language branches in Python are forbidden. Language- +specific data (namespace, package, fn_prefix) belongs under `[vars]` in `config.toml` and passes +through as `{{target.vars.namespace}}`. + +**2. The IR is a pure function of the XSD.** No configurable knobs may enter `gen/ir/`. The +`sounds.xml` fold is the one documented exception (opt-in input selection; runs before IR is handed +to Plates). + +**3. Plates carry decisions; templates stay dumb.** If a template needs to branch on a naming or +structural fact, that fact belongs in `gen/plates/` or `gen/press/context.py`, not in template +text. The Mustache engine has no logic: no expressions, no filters, no assignments. + +**4. Fail loud.** Missing template keys, validation errors, rename mismatches, identifier +collisions -- all exit non-zero with a clear `template:line` message. + +## Workflows + +**Modifying IR or Plates** -- change `gen/ir/` or `gen/plates/`. Run `make test-gen` and +`make gen-check`. Commit updated output as a separate hunk or commit from the Python change. + +**Modifying a target** -- edit only that target's `config.toml` and `templates/`. Regenerate, +run the target's corert, commit the updated output. + +**Adding a target** -- create a new directory with `config.toml` and `templates/`. Run +`make test-gen` to confirm `test_agnosticism.py` passes. + +**Renaming a schema type or element** -- use `[rename.type]`, `[rename.element]`, or +`[rename.attribute]` in the target's `config.toml`. Keys are validated against the IR; a typo or +stale key is a build error. + +**Debugging a template** -- `python3 -m gen render --config C --type note` renders one plate to +stdout. The Mustache engine must continue to pass the spec test suite (`gen/tests/test_press.py`). + +## Layout + +``` +gen/ + __main__.py CLI entry point + config.py typed config.toml loader + names.py tokenizer, convention registry, sanitizer + naming.base.toml renames shared across all targets (segno/coda collision fix) + quality.py design-quality scorer + .pylintrc pylint config + xsd/ XSD parser (model.py, parser.py, analyze.py) -- no external deps + ir/ IR model, lowering (build.py), Resolver (resolve.py), sounds fold + plates/ projection: model.py, build.py, check.py + press/ engine.py, context.py, render.py, writer.py + tests/ test suite including test_agnosticism.py and test_press.py + cpp/ C++ target: config.toml + templates/ + schema/ JSON Schema target: config.toml + templates/ + out/ + test/go/ Go corert target + harness + test/c/ C corert target + harness +``` + +## Key files by task + +| Task | Files | +|------------------------------|-----------------------------------------------------| +| IR shape or resolution | `gen/ir/model.py`, `gen/ir/build.py`, `gen/ir/resolve.py` | +| Plates projection | `gen/plates/model.py`, `gen/plates/build.py` | +| Collision detection | `gen/plates/check.py` | +| Naming / tokenizer | `gen/names.py` | +| Mustache engine | `gen/press/engine.py` | +| Context building | `gen/press/context.py` | +| Manifest / write | `gen/press/render.py`, `gen/press/writer.py` | +| C++ target | `gen/cpp/config.toml`, `gen/cpp/templates/` | +| Agnosticism enforcement | `gen/tests/test_agnosticism.py` | +| Config schema | `gen/config.py` | diff --git a/gen/DESIGN.md b/gen/DESIGN.md new file mode 100644 index 000000000..c9c20ef21 --- /dev/null +++ b/gen/DESIGN.md @@ -0,0 +1,549 @@ +# Generator Design (`gen/`) + +This document covers the full design of the `gen/` code generator: goals, pipeline stages, +design principles, IR, Plates, press, and the cardinal agnosticism rule. It is the primary +reference for anyone reasoning about the architecture or making structural decisions. + +For the hot path -- commands, gates, rules to obey while coding -- see `gen/AGENTS.md`. For a +brief human overview, see `gen/README.md`. + +## Goals + +`mx::core` was originally hand-generated by human brute-force from the MusicXML 3.0-era XSD +(Ruby scripts, then an abandoned Rust attempt). The library was stuck near MusicXML 3.0: the +types could not be reliably regenerated from a newer spec without re-doing that brute-force +work. + +The `gen/` pipeline is the working replacement. Its goals: + +- Emit `mx::core` from the MusicXML 4.0 XSD as a regenerable artifact, so schema upgrades are a + `make gen` away. +- Human design choices (renames, import defaults, bespoke type rows) live in `config.toml`, + not in generated files. +- The generator is language agnostic: adding a new target must not require editing any + generator Python. Language knowledge lives in the target's directory. +- Code generation is bespoke: the goal is to produce what the `mx` library needs from the + MusicXML specification, not to compile arbitrary XSD. + +Secondary targets (Go, C, JSON Schema) exist to keep the agnosticism architecture honest, not +as production deliverables. + +## Pipeline + +``` +XSD -> XSD model -> IR -> Plates -> press -> output files + (gen/xsd) (gen/ir) (gen/plates) (gen/press) +``` + +1. **Parse** (`gen/xsd/`): reads the XSD into a model mirroring it 1:1, still speaking XSD: + restriction chains, attribute-group references, anonymous inline types. + +2. **Lower** (`gen/ir/`): resolves that into the intermediate representation (IR): a flat, + fully-named, dependency-ordered model in code-generation terms. A pure function of the XSD + (see Design principles). No configurable knobs. + +3. **Project** (`gen/plates/`): binds the IR to one target. Every per-target decision -- + identifier casings, renames, primitive type mappings, emit strategies, file layout -- is made + here, once, producing one **plate** per emitted type. The collection projected for a target + is the **Plates**. + +4. **Render** (`gen/press/`): the target's own Mustache templates, declared in its `[render]` + manifest, are pressed against contexts built from the plates. + +5. **Write** (`gen/press/writer.py`): deterministic, write-if-changed, marker-gated pruning. + Same IR + config always yields byte-identical output. + +## Design principles + +**Generate by shape, not by element.** Every type falls into one of eight shapes (four value, +four complex). One template per shape; no per-element special casing. + +**The IR is a pure, canonical function of the XSD.** All schema-specific reasoning -- resolving +references, ordering, dead-code removal, naming -- happens once, in the IR, shared by every +target. Per-language choices (inheritance vs flattening, mixins vs inlined attributes) belong +to the emitter. The IR takes no configuration. + +**Resolve, but preserve names.** The IR computes every resolved answer (effective primitives, +cardinalities, dependency order) yet keeps the schema's named structure (aliases, inheritance +edges, model groups, attribute groups) so each emitter can decide how much to collapse. + +**One resolution, shared.** The collapsed form most emitters actually want -- attribute groups +flattened, model-group refs spliced, a derived type's full attribute set -- is computed on +demand by `ir/resolve.py`, so the splicing-and-deduping reasoning lives once rather than once +per language. + +**Plates carry decisions; templates stay dumb.** Templates walk a structure and print text. +They contain no naming logic and no per-element conditionals. If a template needs to make a +decision, that decision belongs in the projection. + +**Fail loud.** Missing keys, validation errors, rename mismatches, identifier collisions -- all +exit non-zero with a clear message. No silent fallbacks. + +## The XSD layer (`gen/xsd/`) + +Parses the MusicXML XSD into a model mirroring the XSD subset MusicXML uses: `simpleType`, +`complexType`, `group`, `attributeGroup`, `restriction`, `extension`, `simpleContent`, +`complexContent`, `facet`, `particle`, anonymous inline types. No external dependencies; +uses Python's `ElementTree`. + +The output is a 1:1 XSD mirror -- it does not resolve, rename, or classify. That is the IR's +job. + +## The Intermediate Representation (`gen/ir/`) + +### Value types (4 kinds) + +- **enum** -- a closed set of allowed string tokens, e.g. `step` = {A..G}. IR fields: `base` + (usually `token`/`string`) and `values`. Emits an enum class plus wire<->variant lookup + tables. +- **number** -- a numeric value whose primitive is `decimal`/`integer`/`positive_integer`/ + `non_negative_integer`, with optional bounds. Emits a numeric wrapper that range-validates on + assignment. +- **string** -- a text value with optional `patterns` and length constraints. Emits a string + wrapper with an optional pattern check. +- **union** -- a value that may be any one of several member types or inline literal sets, e.g. + `number-or-normal` = decimal | "normal". Emits a small tagged variant. The open-string member + (if any) must be last; a schema that orders one earlier is a build error. + +### Complex types (4 kinds) + +- **value** -- a typed text body plus attributes (from XSD `simpleContent`), e.g. + `accidental-text`. Emits a class with a `value` field plus attribute fields. +- **composite** -- child elements in sequences/choices, plus attributes. The structural + workhorse, e.g. `note`. Emits a class with one member per child element (cardinality: + required/optional/vector) in schema order. +- **empty** -- an element with no child elements. Two sub-cases: presence-only (maps to a bool) + and attributes-only (attributes but no children). +- **derived** -- extends another complex type and adds attributes (from XSD `complexContent`). + Emits inheritance, or a flattened copy where the language has none. + +### Synthesized types + +Seven complex types the IR creates by naming and hoisting anonymous XSD types: +`score-partwise`, `score-timewise`, `partwise-part`, `partwise-measure`, `timewise-part`, +`timewise-measure`, `directive`. The part/measure pairs are context-qualified because the +partwise and timewise hierarchies give them genuinely different shapes -- the one place +element name -> type is not 1:1. + +### Dead types + +Five named XSD types referenced by nothing are dropped and reported: +`positive-decimal`, `start-stop-change-continue`, `formatted-symbol`, `empty-print-style`, +`empty-print-style-align`. Verified by direct text search of the XSD. + +### Resolution layer (`gen/ir/resolve.py`) + +The IR preserves the schema's named structure. `Resolver` collapses it on demand: + +- `attributes(ct)` -- own attributes with `attribute_groups` expanded inline, deduped. +- `all_attributes(ct)` -- `attributes(ct)` plus the base chain's attributes (base-most first). +- `content(ct)` -- `ct.content` with every model-group ref spliced in; a self-contained + sequence/choice tree with no `group` nodes left. +- `elements(ct)` -- every element occurrence in the resolved content, in document order. +- `flat_elements(ct)` -- each distinct element name with its *effective* cardinality: + repeated wrappers make vectors, choices demote to optional, duplicate occurrences of one name + merge by co-occurrence analysis. +- The field view (`normalized`, `field_nodes`, `trivial_group`, `group_shape`, `first_names`, + `nullable`) -- the grammar-preserving counterpart of `flat_elements`: trivial single-element + groups splice inline (bounds multiplied through), structural groups stay referenced, each + field node carries dispatch facts (first-name sets, nullability) a strict in-order parser + needs. The Plates project this as `ComplexPlate.fields` and `Plates.groups`. + +The Resolver is on-demand (pure over the IR) because it is needed mid-build to compute `deps`. + +### Ordering + +Both `value_types` and `complex_types` are emitted deps-first: a type's dependencies always +precede it. The list order IS the topological order -- no separate rank field. Value types +never reference complex types, so concatenating `value_types` then `complex_types` is a valid +total order for a single-file emit. + +### Two load-bearing XSD invariants + +Run `python3 -m gen analyze` to verify. Both hold for MusicXML 3.0, 3.1, 4.0, and 4.1: + +1. **The complex-type graph is a DAG.** Zero cycles. Generated code can use plain by-value + members, emit types in topological order, and skip forward declarations and heap indirection + entirely -- removing the hardest problem in typed-XML codegen. + +2. **No element-name collisions.** Every element name maps to exactly one type. Parse/serialize + dispatch is a flat name -> type table with no context-sensitive resolution. + +Because these are empirical, not guaranteed by the XSD standard, `analyze` runs as a CI gate: +a future schema introducing a cycle or name collision fails the build before the value-type and +flat-dispatch assumptions silently break. + +### MusicXML 4.0 inventory + +145 simpleTypes, 224 complexTypes, 27 model groups, 45 attribute groups, 2 document roots. 440 +distinct element names across 478 declaration sites. 351 attribute declarations (60 required). + +## The Plates (`gen/plates/`) + +### Name and rationale + +In music engraving, a publisher prepares an edition by engraving the manuscript onto metal +plates: every spelling, layout, and spacing decision committed into the metal, one plate per +page, ready for the press to ink and print. The metaphor maps exactly: + +- The IR is the abstract manuscript: neutral content, no typeface, no layout. +- A **plate** is one type engraved for a specific target: the same content in that target's + concrete identifiers, order, and file layout. +- The **Plates** are the complete set of plates for one target (one edition). +- The templates are the press: they ink and print what the plates already fixed. No composition + decisions of their own. + +### One layer, two field groups + +The Plates are one rich object, but each plate is internally partitioned: + +- a **neutral core** -- wire-faithful, target-independent facts (wire name, shape, resolved + structure, value lists, facets, docs), mirrored from the IR + Resolver. +- a **target binding** -- the per-target overlay (casing bundle, resolved target types, emit + strategy tags, file assignment, reserved-word resolution). + +Code targets read both groups. A neutral target (JSON Schema) reads only the neutral core and +leaves the binding's optional pieces unconfigured. + +**Why one object, not two artifacts.** Two separate objects would force every template to +cross-reference them by name and re-walk the structure to stitch them -- reintroducing exactly +the per-emitter splicing the IR worked to centralize. One object with a disciplined field split +gives the ergonomics of one tree and the generality proof of two. + +### Name model + +#### Tokenizer + +A wire name is split into a lowercase word vector, then recased per convention. + +Rules in order: +1. Split on separators: hyphen, dot, underscore, colon, ASCII whitespace. +2. Case-transition splits: lower-to-upper boundary; acronym boundary (uppercase run followed by + uppercase + lowercase; e.g. `MIDIChannel` -> `midi`, `channel`). +3. Digits do not split: a letter-digit boundary is not a word boundary. +4. Lowercase each resulting word. +5. Degenerate input (empty vector): substitute the configured fallback word, default `"empty"`. + The wire form stays `""`. + +#### Five standard casings + +- **PascalCase**: capitalize every word, concatenate. +- **camelCase**: first word lowercased, later words capitalized. +- **snake_case**: words joined with `_`. +- **kebab-case**: words joined with `-`. +- **SCREAMING_SNAKE_CASE**: words uppercased, joined with `_`. + +"Capitalize a word": if the word is in the acronym set (`midi`, `id`, `xml`, `css`, `smufl`, +`uri`, `url` by default), uppercase it whole; else capitalize the first letter. Acronyms affect +only PascalCase and non-leading camelCase words. The set is config-extensible via +`[naming] acronyms`. + +#### Worked conversion table + +| wire | words | pascal | snake | +|---------------------|---------------------------|---------------------|------------------------| +| `note` | [note] | `Note` | `note` | +| `default-x` | [default, x] | `DefaultX` | `default_x` | +| `midi-channel` | [midi, channel] | `MIDIChannel` | `midi_channel` | +| `optional-unique-id`| [optional, unique, id] | `OptionalUniqueID` | `optional_unique_id` | +| `brass.alphorn` | [brass, alphorn] | `BrassAlphorn` | `brass_alphorn` | +| `up down` | [up, down] | `UpDown` | `up_down` | +| `1024th` | [1024th] | `1024th` | `1024th` | +| `` (empty) | [empty] | `Empty` | `empty` | + +`1024th` yields a non-identifier casing; the sanitizer handles it (see below). The wire `""` and +the recased `1024th` are both retained; casing is never silently changed to make it legal. + +### Override system + +Two tiers: + +- **(a) Fundamental rename.** Rename the canonical root; every casing re-expands automatically. + `attributes` -> `properties` makes PascalCase `Properties`, snake `properties`, etc. +- **(b) Per-convention override.** Override one flavor, leave the rest auto-expanded. Keeping + root `note`, forcing `pascal = MusicNote`, leaves `snake_case` as `note`. + +Addressing: `rename.type.`, `rename.element.`, +`rename.attribute.`, `rename.attribute..` (scoped beats +global), `rename.enum-value..`. + +All keys are validated against the IR at build time. A typo or a key left stale after a schema +bump is a build error. + +A shared base (`gen/naming.base.toml`, referenced via `[naming] extends`) holds renames common +to all targets (rare). Target config wins over the base. + +### Collision detection + +After recasing, renames, and reserved-word mangling, two distinct wire names can collapse to +one identifier. The Plates build detects these and exits non-zero. Scopes checked: + +- Type identifiers: all emitted type identifiers unique within the target's namespace, in the + type-convention. +- Enum variant identifiers: unique within each enum type. +- Member identifiers: within each complex plate's flat member list. +- File stems (when partitioning): unique case-insensitively (macOS/Windows hazard). + +The report lists: scope, colliding wire names, the identifier they share. + +### Transformation catalog + +#### Shape -> emit strategy + +| IR shape | strategy | typical code output | +|-----------------|--------------------|-----------------------------------------------------| +| value: enum | `enum-class` | enum class + wire<->variant lookup tables | +| value: number | `numeric-wrapper` | wrapper over a target numeric type, range-validating| +| value: string | `string-wrapper` | wrapper over target string type, optional pattern | +| value: union | `tagged-variant` | small tagged variant over member types | +| complex: value | `value-class` | class with `value` field + attrs | +| complex: composite | `composite-class` | class with attrs + ordered children | +| complex: empty | `flag`/`attrs-class`| bool if `presence_only`, else attrs-only class | +| complex: derived| `inherit`/`flatten`| base-class inheritance, or flattened copy | + +#### Identifier validity and reserved words + +After recasing and renames, the binding applies a sanitizer: + +- **Reserved words** (language keywords plus `[reserved] words`) are mangled per the configured + policy (default: append `_`, so `class` -> `class_`). +- **Invalid identifiers** -- leading digit, empty result -- get the configured `invalid-prefix` + (default `_`, so `1024th` -> `_1024th`). + +#### Cardinality -> representation + +- **required** -> by-value member (the DAG invariant means no heap indirection is ever needed). +- **optional** -> target's optional type (`std::optional`, `*T` in Go, `bool has_x` in C). +- **vector** -> target's collection type. + +#### Primitive -> target type + +The IR primitive set (`string`, `token`, `decimal`, `integer`, `positive_integer`, +`non_negative_integer`, `date`, `nmtoken`) maps to target types through `Plates.type_map`, +seeded from `[types]` in `config.toml`. This is the single place a target decides that +`decimal` is a `Decimal` wrapper or that `token` is `std::string`. + +#### Clamp policy + +`NumberPlate.clamp` carries resolved `ClampStep`s: facet bounds merged with primitive-implied +lower bounds (`positive_integer` >= 1, `non_negative_integer` >= 0), tightest bound winning. +Both backends had hand-mirrored copies of this logic before it was centralized here. + +### JSON Schema as generality proof + +The JSON Schema target (`gen/schema/`) reads only the neutral core of each plate: + +- Wire names (not casings) for `$defs` keys, property names, enum values. +- `ComplexPlate.content` (resolved sequence/choice tree) for `oneOf`/`object`/`array` structure. +- `EnumPlate.variants[*].wire` for `"enum": [...]`. +- `NumberPlate.bounds` for `minimum`/`maximum`. +- Raw `doc` text for `"description"`. + +It never touches: casings, reserved-word mangling, the primitive type map, namespaces, file +partitioning, the include/import graph. It sets `partition = single` and reads no `file`. + +This is the concrete proof that the Plates are not C++-shaped: the JSON Schema target uses a +strict subset of one shared object. The neutral core is self-sufficient; code targets layer +their binding on top. + +## The Press (`gen/press/`) + +### Template language: Mustache with three deviations + +The press renders target Mustache templates against contexts built from the plates. Mustache's +poverty is the feature: **if a template cannot express something, the plates must carry it**, +which keeps decisions in the projection where they are dumpable, diffable, and collision-gated. + +Implemented subset: + +- **Variables**: `{{ident}}`, dotted paths `{{name.snake}}`, `{{target.vars.prefix}}`. +- **Sections**: `{{#members}}...{{/members}}` iterates lists and gates on truthiness; `{{^x}}... + {{/x}}` inverts. +- **Partials**: `{{> member-parse}}`, resolved in the target's `templates/` directory, with + spec-conformant call-site indentation and recursion (depth-limited; needed for content trees). +- **Whitespace discipline**: the spec's standalone-line rules. + +Three deliberate deviations from spec semantics: + +1. **Missing keys are a render error.** The spec mandates silent empty output -- the worst + possible failure mode for a generator. This project fails loud; the engine follows it. +2. **No HTML escaping.** `{{x}}` interpolates verbatim. There is no HTML here. +3. **No lambdas.** The spec's one escape hatch into logic is closed. + +Conformance to everything else is tested against the official Mustache spec test suite (the +published YAML cases for interpolation, sections, inverted, and partials), asserting agreement +everywhere except the three deviations. + +Because template syntax is pure Mustache, the engine is swappable. If the press grows past +~600 lines or cannot pass the spec suite, the committed fallback is to vendor chevron and patch +strictness/escaping/diagnostics -- with zero template changes. + +### Context builder (`gen/press/context.py`) + +The context builder adds three mechanical conveniences to every plate context, without engine +extensions: + +- **Discriminant expansion**: every closed enumerated field gets boolean companions -- + `kind: "enum"` yields `is_enum`, `cardinality: "vector"` yields `is_vector`. Templates branch + with plain sections: `{{#type_ref.is_complex}}...{{/type_ref.is_complex}}`. +- **Loop metadata**: every list item gets `is_first`, `is_last`, `index0`. Expresses + `if/else if` chains and separator joins. +- **`_q` companions**: every wire-string leaf gets a double-quoted, backslash-escaped companion + (`wire` -> `wire_q`). The quoting repertoire (JSON escaping with non-ASCII as `\uXXXX`) is + valid verbatim in C, C++, Go, Java, JS, and Rust. This is the one acknowledged compromise: + it encodes a language family, but it lives in the neutral context layer, where a future + non-C-family target would extend it -- not in the engine or templates. + +### The render manifest + +Each target's `config.toml` declares what gets rendered where: + +```toml +[render] +dir = "templates" + +# Per-type: rendered once per plate whose strategy matches. +[[render.type]] +strategies = ["enum-class"] +template = "enum.h.tmpl" +output = "mx_{snake}.h" # casing placeholder from the plate's Name + +# Once: rendered once per target, against the whole Plates context. +[[render.once]] +template = "runtime.c.tmpl" +output = "mx_runtime.c" +``` + +The manifest absorbs several things that were previously Python: + +- **Header/implementation pairs**: two entries per strategy. +- **Partitioning**: per-type entries are the `per-type` partition; only `once` entries is + `single` (the JSON Schema target). +- **File naming**: `output` patterns with casing placeholders (`{snake}`, `{pascal}`, ...). + The generator expands every pattern and runs a case-insensitive file-collision gate. +- **Support files**: the runtime sources are templates (mostly static text; a template with no + tags is a static file). +- **Format hook**: `[render] format = ["gofmt", "-w", "{dir}"]` -- run against the scratch + render directory before write-if-changed, preserving idempotence. Target data; the generator + knows only "run this, fail loud if absent or failing." + +## Generator agnosticism + +### The cardinal rule + +**The generator is language agnostic. Adding a new language target must not require edits to +the generator's Python files.** + +The letter: `git diff --name-only` for a change that adds a new target touches no `*.py` under +`gen/` outside the new target's directory. + +The spirit: the Python pipeline must be a closed machine -- schema in, files out -- that is +*incapable* of expressing a language-specific decision, so that language knowledge has nowhere +to live except in the target's own directory. A corollary: the generator has no concept of +"Go" or "C" at all. There is no language registry, no language name in config. A target is a +directory of data and templates; the generator cannot tell which language it is emitting. + +Enforced structurally by `gen/tests/test_agnosticism.py`: the generator's Python is a closed +set; no module is named after a language; targets contain no Python. + +Proof: `gen/schema/` (config + one template, 373 `$defs`) was added with zero Python edits. + +### Config: projection contract vs freeform vars + +A litmus test for any config key: *if you cannot define it without naming a language, it is a +var, not a key.* + +**Prescribed keys** are the projection contract -- definable in projection terms without +reference to any language: + +- `[naming]` conventions, acronyms, `[rename.*]`, `[reserved] words` -- language-neutral naming. +- `[target] symbol-prefix`: "prepended to every type identifier before sanitization." Neutral; + must stay in the projection so the collision gate certifies *final* identifiers. +- `[target] variant-scope = "bare" | "composed"`: how enum constants are scoped. +- `[target] inheritance = true | false`: selects the `inherit` vs `flatten` derived strategy. +- `[types]`: primitive -> spelling map. Required for any target emitting typed code. +- `[reserved] words`, `members`, `type-suffixes`: keyword lists plus names the templates + synthesize (e.g. Go's `Children` field), fed to the existing collision gate. + +**Everything else is freeform.** A `[vars]` table of string key-values passes through to +templates verbatim (`{{target.vars.namespace}}`, `{{target.vars.package}}`). `namespace` is not +a generator key; it is a variable the Go templates happen to use as a package name. The litmus +test filters it cleanly. + +### Alternatives considered (and rejected) + +**Per-target Python plugins** (each target ships a `backend.py` the generator loads +dynamically). Satisfies the letter -- no edits to the generator's files -- but not the spirit: +language knowledge would still be Python programs, just relocated; the C++ backend would again +be two thousand lines of imperative emission; and nothing would force decisions into the plates. +The review's instruction was that bespoke backends "should not exist," not that they should move. + +**Jinja2** (or any expressive template engine). Expressive is the problem: filters, macros, +arbitrary expressions, and `set` would let a backend be reconstituted inside template files, +hiding naming logic where no structural gate can see it. It also adds a pip/vendored dependency +to a deliberately dependency-free Python side. + +**An existing Mustache library** (chevron, pystache). A close call, declined on four counts: +(1) the spec mandates silent empty output for missing keys, which is disqualifying and not +configurable in chevron; (2) we would patch a vendored copy in three places and own the result +anyway; (3) the Python side has a deliberate no-dependencies precedent; (4) conformance risk is +neutralized by running the official Mustache spec test suite against the press. The decision is +cheaply reversible: if the press grows past ~600 lines or fails the spec suite, vendor chevron +and patch -- zero template changes needed. + +**AST-based emitters** (language-neutral syntax tree, printed per language). A second +language-shaped abstraction to design, with per-language printers landing right back in Python. +Wrong direction. + +**Keeping `languages.py`** (the old per-language data tables inside the generator). Same data, +wrong place: keyed by language name inside the generator, it requires every new target to edit +it. Config is the same data in the right place. + +## Companion data (`gen/ir/sounds.py`) + +`instrument-sound` is `xs:string` in the XSD; the ~900 standard sound identifiers live only in +`sounds.xml`, a separately versioned MusicXML file. `ir/sounds.py` folds them into the IR: it +adds a `sound-id` enum over the identifiers and an `instrument-sound` union of that enum with +an open string, then retypes the element from `string` to the union. + +This is the one documented exception to "the IR is a pure function of the XSD." It is an +*input selection* (which input files to read), not a type-shaping decision. It runs only when +a target's `config.toml` sets `[sounds] xml`; the base IR stays pure. `python3 -m gen ir +--config ` shows the patched view. The three targets deliberately span the matrix: +C++ is 4.0 with sounds, C is 3.1 with sounds, Go is 3.1 without sounds. + +## IR glossary + +### XSD source terms + +- **simpleType** -- a type with no child elements and no attributes: just a constrained text + value. Becomes an IR value type. +- **complexType** -- a type for an element with attributes and/or child elements. Becomes an IR + complex type. +- **group** (`xs:group`) -- a named, reusable fragment of element content (a sequence/choice) + spliced into complex types by reference. No identity in the XML document. +- **attributeGroup** -- a named, reusable bundle of attributes referenced by complex types. +- **restriction / extension** -- the two ways one type derives from another. +- **simpleContent / complexContent** -- a complex type whose body is a text value plus attributes + (simpleContent), or that derives from another complex type (complexContent). +- **facet** -- a constraint on a simpleType: `enumeration`, `pattern`, `minInclusive`, + `maxInclusive`, `minExclusive`, `maxExclusive`, `minLength`, `maxLength`, `length`. +- **particle** -- a piece of a content model: an element, `sequence`, `choice`, or group ref, + each with `minOccurs`/`maxOccurs`. +- **anonymous type** -- a type defined in place on an element rather than named at top level. + The IR names and hoists these (see synthesized type). + +### IR structural terms + +- **Ref** `{ name, category }` -- a typed reference to another type. `category` is `complex`, + `value`, or `primitive`. +- **primitive** -- a builtin base type the generator does not emit. Canonicalized from XSD + builtins: `decimal`, `integer`, `positive_integer`, `non_negative_integer`, `string`, `token`, + `nmtoken`, `date`, plus `id`, `idref`. +- **cardinality** -- normalized occurrence of an element field: `required` (exactly 1), + `optional` (0 or 1), or `vector` (repeatable). Derived from min/max. +- **presence_only** -- true for an empty element with no attributes: its only information is + whether it appears, so it maps to a bool. +- **deps** -- the complex types a type structurally depends on (child element types + base), + resolved through groups. Drives topological ordering. +- **roots** -- the document root elements: `score-partwise` and `score-timewise`. diff --git a/gen/README.md b/gen/README.md new file mode 100644 index 000000000..3a705f7c1 --- /dev/null +++ b/gen/README.md @@ -0,0 +1,43 @@ +# mx generator (`gen/`) + +A Python pipeline that reads a MusicXML XSD and emits typed +serialization/deserialization libraries. The C++ `mx::core` model +(`src/private/mx/core/generated/`) is its primary output. Secondary Go, C, and JSON Schema +targets validate language-agnosticism. + +## How it works + +``` +XSD -> XSD model -> IR -> Plates -> press -> C++ / Go / C / JSON Schema + (gen/xsd) (gen/ir) (gen/plates) (gen/press) +``` + +Parse the XSD. Lower it to a language-agnostic IR. Project the IR onto one target as the +**Plates** (per-type metadata: identifiers in each casing, type mappings, emit strategies, +file assignment). Render the target's Mustache templates through the **press** per its +`[render]` manifest. + +A **target** is a directory: `config.toml` plus `templates/`. All language knowledge lives +there. Adding a new language touches no generator Python. + +## Running it + +``` +python3 -m gen analyze [xsd] # structural analysis report +python3 -m gen ir [--type NAME] [--resolve] [--config C] # IR as JSON +python3 -m gen plates --config C [--type NAME] [--check] # Plates as JSON; --check gates CI +python3 -m gen render --config C --type NAME # render one type to stdout +python3 -m gen # emit the target (full run) +``` + +Full runs: `make gen-cpp` (C++ target), `make gen` (all targets). Generated output is +committed; `make test-gen` regenerates and asserts `git diff --exit-code`. + +## For agents + +Read `gen/AGENTS.md` before working on the generator. It has the hot path: commands, gates, +cardinal rules, and what must never break. + +Read `gen/DESIGN.md` for the full design: goals, pipeline stage responsibilities, IR model and +glossary, Plates architecture (name model, override system, collision detection), the press +and Mustache spec, generator agnosticism rationale, and alternatives considered. diff --git a/gen/__init__.py b/gen/__init__.py new file mode 100644 index 000000000..bd0866d7a --- /dev/null +++ b/gen/__init__.py @@ -0,0 +1 @@ +"""mx code generator package.""" diff --git a/gen/__main__.py b/gen/__main__.py new file mode 100644 index 000000000..4845e4100 --- /dev/null +++ b/gen/__main__.py @@ -0,0 +1,280 @@ +"""mx code generator entry point. + +Usage: + python3 -m gen emit code for the target the config describes + python3 -m gen analyze [xsd] parse the XSD and print a structural analysis + python3 -m gen ir [--type N] [--resolve] [--config C] [xsd] + lower the XSD to the IR and print it as JSON; + --resolve prints the collapsed (group-spliced, + attribute-flattened) view of complex types; + --config applies a target's companion patches + (e.g. the sounds.xml fold) before dumping + python3 -m gen plates --config C [--type N] [--check] + project the IR onto the target the config + describes and print the Plates as JSON; + --check validates renames and detects + identifier collisions, exiting non-zero on + any failure (a CI gate, like analyze) + python3 -m gen render --config C --type N + render one type through the target's + templates to stdout (template debugging) + +Reads a MusicXML 4.0 XSD specification and generates typed document +serialization/deserialization code for the target described in the given +config file. +""" + +import sys +from pathlib import Path + +# The MusicXML version this generator targets, used as the default for analyze. +DEFAULT_XSD = Path(__file__).resolve().parent.parent / "docs" / "musicxml-4.0-ed15c23.xsd" + + +def _analyze(args: list[str]) -> int: + from gen.xsd.analyze import report + from gen.xsd.parser import parse + + xsd = Path(args[0]) if args else DEFAULT_XSD + if not xsd.exists(): + print(f"error: XSD not found: {xsd}", file=sys.stderr) + return 1 + print(report(parse(xsd))) + return 0 + + +def _ir(args: list[str]) -> int: + from gen.ir.dump import resolved_view, to_json + from gen.ir.resolve import Resolver + + type_name = None + resolve = False + config_path = None + rest = [] + i = 0 + while i < len(args): + if args[i] == "--type" and i + 1 < len(args): + type_name = args[i + 1] + i += 2 + elif args[i] == "--resolve": + resolve = True + i += 1 + elif args[i] == "--config" and i + 1 < len(args): + config_path = args[i + 1] + i += 2 + else: + rest.append(args[i]) + i += 1 + + cfg = None + if config_path is not None: + from gen.config import load as load_config + + cfg = load_config(config_path) + + # XSD precedence: an explicit positional argument wins, else the target + # config's pinned version, else the 4.0 default. + if rest: + xsd = Path(rest[0]) + elif cfg is not None and cfg.xsd is not None: + xsd = cfg.xsd + else: + xsd = DEFAULT_XSD + if not xsd.exists(): + print(f"error: XSD not found: {xsd}", file=sys.stderr) + return 1 + ir = _lower(xsd, cfg) + + resolver = Resolver.from_ir(ir) if resolve else None + + if type_name: + ct = next((c for c in ir.complex_types if c.name == type_name), None) + if ct is not None: + print(to_json(resolved_view(resolver, ct) if resolver else ct)) + return 0 + vt = next((v for v in ir.value_types if v.name == type_name), None) + if vt is None: + print(f"error: type not found in IR: {type_name}", file=sys.stderr) + return 1 + print(to_json(vt)) # value types are already fully resolved + return 0 + + if resolver: + print(to_json([resolved_view(resolver, c) for c in ir.complex_types])) + else: + print(to_json(ir)) + return 0 + + +def _lower(xsd: Path, cfg): + """Lower an XSD to the IR, applying a config's companion patches (today: + the sounds.xml fold). One definition, shared by every command.""" + from gen.ir.build import build_ir + from gen.xsd.parser import parse + + ir = build_ir(parse(xsd), source=xsd.stem) + if cfg is not None and cfg.sounds_xml is not None: + from gen.ir.sounds import patch_sounds, read_sound_ids + + patch_sounds(ir, read_sound_ids(cfg.sounds_xml)) + return ir + + +def _plates(args: list[str]) -> int: + from gen.ir.dump import to_json + from gen.plates import PlatesError + + config_path = None + type_name = None + check = False + i = 0 + while i < len(args): + if args[i] == "--config" and i + 1 < len(args): + config_path = args[i + 1] + i += 2 + elif args[i] == "--type" and i + 1 < len(args): + type_name = args[i + 1] + i += 2 + elif args[i] == "--check": + check = True + i += 1 + else: + print(f"error: unexpected argument: {args[i]}", file=sys.stderr) + return 2 + if config_path is None: + print("error: plates requires --config ", file=sys.stderr) + return 2 + + from gen.plates import build_for_config + + try: + plates, _ = build_for_config(config_path) + except PlatesError as e: + for line in e.errors: + print(f"error: {line}", file=sys.stderr) + return 1 + + if check: + # Rename validation and collision detection already ran in the build; + # reaching here means the projection is clean. + print( + f"plates ok: {len(plates.value_types)} value types, " + f"{len(plates.complex_types)} complex types, " + f"{len(plates.groups)} content types" + ) + return 0 + + if type_name: + if not plates.has_plate(type_name): + print(f"error: type not found in plates: {type_name}", file=sys.stderr) + return 1 + print(to_json(plates.plate(type_name))) + return 0 + + print(to_json(plates)) + return 0 + + +def _emit(config_path: str) -> int: + from gen.config import ConfigError + from gen.plates import PlatesError, build_for_config + from gen.press.engine import PressError + from gen.press.render import RenderError, render_target + + try: + plates, cfg = build_for_config(config_path) + result = render_target(plates, cfg) + except PlatesError as e: + for line in e.errors: + print(f"error: {line}", file=sys.stderr) + return 1 + except ( + ConfigError, + FileNotFoundError, + PressError, + RenderError, + RuntimeError, + ValueError, + ) as e: + print(f"error: {e}", file=sys.stderr) + return 1 + print(result.summary()) + return 0 + + +def _render_debug(args: list[str]) -> int: + from gen.plates import PlatesError, build_for_config + from gen.press.engine import PressError + from gen.press.render import RenderError, render_files + + config_path = None + type_name = None + i = 0 + while i < len(args): + if args[i] == "--config" and i + 1 < len(args): + config_path = args[i + 1] + i += 2 + elif args[i] == "--type" and i + 1 < len(args): + type_name = args[i + 1] + i += 2 + else: + print(f"error: unexpected argument: {args[i]}", file=sys.stderr) + return 2 + if config_path is None or type_name is None: + print("error: render requires --config --type ", + file=sys.stderr) + return 2 + try: + plates, cfg = build_for_config(config_path) + if cfg.render is None: + print(f"error: config has no [render] manifest: {cfg.path}", + file=sys.stderr) + return 1 + if not plates.has_plate(type_name): + print(f"error: type not found in plates: {type_name}", file=sys.stderr) + return 1 + plate = plates.plate(type_name) + files = render_files(plates, cfg) + from gen.press.render import _expand + + shown = 0 + for entry in cfg.render.types: + if plate.strategy in entry.strategies: + path = _expand(entry.output, plate) + print(f"==== {path} (from {entry.template})") + print(files[path], end="") + shown += 1 + if not shown: + print(f"error: no manifest entry renders strategy " + f"'{plate.strategy}'", file=sys.stderr) + return 1 + except PlatesError as e: + for line in e.errors: + print(f"error: {line}", file=sys.stderr) + return 1 + except (PressError, RenderError, FileNotFoundError, ValueError) as e: + print(f"error: {e}", file=sys.stderr) + return 1 + return 0 + + +def main(argv: list[str]) -> int: + if not argv: + print(__doc__, file=sys.stderr) + return 2 + if argv[0] == "analyze": + return _analyze(argv[1:]) + if argv[0] == "ir": + return _ir(argv[1:]) + if argv[0] == "plates": + return _plates(argv[1:]) + if argv[0] == "render": + return _render_debug(argv[1:]) + if argv[0].endswith(".toml"): + return _emit(argv[0]) + print(f"error: unknown command: {argv[0]}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/gen/config.py b/gen/config.py new file mode 100644 index 000000000..4ed362ce4 --- /dev/null +++ b/gen/config.py @@ -0,0 +1,501 @@ +"""Load a target's config.toml into a typed Config. + +A target config describes one generation run: which schema inputs to read, +where generated code lands, which optional companion patches to apply before +emitting, and how the IR is projected onto the target (the Plates: naming +conventions, renames, type mappings, layout). The IR itself stays a pure +function of the schema inputs (see gen.ir); config selects *which* inputs and +how the result is presented, never what the schema means. + +Parsing is structural only: key shapes, types, and the rename addressing +scheme. Semantic validation (does a rename key name something in the IR, do +projected identifiers collide) happens in gen.plates.build, which has the IR +in hand and fails loud there. +""" + +from __future__ import annotations + +import tomllib +from dataclasses import dataclass, field +from pathlib import Path + +from gen.names import CONVENTIONS + +# Keys allowed in a rename entry table: a fundamental rename (all casings +# re-expand from the new root) or per-convention overrides (pin one flavor). +_ENTRY_KEYS = frozenset(CONVENTIONS) | {"fundamental"} + +# Rename kinds the Plates build consumes today. `group` and `attribute-group` +# are reserved by the design for targets that emit shared fragments/mixins; +# none of ours does, so configuring them is an error rather than a silently +# dead table. +_RENAME_KINDS = ("type", "element", "attribute", "enum-value") + + +class ConfigError(ValueError): + """A malformed config file. Always raised with the offending key path.""" + + +@dataclass +class RenameEntry: + """One rename: an optional fundamental root plus per-convention pins.""" + + fundamental: str | None = None + cased: dict[str, str] = field(default_factory=dict) + + +@dataclass +class Renames: + """Parsed [rename.*] tables, keyed by the design's addressing scheme.""" + + types: dict[str, RenameEntry] = field(default_factory=dict) + elements: dict[str, RenameEntry] = field(default_factory=dict) + attributes: dict[str, RenameEntry] = field(default_factory=dict) # global + scoped_attributes: dict[tuple[str, str], RenameEntry] = field(default_factory=dict) + enum_values: dict[tuple[str, str], RenameEntry] = field(default_factory=dict) + + def __bool__(self) -> bool: + return bool( + self.types + or self.elements + or self.attributes + or self.scoped_attributes + or self.enum_values + ) + + +@dataclass +class TargetSection: + symbol_prefix: str = "" # prepended to type idents and composed constants + inheritance: bool = True # derived types: inherit (True) or flatten + variant_scope: str = "bare" # constant scoping: "bare" | "composed" + + +@dataclass +class NamingSection: + acronyms: tuple[str, ...] | None = None # None -> the built-in default set + type_convention: str = "pascal" + field_convention: str = "snake" + variant_convention: str = "pascal" + field_prefix: str = "" + pluralize_vectors: bool = False + + +@dataclass +class ReservedSection: + words: tuple[str, ...] = () # the target's WHOLE reserved-word list + members: tuple[str, ...] = () # member idents the target's templates reserve + type_suffixes: tuple[str, ...] = () # compositions templates append to type idents + invalid_prefix: str = "_" + + +@dataclass +class RenderEntry: + """One manifest row: render `template` for every plate the row selects, + writing to the `output` pattern (casing placeholders like {snake} come + from the plate's name). A row selects either by `strategies` (the normal + shape-driven case) or by `types` -- exact wire names, for bespoke + handling of individual types. Type rows OVERRIDE strategy rows: a plate + named by any type row is rendered only by its type rows, so custom code + for one element or attribute is a config-and-template change, never a + generator change. A once-per-target row has neither.""" + + template: str + output: str + strategies: tuple[str, ...] = () + types: tuple[str, ...] = () + + +@dataclass +class RenderSection: + """The render manifest: which templates produce which files. Its presence + selects the press pipeline.""" + + dir: Path # the target's templates directory, resolved + format: tuple[str, ...] = () # optional post-render command; {dir} expands + types: list[RenderEntry] = field(default_factory=list) + once: list[RenderEntry] = field(default_factory=list) + + +@dataclass +class DocsSection: + # Width of the wrapped doc TEXT, excluding comment syntax (templates add + # their own prefixes). 97 + a 3-character prefix is the 100-column house + # style. + wrap: int = 97 + + +@dataclass +class Config: + path: Path = Path(".") # the config file itself, resolved + xsd: Path | None = None # the MusicXML XSD this target generates from + output_dir: Path | None = None # where generated code lands, resolved + sounds_xml: Path | None = None # companion sounds file to fold in, or None + target: TargetSection = field(default_factory=TargetSection) + vars: dict[str, str] = field(default_factory=dict) # freeform, for templates + naming: NamingSection = field(default_factory=NamingSection) + reserved: ReservedSection = field(default_factory=ReservedSection) + 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 + # 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. + import_attribute_defaults: dict[tuple[str, str], str] = field(default_factory=dict) + render: RenderSection | None = None # presence selects the press pipeline + + +def load(config_path) -> Config: + """Parse config.toml. Paths inside it are interpreted relative to the + config file's own directory, so a target's config stays self-contained.""" + path = Path(config_path).resolve() + if not path.exists(): + raise FileNotFoundError(f"config not found: {path}") + with open(path, "rb") as f: + data = tomllib.load(f) + base = path.parent + _check_keys( + data, + {"input", "output", "sounds", "target", "naming", "reserved", "types", + "docs", "rename", "vars", "render", "import"}, + "top level", + ) + _check_keys(data.get("input", {}), {"xsd"}, "input") + _check_keys(data.get("output", {}), {"dir"}, "output") + _check_keys(data.get("sounds", {}), {"xml"}, "sounds") + + # A shared naming base (design: [naming] extends) contributes [naming] + # keys and [rename.*] entries; the target's own win on any conflict. + data = _apply_extends(data, base) + + # Each target pins its own MusicXML version: the schema it generates from + # is part of the target's identity, not a global default. + xsd = None + inp = data.get("input", {}) + if inp.get("xsd"): + xsd = (base / inp["xsd"]).resolve() + if not xsd.exists(): + raise FileNotFoundError(f"xsd not found: {xsd}") + + output_dir = None + out = data.get("output", {}) + if out.get("dir"): + output_dir = (base / out["dir"]).resolve() + + # Companion sounds patch is on iff [sounds] xml names a file (see + # gen.ir.sounds). Resolve and existence-check it here so a bad path fails + # at config load, not deep in the lowering. + sounds_xml = None + sounds = data.get("sounds", {}) + if sounds.get("xml"): + sounds_xml = (base / sounds["xml"]).resolve() + if not sounds_xml.exists(): + raise FileNotFoundError(f"sounds file not found: {sounds_xml}") + + return Config( + path=path, + xsd=xsd, + output_dir=output_dir, + sounds_xml=sounds_xml, + target=_target(data.get("target", {})), + vars=_vars(data.get("vars", {})), + naming=_naming(data.get("naming", {})), + reserved=_reserved(data.get("reserved", {})), + types=_types(data.get("types", {})), + docs=_docs(data.get("docs", {})), + renames=_renames(data.get("rename", {})), + import_attribute_defaults=_import_defaults(data.get("import", {})), + render=_render(data["render"], base) if "render" in data else None, + ) + + +# --------------------------------------------------------------------------- # +# Section parsers. Each takes the raw TOML table and fails loud on unknown +# keys, so a typo is a config error, not a silently ignored line. +# --------------------------------------------------------------------------- # + + +def _import_defaults(t: dict) -> dict[tuple[str, str], str]: + """[import] attribute-defaults: { "type" = { "attr" = "literal" } }.""" + _check_keys(t, {"attribute-defaults"}, "import") + out: dict[tuple[str, str], str] = {} + for type_wire, attrs in t.get("attribute-defaults", {}).items(): + if not isinstance(attrs, dict): + raise ConfigError( + f"[import.attribute-defaults] {type_wire}: expected a table " + f"of attribute = literal" + ) + for attr_wire, literal in attrs.items(): + if not isinstance(literal, str): + raise ConfigError( + f"[import.attribute-defaults] {type_wire}.{attr_wire}: " + f"expected a string literal" + ) + out[(type_wire, attr_wire)] = literal + return out + + +def _check_keys(table: dict, allowed: set[str], where: str) -> None: + unknown = set(table) - allowed + if unknown: + raise ConfigError(f"unknown key(s) in [{where}]: {', '.join(sorted(unknown))}") + + +def _target(t: dict) -> TargetSection: + _check_keys(t, {"symbol-prefix", "inheritance", "variant-scope"}, "target") + section = TargetSection( + symbol_prefix=t.get("symbol-prefix", ""), + inheritance=bool(t.get("inheritance", True)), + variant_scope=t.get("variant-scope", "bare"), + ) + if section.variant_scope not in ("bare", "composed"): + raise ConfigError( + f"[target] variant-scope = {section.variant_scope!r}: expected bare or composed" + ) + return section + + +def _vars(t: dict) -> dict[str, str]: + """Freeform key-values passed verbatim to templates ({{target.vars.x}}). + The generator never interprets them; this is where anything that cannot + be defined without naming a language belongs.""" + for k, v in t.items(): + if not isinstance(v, str): + raise ConfigError(f"[vars] {k} must be a string") + return dict(t) + + +def _string_list(value, where: str) -> tuple[str, ...]: + """A TOML array of strings. A bare string is rejected rather than being + silently exploded into characters.""" + if not isinstance(value, list) or not all(isinstance(x, str) for x in value): + raise ConfigError(f"[{where}] must be an array of strings") + return tuple(value) + + +def _naming(t: dict) -> NamingSection: + _check_keys( + t, + { + "extends", "acronyms", "type-convention", "field-convention", + "variant-convention", "field-prefix", "pluralize-vectors", + }, + "naming", + ) + section = NamingSection( + acronyms=_string_list(t["acronyms"], "naming.acronyms") if "acronyms" in t else None, + type_convention=t.get("type-convention", "pascal"), + field_convention=t.get("field-convention", "snake"), + variant_convention=t.get("variant-convention", "pascal"), + field_prefix=t.get("field-prefix", ""), + pluralize_vectors=bool(t.get("pluralize-vectors", False)), + ) + for key in ("type_convention", "field_convention", "variant_convention"): + value = getattr(section, key) + if value not in CONVENTIONS: + raise ConfigError( + f"[naming] {key.replace('_', '-')} = {value!r} is not a " + f"registered convention ({', '.join(sorted(CONVENTIONS))})" + ) + return section + + +def _reserved(t: dict) -> ReservedSection: + _check_keys(t, {"words", "members", "type-suffixes", "invalid-prefix"}, "reserved") + return ReservedSection( + words=_string_list(t["words"], "reserved.words") if "words" in t else (), + members=_string_list(t["members"], "reserved.members") if "members" in t else (), + type_suffixes=_string_list(t["type-suffixes"], "reserved.type-suffixes") + if "type-suffixes" in t + else (), + invalid_prefix=t.get("invalid-prefix", "_"), + ) + + +def _types(t: dict) -> dict[str, str]: + for k, v in t.items(): + if not isinstance(v, str): + raise ConfigError(f"[types] {k} must be a string target type") + return dict(t) + + +def _render_entry(t: dict, where: str, once: bool) -> RenderEntry: + allowed = {"template", "output"} | (set() if once else {"strategies", "types"}) + _check_keys(t, allowed, where) + for key in ("template", "output"): + if not isinstance(t.get(key), str) or not t[key]: + raise ConfigError(f"[{where}] requires a non-empty '{key}' string") + strategies: tuple[str, ...] = () + types: tuple[str, ...] = () + if not once: + strategies = tuple(_string_list(t.get("strategies", []), f"{where}.strategies")) + types = tuple(_string_list(t.get("types", []), f"{where}.types")) + if bool(strategies) == bool(types): + raise ConfigError( + f"[{where}] requires exactly one of 'strategies' or 'types'" + ) + return RenderEntry( + template=t["template"], output=t["output"], strategies=strategies, types=types + ) + + +def _render(t: dict, base: Path) -> RenderSection: + _check_keys(t, {"dir", "format", "type", "once"}, "render") + if not isinstance(t.get("dir"), str) or not t["dir"]: + raise ConfigError("[render] requires a 'dir' (the templates directory)") + directory = (base / t["dir"]).resolve() + if not directory.is_dir(): + raise FileNotFoundError(f"templates directory not found: {directory}") + section = RenderSection( + dir=directory, + format=tuple(_string_list(t["format"], "render.format")) if "format" in t else (), + types=[ + _render_entry(e, "render.type", once=False) for e in t.get("type", []) + ], + once=[ + _render_entry(e, "render.once", once=True) for e in t.get("once", []) + ], + ) + if not section.types and not section.once: + raise ConfigError("[render] declares no template entries") + return section + + +def _docs(t: dict) -> DocsSection: + _check_keys(t, {"wrap"}, "docs") + return DocsSection(wrap=int(t.get("wrap", 97))) + + +# --------------------------------------------------------------------------- # +# Renames (design 6.2/6.3): two tiers (fundamental + per-convention), four +# addressable kinds, with enum values scoped to their enum and attributes +# optionally scoped to their owner type. +# --------------------------------------------------------------------------- # + + +def _entry(value, where: str) -> RenameEntry: + """A rename value is either the string shorthand (sugar for a table with + only `fundamental`) or a table of fundamental/convention keys.""" + if isinstance(value, str): + return RenameEntry(fundamental=value) + if isinstance(value, dict): + unknown = set(value) - _ENTRY_KEYS + if unknown: + raise ConfigError( + f"unknown key(s) in [{where}]: {', '.join(sorted(unknown))} " + f"(expected fundamental or a convention: {', '.join(sorted(CONVENTIONS))})" + ) + bad = [k for k, v in value.items() if not isinstance(v, str)] + if bad: + raise ConfigError(f"[{where}] {bad[0]} must be a string") + if not value: + raise ConfigError(f"[{where}] is empty: set fundamental or a convention") + return RenameEntry( + fundamental=value.get("fundamental"), + cased={k: v for k, v in value.items() if k != "fundamental"}, + ) + raise ConfigError(f"[{where}] must be a string or a table") + + +def _is_entry_table(value) -> bool: + return isinstance(value, dict) and set(value) <= _ENTRY_KEYS + + +def _renames(t: dict) -> Renames: + unknown = set(t) - set(_RENAME_KINDS) - {"group", "attribute-group"} + if unknown: + raise ConfigError(f"unknown rename kind(s): {', '.join(sorted(unknown))}") + for reserved_kind in ("group", "attribute-group"): + if reserved_kind in t: + raise ConfigError( + f"rename kind '{reserved_kind}' is reserved for targets that emit " + f"shared fragments; no current target does" + ) + + for kind in _RENAME_KINDS: + if kind in t and not isinstance(t[kind], dict): + raise ConfigError(f"[rename.{kind}] must be a table") + + r = Renames() + for wire, value in t.get("type", {}).items(): + r.types[wire] = _entry(value, f"rename.type.{wire}") + for wire, value in t.get("element", {}).items(): + r.elements[wire] = _entry(value, f"rename.element.{wire}") + + # [rename.attribute] mixes global entries (string, or a table of entry + # keys) with owner scopes (a table keyed by attribute names). The key sets + # are disjoint: entry keys are fundamental/conventions, never wire names. + for key, value in t.get("attribute", {}).items(): + if isinstance(value, str) or _is_entry_table(value): + r.attributes[key] = _entry(value, f"rename.attribute.{key}") + elif isinstance(value, dict): + for attr, sub in value.items(): + r.scoped_attributes[(key, attr)] = _entry( + sub, f"rename.attribute.{key}.{attr}" + ) + else: + raise ConfigError(f"[rename.attribute] {key} must be a string or a table") + + for enum, table in t.get("enum-value", {}).items(): + if not isinstance(table, dict): + raise ConfigError(f"[rename.enum-value.{enum}] must be a table of values") + for wire, value in table.items(): + r.enum_values[(enum, wire)] = _entry(value, f"rename.enum-value.{enum}.{wire}") + return r + + +# --------------------------------------------------------------------------- # +# Shared naming base ([naming] extends) +# --------------------------------------------------------------------------- # + + +def _apply_extends(data: dict, base_dir: Path) -> dict: + """Merge a shared base file under the target's config: the base + contributes [naming] keys and [rename] entries; the target's own win per + key/entry. Anything else in the base is an error, as is chaining bases.""" + extends = data.get("naming", {}).get("extends") + if not extends: + return data + base_path = (base_dir / extends).resolve() + if not base_path.exists(): + raise FileNotFoundError(f"naming base not found: {base_path}") + with open(base_path, "rb") as f: + shared = tomllib.load(f) + _check_keys(shared, {"naming", "rename"}, f"naming base {base_path.name}") + if "extends" in shared.get("naming", {}): + raise ConfigError(f"naming base {base_path.name} may not chain to another base") + + merged = dict(data) + naming = dict(shared.get("naming", {})) + naming.update(data.get("naming", {})) + naming.pop("extends", None) + merged["naming"] = naming + + rename: dict = {} + for kind in set(shared.get("rename", {})) | set(data.get("rename", {})): + base_table = shared.get("rename", {}).get(kind, {}) + own_table = data.get("rename", {}).get(kind, {}) + table: dict = {} + for key in list(base_table) + [k for k in own_table if k not in base_table]: + b, o = base_table.get(key), own_table.get(key) + b_scope = isinstance(b, dict) and not _is_entry_table(b) + o_scope = isinstance(o, dict) and not _is_entry_table(o) + if b is not None and o is not None and b_scope != o_scope: + # One side addresses a scope table, the other a single entry: + # a silent wholesale replacement would quietly drop the + # base's renames, so the disagreement is an error. + raise ConfigError( + f"[rename.{kind}.{key}]: the target and its naming base " + f"disagree on whether this is a scope or an entry" + ) + if b_scope and o_scope: + # A nested scope (an enum's value table, an owner's attribute + # table): merge per inner entry, target winning. + table[key] = {**b, **o} + else: + table[key] = o if key in own_table else b + rename[kind] = table + if rename: + merged["rename"] = rename + return merged diff --git a/gen/generate.py b/gen/generate.py deleted file mode 100644 index 789aa87a9..000000000 --- a/gen/generate.py +++ /dev/null @@ -1,13370 +0,0 @@ -#!/usr/bin/env python3 -"""MusicXML codegen experiment: generate mx/core element classes from musicxml.xsd. - -Iteration 6: Group inlining, fromXElementImpl/streamContents/hasContents fixes. -""" -import os -import re -import sys -from collections import OrderedDict -from dataclasses import dataclass, field -from typing import Optional - -from parse import ( - XS, - ChoiceNode, - ElementRefNode, - GroupRefNode, - ParseConfig, - SequenceNode, - XsdAttribute, - XsdChildRef, - XsdComplexType, - XsdElement, - XsdModel, - pascal, -) - -XSD_PATH = "docs/musicxml.xsd" -CORE_DIR = "src/private/mx/core" -ELEM_DIR = os.path.join(CORE_DIR, "elements") - -LICENSE = """\ -// MusicXML Class Library -// Copyright (c) by Matthew James Briggs -// Distributed under the MIT License -""" - -CPP_KEYWORDS = { - "continue", "double", "long", "short", "int", "float", "bool", "char", - "class", "struct", "enum", "union", "void", "for", "while", "do", "if", - "else", "switch", "case", "default", "break", "return", "new", "delete", - "this", "true", "false", "const", "static", "virtual", "public", "private", - "protected", "namespace", "using", "template", "typename", "operator", - "and", "or", "not", "xor", "auto", "register", "signed", "unsigned", - "goto", "throw", "try", "catch", "explicit", "string", -} - - -def camel(name: str) -> str: - parts = re.split(r"[-_]", name) - result = parts[0].lower() + "".join(p[:1].upper() + p[1:] for p in parts[1:]) - if result in CPP_KEYWORDS: - result += "_" - return result - - -def has_flag_name(cpp_n: str) -> str: - # The presence flag is built from the unescaped identifier: the value field may - # be keyword-escaped (e.g. 'long_'), but the has-flag must not be ('hasLong', - # not 'hasLong_'). Strip a trailing underscore added by camel() for keywords. - base = cpp_n[:-1] if cpp_n.endswith("_") and cpp_n[:-1] in CPP_KEYWORDS else cpp_n - return "has" + base[0].upper() + base[1:] - - -# --------------------------------------------------------------------------- -# C++ Type Mapping -# --------------------------------------------------------------------------- - -XSD_TO_CPP_TYPE = { - "xs:string": "XsString", - "xs:token": "XsToken", - "xs:ID": "XsID", - "xs:IDREF": "XsIDREF", - "xs:NMTOKEN": "XsNMToken", - "xs:anyURI": "XsAnyUri", - "xs:decimal": "DecimalType", - "xs:integer": "Integer", - "xs:nonNegativeInteger": "NonNegativeInteger", - "xs:positiveInteger": "PositiveInteger", - "xs:date": "Date", - "xs:time": "TimeOnly", - "xml:lang": "XmlLang", - "xml:space": "XmlSpace", - "xlink:href": "XlinkHref", - "xlink:type": "XlinkType", - "xlink:role": "XlinkRole", - "xlink:title": "XlinkTitle", - "xlink:show": "XlinkShow", - "xlink:actuate": "XlinkActuate", -} - -SIMPLE_TYPE_TO_CPP = { - "above-below": "AboveBelow", - "accidental-value": "AccidentalValue", - "backward-forward": "BackwardForward", - "bar-style": "BarStyleEnum", - "beam-value": "BeamValue", - "cancel-location": "CancelLocation", - "clef-sign": "ClefSign", - "css-font-size": "CssFontSize", - "degree-symbol-value": "DegreeSymbolValue", - "degree-type-value": "DegreeTypeValue", - "effect-value": "EffectValue", - "enclosure-shape": "EnclosureShape", - "fan": "Fan", - "fermata-shape": "FermataShape", - "font-style": "FontStyle", - "font-weight": "FontWeight", - "group-barline-value": "GroupBarlineValue", - "group-symbol-value": "GroupSymbolValue", - "handbell-value": "HandbellValue", - "harmony-type": "HarmonyType", - "kind-value": "KindValue", - "left-center-right": "LeftCenterRight", - "left-right": "LeftRight", - "line-end": "LineEnd", - "line-shape": "LineShape", - "line-type": "LineType", - "margin-type": "MarginType", - "measure-numbering-value": "MeasureNumberingValue", - "membrane-value": "MembraneValue", - "metal-value": "MetalValue", - "mute": "MuteEnum", - "notehead-value": "NoteheadValue", - "note-size-type": "NoteSizeType", - "note-type-value": "NoteTypeValue", - "on-off": "OnOff", - "over-under": "OverUnder", - "pitched-value": "PitchedValue", - "placement": "AboveBelow", - "right-left-middle": "RightLeftMiddle", - "semi-pitched": "SemiPitchedEnum", - "show-frets": "ShowFrets", - "show-tuplet": "ShowTuplet", - "staff-type": "StaffTypeEnum", - "start-note": "StartNote", - "start-stop": "StartStop", - "start-stop-change-continue": "StartStopChangeContinue", - "start-stop-continue": "StartStopContinue", - "start-stop-discontinue": "StartStopDiscontinue", - "start-stop-single": "StartStopSingle", - "stem-value": "StemValue", - "step": "StepEnum", - "syllabic": "SyllabicEnum", - "symbol-size": "SymbolSize", - "tap-hand": "TapHand", - "text-direction": "TextDirection", - "tied-type": "TiedType", - "time-relation": "TimeRelationEnum", - "time-symbol": "TimeSymbol", - "tip-direction": "TipDirection", - "top-bottom": "TopBottom", - "tremolo-type": "TremoloType", - "trill-step": "TrillStep", - "two-note-turn": "TwoNoteTurn", - "up-down": "UpDown", - "up-down-stop-continue": "UpDownStopContinue", - "upright-inverted": "UprightInverted", - "valign": "Valign", - "valign-image": "ValignImage", - "wedge-type": "WedgeType", - "winged": "Winged", - "wood-value": "WoodValue", - "yes-no": "YesNo", -} - -NUMERIC_TYPE_MAP = { - "accordion-middle": "AccordionMiddleValue", - "beam-level": "BeamLevel", - "divisions": "DivisionsValue", - "fifths": "FifthsValue", - "midi-128": "Midi128", - "midi-16": "Midi16", - "midi-16384": "Midi16384", - "millimeters": "MillimetersValue", - "non-negative-decimal": "NonNegativeDecimal", - "number-level": "NumberLevel", - "number-of-lines": "NumberOfLines", - "octave": "OctaveValue", - "percent": "Percent", - "positive-decimal": "PositiveDecimal", - "positive-divisions": "PositiveDivisionsValue", - "rotation-degrees": "RotationDegrees", - "semitones": "Semitones", - "staff-line": "StaffLine", - "staff-number": "StaffNumber", - "string-number": "StringNumber", - "tenths": "TenthsValue", - "trill-beats": "TrillBeats", - "tremolo-marks": "TremoloMarks", - "byte": "Byte", -} - -BESPOKE_TYPES = { - "color": "Color", - "comma-separated-text": "CommaSeparatedText", - "distance-type": "DistanceType", - "font-size": "FontSize", - "line-width-type": "LineWidthType", - "mode": "ModeValue", - "number-or-normal": "NumberOrNormal", - "positive-integer-or-empty": "PositiveIntegerOrEmpty", - "yes-no-number": "YesNoNumber", - "ending-number": "EndingNumber", - "date": "Date", - "time-only": "TimeOnly", -} - -STRING_LIKE_TYPES = { - "XsString", "XsToken", "XsID", "XsIDREF", "XsNMToken", "XsAnyUri", - "PlaybackSoundType", -} - - -def uses_set_value(cpp_type: str) -> bool: - return cpp_type in STRING_LIKE_TYPES - - -def is_enum_value_type(cpp_type: str) -> bool: - return needs_parse_func(cpp_type) or cpp_type.endswith("Enum") or cpp_type in XMACRO_ENUM_TYPES - - -def resolve_cpp_type(xsd_type: str, model: XsdModel) -> str: - if xsd_type in XSD_TO_CPP_TYPE: - return XSD_TO_CPP_TYPE[xsd_type] - if xsd_type.startswith("xs:"): - return XSD_TO_CPP_TYPE.get(xsd_type, "XsString") - if xsd_type in SIMPLE_TYPE_TO_CPP: - return SIMPLE_TYPE_TO_CPP[xsd_type] - if xsd_type in NUMERIC_TYPE_MAP: - return NUMERIC_TYPE_MAP[xsd_type] - if xsd_type in BESPOKE_TYPES: - return BESPOKE_TYPES[xsd_type] - if xsd_type in model.enum_types: - base = pascal(xsd_type) - if base in model.class_names: - return base + "Enum" - return base - if xsd_type in model.simple_types: - st = model.simple_types[xsd_type] - if st["kind"] == "restriction": - return resolve_cpp_type(st["base"], model) - if st["kind"] == "union": - return pascal(xsd_type) - return pascal(xsd_type) - - -def resolve_attr_cpp_type(attr: XsdAttribute, model: XsdModel) -> str: - return resolve_cpp_type(attr.type_name, model) - - -ENUM_PARSE_FUNCS = {} - - -def needs_parse_func(cpp_type: str) -> bool: - return cpp_type in { - "FontStyle", "FontWeight", "AboveBelow", "LeftCenterRight", "Valign", - "ValignImage", "OverUnder", "TopBottom", "EnclosureShape", "StartStop", - "StartStopContinue", "StartStopSingle", "StartStopChangeContinue", - "StartStopDiscontinue", "YesNo", "OnOff", "UpDown", "BackwardForward", - "LineType", "LineShape", "WedgeType", "BarStyleEnum", "Fan", - "TipDirection", "TextDirection", "UprightInverted", "LeftRight", - "RightLeftMiddle", "BeamValue", "AccidentalValue", "ClefSign", - "StemValue", "NoteheadValue", "StepEnum", "Syllabic", "SymbolSize", - "TiedType", "FermataShape", "KindValue", "HarmonyType", - "DegreeTypeValue", "DegreeSymbolValue", "GroupSymbolValue", - "GroupBarlineValue", "MarginType", "TimeSymbol", "CancelLocation", - "ShowTuplet", "NoteTypeValue", "HandbellValue", "EffectValue", - "MetalValue", "WoodValue", "PitchedValue", "MembraneValue", - "SemiPitched", "TapHand", "TimeRelation", "LineEnd", "ShowFrets", - "CssFontSize", "MeasureNumberingValue", "StaffTypeEnum", - "StartNote", "TrillStep", "TwoNoteTurn", "Winged", "TremoloType", - "UpDownStopContinue", "NoteSizeType", "MuteEnum", - "BeaterValue", "BreathMarkValue", "HoleClosedValue", - "HoleClosedLocation", "TimeSeparator", "PrincipalVoiceSymbol", - "ModeValue", "XmlSpace", "XlinkType", "XlinkShow", "XlinkActuate", - } - - -def parse_func_name(cpp_type: str) -> str: - if cpp_type in XMACRO_ENUM_TYPES: - return f"{cpp_type}FromString" - return f"parse{cpp_type}" - - -# --------------------------------------------------------------------------- -# Include resolution -# --------------------------------------------------------------------------- - -TYPE_TO_HEADER = { - "XsString": "mx/core/XsString.h", - "XsToken": "mx/core/XsToken.h", - "XsID": "mx/core/XsID.h", - "XsIDREF": "mx/core/XsIDREF.h", - "XsNMToken": "mx/core/XsNMToken.h", - "XsAnyUri": "mx/core/XsAnyUri.h", - "XmlLang": "mx/core/XmlLang.h", - "XlinkHref": "mx/core/XlinkHref.h", - "XlinkRole": "mx/core/XlinkRole.h", - "XlinkTitle": "mx/core/XlinkTitle.h", - "Color": "mx/core/Color.h", - "CommaSeparatedText": "mx/core/CommaSeparatedText.h", - "CommaSeparatedPositiveIntegers": "mx/core/CommaSeparatedPositiveIntegers.h", - "FontSize": "mx/core/FontSize.h", - "NumberOrNormal": "mx/core/NumberOrNormal.h", - "PositiveIntegerOrEmpty": "mx/core/PositiveIntegerOrEmpty.h", - "YesNoNumber": "mx/core/YesNoNumber.h", - "EndingNumber": "mx/core/EndingNumber.h", - "Date": "mx/core/Date.h", - "TimeOnly": "mx/core/TimeOnly.h", - "PlaybackSound": "mx/core/PlaybackSound.h", - "PlaybackSoundType": "mx/core/PlaybackSoundType.h", -} - - -def header_for_type(cpp_type: str) -> str: - if cpp_type in TYPE_TO_HEADER: - return TYPE_TO_HEADER[cpp_type] - if "Decimal" in cpp_type or "Tenths" in cpp_type or "Millimeters" in cpp_type or \ - "Percent" in cpp_type or "Semitones" in cpp_type or "TrillBeats" in cpp_type or \ - "RotationDegrees" in cpp_type or "Divisions" in cpp_type: - return "mx/core/Decimals.h" - if any(cpp_type == t for t in [ - "AccordionMiddleValue", "BeamLevel", "Byte", "FifthsValue", "Integer", - "Midi128", "Midi16", "Midi16384", "NonNegativeInteger", "NumberLevel", - "NumberOfLines", "OctaveValue", "PositiveInteger", "StaffLine", - "StaffNumber", "StringNumber", "TremoloMarks", - ]): - return "mx/core/Integers.h" - return "mx/core/Enums.h" - - -# --------------------------------------------------------------------------- -# Enums.h / Enums.cpp generation -# --------------------------------------------------------------------------- - - -def generate_enums_h(model: XsdModel) -> str: - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/EnumsBuiltin.h"\n') - lines.append("#include ") - lines.append("#include ") - lines.append("#include \n") - lines.append("namespace mx\n{") - lines.append("namespace core\n{") - - for xsd_name, et in model.enum_types.items(): - cpp_name = pascal(xsd_name) - if cpp_name in model.class_names: - cpp_name += "Enum" - - doc = model.enum_docs.get(xsd_name, "") - - lines.append(f"/// {cpp_name} " + "/" * (80 - len(cpp_name) - 5)) - lines.append("///") - if doc: - for dline in _wrap_doc(doc, 96): - lines.append(f"/// {dline}") - lines.append("///") - - lines.append(f"enum class {cpp_name}") - lines.append("{") - for i, val in enumerate(et.values): - member = camel(val) - if val == "": - member = "emptystring" - lines.append(f" {member} = {i}" + ("," if i < len(et.values) - 1 else "")) - lines.append("};\n") - - lines.append(f"{cpp_name} parse{cpp_name}(const std::string &value);") - lines.append(f"std::optional<{cpp_name}> tryParse{cpp_name}(const std::string &value);") - lines.append(f"std::string toString(const {cpp_name} value);") - lines.append(f"std::ostream &toStream(std::ostream &os, const {cpp_name} value);") - lines.append(f"std::ostream &operator<<(std::ostream &os, const {cpp_name} value);\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _wrap_doc(text, width): - words = text.split() - result = [] - current = "" - for w in words: - if current and len(current) + 1 + len(w) > width: - result.append(current) - current = w - else: - current = current + " " + w if current else w - if current: - result.append(current) - return result - - -# --------------------------------------------------------------------------- -# Attributes Struct Generation -# --------------------------------------------------------------------------- - - -def attrs_struct_name(ct_name: str, model: XsdModel) -> str: - return pascal(ct_name) + "Attributes" - - -def element_attrs_struct_name(elem_name: str, model: XsdModel) -> str: - return element_class_name(elem_name) + "Attributes" - - -CORE_ROOT_ATTRS = { - "EmptyPrintObjectStyleAlignAttributes", -} - -ATTRS_TYPE_ALIAS = { - "empty-print-style-align": "empty-print-object-style-align", -} - -ELEMENTS_DIR_SHARED_ATTRS = { - "EmptyPlacementAttributes", - "EmptyLineAttributes", - "EmptyTrillSoundAttributes", - "EmptyFontAttributes", - "EmptyPrintStyleAlignAttributes", -} - - -def resolve_attrs_name(elem_name: str, type_name: str, model: XsdModel) -> str: - """Determine the correct attributes struct name for an element. - Some empty-* types use the type name (shared). Others use element name.""" - aliased = ATTRS_TYPE_ALIAS.get(type_name, type_name) - type_attrs = pascal(aliased) + "Attributes" - if type_attrs in CORE_ROOT_ATTRS or type_attrs in ELEMENTS_DIR_SHARED_ATTRS: - return type_attrs - return element_class_name(elem_name) + "Attributes" - - -def generate_attrs_h(struct_name: str, attrs: list, model: XsdModel) -> str: - preserves_xmlns = struct_name in XMLNS_PRESERVING_ATTRS - includes = set() - includes.add("mx/core/AttributesInterface.h") - includes.add("mx/core/ForwardDeclare.h") - for a in attrs: - cpp_t = resolve_attr_cpp_type(a, model) - h = header_for_type(cpp_t) - includes.add(h) - - lines = [LICENSE, "#pragma once\n"] - for inc in sorted(includes): - lines.append(f'#include "{inc}"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - if preserves_xmlns: - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({struct_name})\n") - lines.append(f"struct {struct_name} : public AttributesInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {struct_name}();") - lines.append(" virtual bool hasValues() const;") - lines.append(" virtual std::ostream &toStream(std::ostream &os) const;") - - for a in attrs: - cpp_t = resolve_attr_cpp_type(a, model) - cpp_n = camel(a.name) - lines.append(f" {cpp_t} {cpp_n};") - - for a in attrs: - cpp_n = camel(a.name) - has_name = has_flag_name(cpp_n) - const_prefix = "const " if a.use == "required" else "" - lines.append(f" {const_prefix}bool {has_name};") - - if preserves_xmlns: - lines.append(" std::vector> xmlnsDeclarations;") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# Per-attribute default value override, keyed by (attrs struct name, camelCase -# attribute field name). The value is the literal C++ initializer expression -# (e.g. '"it"'). Used when the committed code initializes an attribute to a -# value that is not encoded in the XSD (typically a hand-applied convention). -ATTR_DEFAULT_OVERRIDE = { - # AccidentalText's xml:lang attribute default: hand-applied "it" by the - # original codegen. Test01_AccidentalText asserts that setting hasLang - # without a value yields xml:lang="it". Not in the XSD. - ("AccidentalTextAttributes", "lang"): '"it"', - ("DirectiveAttributes", "lang"): '"it"', - # Lyric's justify default is hand-applied; the XSD says only "The default - # value varies for different elements". The original codegen chose - # 'center' here based on the doc text in the XSD annotation. - ("LyricAttributes", "justify"): "LeftCenterRight::center", - # Score{Partwise,Timewise}Attributes both expose the 'version' attribute - # from the document-attributes group. XSD says default="1.0" but mx/core - # hand-applies "3.0" so that newly-constructed scores serialize with the - # most recent supported version. Schema-driven generators preserve the - # hand-applied value via this override. - ("ScorePartwiseAttributes", "version"): '"3.0"', - ("ScoreTimewiseAttributes", "version"): '"3.0"', - # R4: xml:lang defaults hand-applied as "it" across text-bearing elements. - ("WordsAttributes", "lang"): '"it"', - ("TextAttributes", "lang"): '"it"', - ("RehearsalAttributes", "lang"): '"it"', - ("LyricLanguageAttributes", "lang"): '"it"', - ("CreditWordsAttributes", "lang"): '"it"', - # R4: BracketAttributes line-end default is 'down', not first enum 'up'. - ("BracketAttributes", "lineEnd"): "LineEnd::down", - # R4: NoteSizeAttributes type default is 'large', not first enum 'cue'. - ("NoteSizeAttributes", "type"): "NoteSizeType::large", - # R4: EndingAttributes number default is "1". - ("EndingAttributes", "number"): '"1"', - # R4: GroupingAttributes number default is "1". - ("GroupingAttributes", "number"): 'XsToken("1")', - # R4: PageMarginsAttributes type default is 'both', not first enum 'odd'. - ("PageMarginsAttributes", "type"): "MarginType::both", - # R4: LinkAttributes show default is 'replace', not first enum 'new'. - # Field name in struct is 'show', not 'xlinkShow'. - ("LinkAttributes", "show"): "XlinkShow::replace", - # R4: OtherAppearanceAttributes type default is "undefined". - ("OtherAppearanceAttributes", "type"): '"undefined"', - # R4: OtherNotationAttributes type default is 'start', not first enum 'single'. - ("OtherNotationAttributes", "type"): "StartStopSingle::start", - # R4: OtherOrnament/TechnicalAttributes placement: default_value_for_type - # returns AboveBelow::below but the committed code uses the default ctor - # which is AboveBelow::above (= 0). - ("OtherOrnamentAttributes", "placement"): "AboveBelow::above", - ("OtherTechnicalAttributes", "placement"): "AboveBelow::above", - # R4: PrincipalVoiceAttributes symbol default is 'none'. - ("PrincipalVoiceAttributes", "symbol"): "PrincipalVoiceSymbol::none", - # R4: StringMuteAttributes type default is 'on', not 'off'. - ("StringMuteAttributes", "type"): "OnOff::on", - # R4: PartGroupAttributes number default is "1". - ("PartGroupAttributes", "number"): 'XsToken("1")', - # R4: MetronomeAttributes halign/justify defaults are 'center'. - ("MetronomeAttributes", "halign"): "LeftCenterRight::center", - ("MetronomeAttributes", "justify"): "LeftCenterRight::center", -} - -# Attribute structs that preserve xmlns:* namespace declarations through -# round-trip. These elements may carry xmlns:xlink or other namespace -# declarations that mx does not model as typed fields, but must not drop. -XMLNS_PRESERVING_ATTRS = { - "ScorePartwiseAttributes", - "ScoreTimewiseAttributes", - "OpusAttributes", - "LinkAttributes", -} - -# Per-(parent-element-xml-name, child-element-xml-name) override for the -# constructor argument passed to make{Child}() when initializing the child -# on the parent's ctor init list. Used when HEAD initializes a required child -# with a non-default value (e.g. historical author choice rather than XSD spec). -CHILD_INIT_VALUE_OVERRIDE = { - # Scaling's millimeters and tenths use non-zero historical defaults. - ("scaling", "millimeters"): "MillimetersValue(7)", - ("scaling", "tenths"): "TenthsValue(40)", - # StaffDetails defaults staff-lines to 5 (author convention, not in XSD). - ("staff-details", "staff-lines"): "NonNegativeInteger(5)", -} - - -# Elements whose hasContents() should always return true regardless of what the -# XSD min/max-occurs analysis would produce. Keyed by element xml-name (not -# class name). Used when the committed HEAD hardcodes `return true;` for an -# element that has only optional children. -ELEMENT_HAS_CONTENTS_ALWAYS_TRUE = { - # MeasureLayout has a single optional child (measure-distance), but HEAD - # returns true unconditionally so that the element serialises as - # rather than . - "measure-layout", -} - -# Per-(element-name, child-xml-name) override for the min_occurs value that the -# generator uses when deciding whether a child needs a myHas flag. Keyed by -# (parent_element_xml_name, child_element_xml_name). Use this when XSD group -# inlining propagates minOccurs=0 from the enclosing group to an element that -# HEAD treats as unconditionally present (no getHas/setHas accessors). -CHILD_MIN_OCCURS_OVERRIDE = {} - - -def _apply_child_min_occurs_override(elem_name: str, children: list) -> list: - """Return a new children list with CHILD_MIN_OCCURS_OVERRIDE applied.""" - if not any((elem_name, c.element_name) in CHILD_MIN_OCCURS_OVERRIDE - for c in children): - return children - result = [] - for c in children: - key = (elem_name, c.element_name) - if key in CHILD_MIN_OCCURS_OVERRIDE: - c = XsdChildRef( - element_name=c.element_name, - min_occurs=CHILD_MIN_OCCURS_OVERRIDE[key], - max_occurs=c.max_occurs, - is_group=c.is_group, - ) - result.append(c) - return result - - -def default_value_for_type(cpp_type: str) -> str: - defaults = { - "FontStyle": "FontStyle::normal", - "FontWeight": "FontWeight::normal", - "AboveBelow": "AboveBelow::below", - "Valign": "Valign::bottom", - "ValignImage": "ValignImage::bottom", - "LeftCenterRight": "LeftCenterRight::left", - "EnclosureShape": "EnclosureShape::none", - "YesNo": "YesNo::no", - "OnOff": "OnOff::off", - "FontSize": "CssFontSize::medium", - "StartStop": "StartStop::start", - "StartStopContinue": "StartStopContinue::start", - "StartStopSingle": "StartStopSingle::single", - "LineType": "LineType::solid", - "LineShape": "LineShape::straight", - "SymbolSize": "SymbolSize::full", - } - return defaults.get(cpp_type, "") - - -def generate_attrs_cpp(struct_name: str, attrs: list, model: XsdModel) -> str: - preserves_xmlns = struct_name in XMLNS_PRESERVING_ATTRS - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{struct_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - # constructor - init_parts = [] - for a in attrs: - cpp_t = resolve_attr_cpp_type(a, model) - cpp_n = camel(a.name) - override = ATTR_DEFAULT_OVERRIDE.get((struct_name, cpp_n)) - if override: - init_parts.append(f"{cpp_n}({override})") - continue - dv = default_value_for_type(cpp_t) - if dv: - init_parts.append(f"{cpp_n}({dv})") - else: - init_parts.append(f"{cpp_n}()") - for a in attrs: - cpp_n = camel(a.name) - has_name = has_flag_name(cpp_n) - init_val = "true" if a.use == "required" else "false" - init_parts.append(f"{has_name}({init_val})") - - _emit_ctor_init(lines, f"{struct_name}::{struct_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - # hasValues - has_parts = [] - for a in attrs: - cpp_n = camel(a.name) - has_name = has_flag_name(cpp_n) - has_parts.append(has_name) - if preserves_xmlns: - has_parts.append("!xmlnsDeclarations.empty()") - lines.append(f"bool {struct_name}::hasValues() const") - lines.append("{") - if has_parts: - lines.append(f" return {' || '.join(has_parts)};") - else: - lines.append(" return false;") - lines.append("}\n") - - # toStream - lines.append(f"std::ostream &{struct_name}::toStream(std::ostream &os) const") - lines.append("{") - lines.append(" if (hasValues())") - lines.append(" {") - for a in attrs: - cpp_n = camel(a.name) - has_name = has_flag_name(cpp_n) - lines.append(f' streamAttribute(os, {cpp_n}, "{a.get_xml_name()}", {has_name});') - if preserves_xmlns: - lines.append(" for (const auto &ns : xmlnsDeclarations)") - lines.append(" {") - lines.append(' os << " " << ns.first << "=\\"" << ns.second << "\\"";') - lines.append(" }") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - - # fromXElementImpl - lines.append(f"bool {struct_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(f' const char *const className = "{struct_name}";') - lines.append(" bool isSuccess = true;") - required_locals = [] - for a in attrs: - if a.use == "required": - cpp_n = camel(a.name) - local_name = "is" + pascal(a.name) + "Found" - required_locals.append((a, local_name)) - lines.append(f" bool {local_name} = false;") - lines.append("") - lines.append(" auto it = xelement.attributesBegin();") - lines.append(" auto endIter = xelement.attributesEnd();\n") - lines.append(" for (; it != endIter; ++it)") - lines.append(" {") - required_local_map = {id(a): ln for a, ln in required_locals} - for a in attrs: - cpp_t = resolve_attr_cpp_type(a, model) - cpp_n = camel(a.name) - parse_has = required_local_map.get(id(a), has_flag_name(cpp_n)) - if needs_parse_func(cpp_t): - pf = parse_func_name(cpp_t) - lines.append(f" if (parseAttribute(message, it, className, isSuccess, {cpp_n}, {parse_has}, " - f'"{a.get_xml_name()}", &{pf}))') - else: - lines.append(f" if (parseAttribute(message, it, className, isSuccess, {cpp_n}, {parse_has}, " - f'"{a.get_xml_name()}"))') - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - if preserves_xmlns: - lines.append(" const auto attrName = it->getName();") - lines.append(' if (attrName == "xmlns" || (attrName.size() > 6 && attrName.substr(0, 6) == "xmlns:"))') - lines.append(" {") - lines.append(" xmlnsDeclarations.emplace_back(attrName, it->getValue());") - lines.append(" continue;") - lines.append(" }") - lines.append(" }\n") - for a, local_name in required_locals: - lines.append(f" if (!{local_name})") - lines.append(" {") - lines.append(" isSuccess = false;") - # Use the XSD attribute name (xml form, e.g. 'non-controlling') in - # the error message rather than hardcoding 'number'. The original - # codegen had a bug here that produced the wrong attribute name for - # any required attribute not named 'number' (visible in committed - # ScorePartAttributes.cpp, which says 'number' when it should say - # 'id'). - xml_name = a.get_xml_name() or a.name - lines.append(f' message << className << ": \'{xml_name}\' is a required attribute but was not found" << std::endl;') - lines.append(" }\n") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Element Class Generation -# --------------------------------------------------------------------------- - - -def classify_element(elem: XsdElement, model: XsdModel) -> str: - if elem.anonymous_type is not None: - ct = elem.anonymous_type - elif not elem.type_name: - return "unknown" - else: - ct = model.complex_types.get(elem.type_name) - if ct is None: - if elem.type_name in model.simple_types or elem.type_name in model.enum_types or \ - elem.type_name.startswith("xs:"): - return "simple-value" - return "unknown" - - if ct.has_simple_content: - if ct.attributes: - return "text-with-attrs" - return "text-value" - - if ct.has_choice and not ct.children: - return "choice" - - if ct.children: - if ct.has_choice: - return "sequence-with-choice" - if ct.attributes: - return "complex-with-attrs" - return "complex" - - if ct.attributes and not ct.children: - return "empty-with-attrs" - - if not ct.attributes and not ct.children: - return "empty" - - return "unknown" - - -def child_class_name(child: XsdChildRef) -> str: - name = pascal(child.element_name) - if child.is_group and child.element_name in GENERATE_GROUPS: - if child.element_name not in SUPPRESS_GROUP_SUFFIX: - name += "Group" - return name - - -PATTERN_B_EXCEPTIONS = {"String"} - - -def is_pattern_b(elem_type: str, ct: Optional[XsdComplexType], model: XsdModel, - class_name: str = "") -> bool: - if class_name in PATTERN_B_EXCEPTIONS: - return True - if ct is None: - return False - if ct.children or ct.has_choice: - return True - type_name = ct.name - if type_name in model.complex_types: - real_ct = model.complex_types[type_name] - if real_ct.children or real_ct.has_choice: - return True - return _ct_has_complex_content(type_name, model) - - -def _ct_has_complex_content(type_name: str, model: XsdModel) -> bool: - return type_name in model.complex_content_or_group_cts - - -def generate_element_h(elem_name: str, class_name: str, stream_name: str, - elem_type: str, ct: Optional[XsdComplexType], - model: XsdModel, type_name: str = "") -> str: - attrs_name = None - value_type = None - children = [] - - if ct: - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name or ct.name, model) - if ct.has_simple_content: - value_type = resolve_cpp_type(ct.simple_content_base, model) - children = _apply_child_min_occurs_override(elem_name, ct.children) - - # Apply value-type override if configured for this element - vt_override = ELEMENT_VALUE_TYPE_OVERRIDE.get(elem_name) - if vt_override: - value_type = vt_override["cpp_type"] - - lines = [LICENSE, "#pragma once\n"] - project_includes = ['"mx/core/ElementInterface.h"', '"mx/core/ForwardDeclare.h"'] - if value_type: - project_includes.append(f'"{header_for_type(value_type)}"') - if vt_override: - for extra in vt_override.get("extra_includes", []): - project_includes.append(f'"{extra}"') - if attrs_name: - if attrs_name in CORE_ROOT_ATTRS: - project_includes.append(f'"mx/core/{attrs_name}.h"') - else: - project_includes.append(f'"mx/core/elements/{attrs_name}.h"') - for inc in sorted(project_includes): - lines.append(f"#include {inc}") - - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - - has_unbounded_synth = any( - c.is_group and c.element_name in SYNTHETIC_UNBOUNDED_GROUPS - for c in children - ) - if has_unbounded_synth: - lines.append("namespace ezxml\n{\nclass XElementIterator;\n}") - lines.append("") - - lines.append("namespace mx\n{\nnamespace core\n{\n") - - if attrs_name: - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - - child_classes = sorted(set(child_class_name(c) for c in children)) - for cc in child_classes: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - if value_type: - if value_type in XMACRO_ENUM_TYPES: - lines.append(f"\ninline {class_name}Ptr make{class_name}({value_type} value)") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>(value);") - lines.append("}") - else: - lines.append(f"\ninline {class_name}Ptr make{class_name}(const {value_type} &value)") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>(value);") - lines.append("}") - lines.append(f"\ninline {class_name}Ptr make{class_name}({value_type} &&value)") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>(std::move(value));") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - - if value_type: - lines.append(f" {class_name}(const {value_type} &value);") - - lines.append("") - pattern_b = is_pattern_b(elem_type, ct, model, class_name) - if pattern_b: - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - else: - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - if attrs_name: - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &attributes);") - - if value_type: - lines.append(f" {value_type} getValue() const;") - lines.append(f" void setValue(const {value_type} &value);") - - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"\n /* _________ {cc} minOccurs = {child.min_occurs}, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - elif child.min_occurs == 0: - lines.append(f"\n /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f"\n /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - - has_private_members = bool(attrs_name) or bool(value_type) or bool(children) - if has_private_members: - lines.append("") - lines.append(" private:") - if value_type: - lines.append(f" {value_type} myValue;") - if attrs_name: - lines.append(f" {attrs_name}Ptr myAttributes;") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if child.min_occurs == 0: - lines.append(f" bool myHas{cc};") - - unbounded_synth_children = [ - c for c in children - if c.is_group and c.element_name in SYNTHETIC_UNBOUNDED_GROUPS - ] - for child in unbounded_synth_children: - cc = child_class_name(child) - lines.append("") - lines.append(f" void import{cc}Set(std::ostream &message, ::ezxml::XElementIterator &iter,") - indent = " " * (len(f" void import{cc}Set(")) - lines.append(f"{indent}::ezxml::XElementIterator &endIter, bool &isSuccess);") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_ctor_init(lines: list, prefix: str, init_parts: list): - if not init_parts: - lines.append(prefix) - return - first_line = f"{prefix} : {init_parts[0]}" - if len(init_parts) == 1: - lines.append(first_line) - return - all_on_one = f"{prefix} : {', '.join(init_parts)}" - if len(all_on_one) <= 110: - lines.append(all_on_one) - return - indent = " " * 4 + ": " - current = f"{prefix}\n{indent}{init_parts[0]}" - for part in init_parts[1:]: - candidate = current + f", {part}" - last_line = candidate.split("\n")[-1] - if len(last_line) > 110: - current += f",\n {part}" - else: - current += f", {part}" - lines.append(current) - - -def generate_element_cpp(elem_name: str, class_name: str, stream_name: str, - elem_type: str, ct: Optional[XsdComplexType], - model: XsdModel, type_name: str = "") -> str: - attrs_name = None - value_type = None - children = [] - - if ct: - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name or ct.name, model) - if ct.has_simple_content: - value_type = resolve_cpp_type(ct.simple_content_base, model) - children = _apply_child_min_occurs_override(elem_name, ct.children) - - # Apply value-type override if configured for this element - vt_override = ELEMENT_VALUE_TYPE_OVERRIDE.get(elem_name) - if vt_override: - value_type = vt_override["cpp_type"] - - has_contents = bool(value_type) or bool(children) - - # R5: elements whose XSD declares a `minOccurs >= 1, maxOccurs=unbounded` - # child set must pre-seed the set with one default member, return - # hasContents() unconditionally, and refuse to drop below size 1 in - # remove/clear. Matches HEAD's pattern for harp-pedals, scordatura, - # frame, figured-bass. - seeded_children = [ - c for c in (children or []) - if c.min_occurs >= 1 and c.max_occurs != 1 and not c.is_group - ] - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - include_set = set( - f'#include "mx/core/elements/{child_class_name(c)}.h"' - for c in children - ) - # The fromXElementImpl for a parent with a synthetic optional group - # inlines the parsing of the group's members; pull those member headers - # into the parent .cpp so make*() and getXxx() resolve to complete types. - for child in children: - if (child.is_group - and (child.element_name in SYNTHETIC_OPTIONAL_GROUPS - or child.element_name in SYNTHETIC_UNBOUNDED_GROUPS)): - for gm in model.groups.get(child.element_name, []): - include_set.add( - f'#include "mx/core/elements/{child_class_name(gm)}.h"' - ) - child_includes = sorted(include_set) - for inc in child_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - # constructor - init_parts = [] - if value_type: - default_val = ELEMENT_DEFAULT_VALUE.get( - elem_name, TYPE_DEFAULT_VALUE.get(value_type, "")) - if default_val: - init_parts.append(f"myValue({default_val})") - else: - init_parts.append("myValue()") - if attrs_name: - init_parts.append(f"myAttributes(std::make_shared<{attrs_name}>())") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - init_parts.append(f"my{cc}Set()") - else: - child_init_val = CHILD_INIT_VALUE_OVERRIDE.get((elem_name, child.element_name)) - if child_init_val: - init_parts.append(f"my{cc}(make{cc}({child_init_val}))") - else: - init_parts.append(f"my{cc}(make{cc}())") - if child.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - - use_ei_ctor = not value_type and not children - if not value_type: - if use_ei_ctor and init_parts: - all_init = ["ElementInterface()"] + init_parts - _emit_ctor_init(lines, f"{class_name}::{class_name}()", all_init) - elif use_ei_ctor: - lines.append(f"{class_name}::{class_name}() : ElementInterface()") - elif init_parts: - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - else: - lines.append(f"{class_name}::{class_name}() : ElementInterface()") - lines.append("{") - for sc in seeded_children: - scc = child_class_name(sc) - lines.append(f" my{scc}Set.push_back(make{scc}());") - lines.append("}\n") - else: - default_val = ELEMENT_DEFAULT_VALUE.get( - elem_name, TYPE_DEFAULT_VALUE.get(value_type, "")) - default_init = f"myValue({default_val})" if default_val else "myValue()" - all_init = [default_init] - if attrs_name: - all_init.append(f"myAttributes(std::make_shared<{attrs_name}>())") - ctor_prefix = f"{class_name}::{class_name}()" - _emit_ctor_init(lines, ctor_prefix, all_init) - lines.append("{") - lines.append("}\n") - val_init = ["myValue(value)"] - if attrs_name: - val_init.append(f"myAttributes(std::make_shared<{attrs_name}>())") - val_prefix = f"{class_name}::{class_name}(const {value_type} &value)" - _emit_ctor_init(lines, val_prefix, val_init) - lines.append("{") - lines.append("}\n") - - pattern_b = is_pattern_b(elem_type, ct, model, class_name) - - def _gen_hasAttributes(): - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - if attrs_name: - lines.append(" return myAttributes->hasValues();") - else: - lines.append(" return false;") - lines.append("}\n") - - def _gen_hasContents(): - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - if elem_name in ELEMENT_HAS_CONTENTS_ALWAYS_TRUE: - lines.append(" return true;") - elif value_type: - lines.append(" return true;") - elif children: - has_required = any( - c.min_occurs >= 1 and c.max_occurs == 1 and not c.is_group - for c in children - ) - if has_required or seeded_children: - lines.append(" return true;") - else: - parts = [] - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - parts.append(f"my{cc}Set.size() > 0") - elif child.is_group and child.max_occurs != 1: - parts.append(f"my{cc}Set.size() > 0") - elif child.is_group and child.min_occurs == 0: - # Optional group: gate on the has-flag, not on the - # group's own hasContents(), which may return true even - # when the group should not be emitted (e.g. - # DisplayStepOctaveGroup always returns hasContents=true - # because its members are required within the group). - parts.append(f"myHas{cc}") - elif child.is_group: - parts.append(f"my{cc}->hasContents()") - elif child.min_occurs == 0: - parts.append(f"myHas{cc}") - if parts: - lines.append(f" return {' || '.join(parts)};") - else: - lines.append(" return false;") - else: - lines.append(" return false;") - lines.append("}\n") - - def _gen_streamAttributes(): - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - if attrs_name and children: - lines.append(" return myAttributes->toStream(os);") - elif attrs_name: - lines.append(" if (myAttributes)") - lines.append(" {") - lines.append(" myAttributes->toStream(os);") - lines.append(" }") - lines.append(" return os;") - else: - lines.append(" return os;") - lines.append("}\n") - - def _gen_streamName(): - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{stream_name}";') - lines.append(" return os;") - lines.append("}\n") - - def _emit_stream_children(indent: str): - for child in children: - cc = child_class_name(child) - if child.is_group and child.max_occurs != 1: - is_unbounded_synth = ( - child.element_name in SYNTHETIC_UNBOUNDED_GROUPS) - lines.append(f"{indent}for (auto x : my{cc}Set)") - lines.append(f"{indent}{{") - if is_unbounded_synth: - # Synthetic unbounded group members are all-optional, so - # the wrapper class can be empty; gate the newline+stream - # on x->hasContents() to suppress blank entries. - lines.append(f"{indent} if (x->hasContents())") - lines.append(f"{indent} {{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent} }}") - else: - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent}}}") - elif child.is_group: - # Any optional group (minOccurs=0) gates on its myHas flag - # rather than on hasContents(). This matches the original code - # for time-modification / NormalTypeNormalDotGroup (synthetic - # optional groups) as well as inlined XSD groups like - # display-step-octave that are also optional on the parent. - if child.min_occurs == 0: - guard = f"myHas{cc}" - else: - guard = f"my{cc}->hasContents()" - lines.append(f"{indent}if ({guard})") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{cc}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent}}}") - elif child.max_occurs != 1: - lines.append(f"{indent}for (auto x : my{cc}Set)") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - elif child.min_occurs == 0: - lines.append(f"{indent}if (myHas{cc})") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{cc}->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - else: - lines.append(f"{indent}os << std::endl;") - lines.append(f"{indent}my{cc}->toStream(os, indentLevel + 1);") - - def _gen_streamContents(): - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - if value_type: - lines.append(" MX_UNUSED(indentLevel);") - lines.append(" isOneLineOnly = true;") - if value_type in XMACRO_ENUM_TYPES: - lines.append(f" os << {value_type}ToString(myValue);") - else: - lines.append(" os << myValue;") - elif children: - has_required = any( - c.min_occurs >= 1 and c.max_occurs == 1 and not c.is_group - for c in children - ) - use_wrapping = elem_name in WRAPPING_STREAMCONTENTS - if has_required or seeded_children: - lines.append(" isOneLineOnly = false;") - _emit_stream_children(" ") - lines.append(" os << std::endl;") - elif use_wrapping: - lines.append(" if (hasContents())") - lines.append(" {") - _emit_stream_children(" ") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - else: - _emit_stream_children(" ") - # Build actual content check from children directly so that - # ELEMENT_HAS_CONTENTS_ALWAYS_TRUE elements (which return - # true unconditionally from hasContents()) still set - # isOneLineOnly correctly based on what was actually emitted. - content_parts = [] - for c in children: - cc = child_class_name(c) - if c.max_occurs != 1: - content_parts.append(f"my{cc}Set.size() > 0") - elif c.min_occurs == 0 and c.is_group: - content_parts.append(f"myHas{cc}") - elif c.min_occurs == 0: - content_parts.append(f"myHas{cc}") - else: - content_parts.append("true") - actual_check = " || ".join(content_parts) if content_parts else "false" - lines.append(f" if ({actual_check})") - lines.append(" {") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - else: - lines.append(" MX_UNUSED(indentLevel);") - lines.append(" isOneLineOnly = true;") - lines.append(" return os;") - - if pattern_b: - _gen_hasAttributes() - _gen_streamAttributes() - _gen_streamName() - _gen_hasContents() - _gen_streamContents() - else: - _gen_hasAttributes() - _gen_hasContents() - _gen_streamAttributes() - _gen_streamName() - _gen_streamContents() - lines.append("}\n") - - # getAttributes / setAttributes - if attrs_name: - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - # getValue / setValue - if value_type: - lines.append(f"{value_type} {class_name}::getValue() const") - lines.append("{") - lines.append(" return myValue;") - lines.append("}\n") - lines.append(f"void {class_name}::setValue(const {value_type} &value)") - lines.append("{") - lines.append(" myValue = value;") - lines.append("}\n") - - # child accessors - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - is_seeded = child in seeded_children - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - if is_seeded: - lines.append(f" if (my{cc}Set.size() > 1)") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - else: - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - if is_seeded: - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append("}\n") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - elif child.min_occurs == 0: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - # fromXElementImpl - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - if attrs_name and not value_type and not children: - lines.append(" return myAttributes->fromXElement(message, xelement);") - elif value_type and attrs_name: - if uses_set_value(value_type): - parse_call = "myValue.setValue(xelement.getValue());" - elif is_enum_value_type(value_type): - pfn = parse_func_name(value_type) - parse_call = f"myValue = {pfn}(xelement.getValue());" - else: - parse_call = "myValue.parse(xelement.getValue());" - lines.append(" bool isSuccess = true;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append(f" {parse_call}") - lines.append(" MX_RETURN_IS_SUCCESS;") - elif value_type: - is_enum = is_enum_value_type(value_type) - if uses_set_value(value_type): - parse_call = "myValue.setValue(xelement.getValue());" - elif is_enum: - pfn = parse_func_name(value_type) - parse_call = f"myValue = {pfn}(xelement.getValue());" - else: - parse_call = "myValue.parse(xelement.getValue());" - lines.append(" MX_UNUSED(message);") - if not is_enum: - lines.append(" MX_UNUSED(xelement);") - lines.append(f" {parse_call}") - lines.append(" return true;") - elif children: - lines.append(" bool isSuccess = true;") - if attrs_name: - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - required_children = [ - c for c in children - if c.min_occurs >= 1 and c.max_occurs == 1 and not c.is_group - ] - for child in required_children: - cc = child_class_name(child) - lines.append(f" bool is{cc}Found = false;") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - for child in children: - cc = child_class_name(child) - if (child.is_group and child.max_occurs != 1 - and child.element_name in SYNTHETIC_UNBOUNDED_GROUPS): - # Synthetic unbounded group: emit a call to a private helper - # method that parses consecutive members into one wrapper - # group instance per pass. The helper is generated below. - lines.append( - f" import{cc}Set(message, it, endIter, isSuccess);") - elif (child.is_group and child.min_occurs == 0 - and child.element_name in SYNTHETIC_OPTIONAL_GROUPS): - # No importGroup() exists for an anonymous synthetic group. - # Inline the parsing of its members directly into the parent - # element's fromXElementImpl loop, matching the original - # TimeModification.cpp parser. - group_members = list(model.groups.get(child.element_name, [])) - for gm in group_members: - gm_cc = child_class_name(gm) - lines.append(f' if (it->getName() == "{gm.element_name}")') - lines.append(" {") - lines.append(f" myHas{cc} = true;") - if gm.max_occurs != 1: - local = camel(gm.element_name) - lines.append(f" auto {local} = make{gm_cc}();") - lines.append(f" isSuccess &= {local}->fromXElement(message, *it);") - lines.append(f" my{cc}->add{gm_cc}({local});") - else: - lines.append(f" isSuccess = my{cc}->get{gm_cc}()->fromXElement(message, *it);") - lines.append(" }") - elif child.is_group and child.min_occurs == 0: - lines.append(f" importGroup(message, it, endIter, isSuccess, my{cc}, myHas{cc});") - elif child.is_group: - lines.append(f" importGroup(message, it, endIter, isSuccess, my{cc});") - elif child.max_occurs != 1: - lines.append(f' importElementSet(message, it, endIter, isSuccess, "{child.element_name}", my{cc}Set);') - elif child.min_occurs == 0: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, myHas{cc}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - else: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, is{cc}Found))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - else: - lines.append(" MX_UNUSED(message);") - lines.append(" MX_UNUSED(xelement);") - lines.append(" return true;") - lines.append("}\n") - - # Helper methods for synthetic unbounded groups (e.g. - # ScorePart::importMidiDeviceInstrumentGroupSet). - for child in children: - if (child.is_group and child.max_occurs != 1 - and child.element_name in SYNTHETIC_UNBOUNDED_GROUPS): - _emit_synthetic_unbounded_helper( - lines, class_name, child, model) - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_synthetic_unbounded_helper(lines: list, parent_class: str, - child_ref: "XsdChildRef", - model: "XsdModel") -> None: - group_class = child_class_name(child_ref) - set_name = f"my{group_class}Set" - members = list(model.groups.get(child_ref.element_name, [])) - if not members: - return - name_check = " || ".join( - f'iter->getName() == "{m.element_name}"' for m in members) - name_not_check = " && ".join( - f'iter->getName() != "{m.element_name}"' for m in members) - has_check = " || ".join( - f"item->getHas{child_class_name(m)}()" for m in members) - - sig_first = f"void {parent_class}::import{group_class}Set(std::ostream &message, ::ezxml::XElementIterator &iter," - sig_indent = " " * len(f"void {parent_class}::import{group_class}Set(") - lines.append(sig_first) - lines.append(f"{sig_indent}::ezxml::XElementIterator &endIter, bool &isSuccess)") - lines.append("{") - lines.append(" bool doDecrementIter = false;") - lines.append(f" while (iter != endIter && ({name_check}))") - lines.append(" {") - lines.append(f" auto item = make{group_class}();") - for idx, m in enumerate(members): - mc = child_class_name(m) - local = camel(m.element_name) - guard = (f'iter->getName() == "{m.element_name}"' if idx == 0 - else f'iter != endIter && iter->getName() == "{m.element_name}"') - lines.append(f" if ({guard})") - lines.append(" {") - lines.append(f" item->setHas{mc}(true);") - lines.append(f" auto {local} = item->get{mc}();") - lines.append(f" isSuccess &= {local}->fromXElement(message, *iter);") - if m.element_name in SYNTHETIC_UNBOUNDED_GROUP_IMPORT_GROUP_AFTER: - lines.append(f" importGroup(message, iter, endIter, isSuccess, {local});") - lines.append(" doDecrementIter = true;") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(f" if ({has_check})") - lines.append(" {") - lines.append(f" {set_name}.push_back(item);") - lines.append(" }") - lines.append("") - lines.append(f" if (iter != endIter && {name_not_check})") - lines.append(" {") - lines.append(" if (doDecrementIter)") - lines.append(" {") - lines.append(" --iter;") - lines.append(" }") - lines.append(" return;") - lines.append(" }") - lines.append(" }") - lines.append(" if (doDecrementIter)") - lines.append(" {") - lines.append(" --iter;") - lines.append(" }") - lines.append("}\n") - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -# Elements whose generated code intentionally replaces the original bespoke -# implementation. Diffs are exempt from eval penalty scoring. The set contains -# file stems (PascalCase, no extension) that eval.py matches against. -OVERWRITE_FILE_STEMS = { - "Direction", "DirectionType", "DirectionAttributes", -} - -# Maps XSD element names to C++ class names when they differ from pascal(elem_name). -# The XML stream name remains the original elem_name; only the C++ identifier changes. -ELEMENT_CLASS_NAME_OVERRIDE = { - "attributes": "Properties", # XSD 'attributes' -> C++ class 'Properties' -} - -# Override the value type for a simple-value or text-value element. -# Each entry maps elem_name -> dict with: -# cpp_type: the C++ type to use instead of whatever the XSD says -# header: the header file providing that type -# default: the default-value expression for the constructor -# extra_includes: additional headers to include (list of strings) -# The streaming / parsing pattern is inferred from XMACRO_ENUM_TYPES, -# is_enum_value_type, or uses_set_value, just like any other value type. -ELEMENT_VALUE_TYPE_OVERRIDE = { - "instrument-sound": { - "cpp_type": "PlaybackSoundType", - "header": "mx/core/PlaybackSoundType.h", - "default": "PlaybackSoundType{}", - }, -} - -# Types defined via X-macros (EnumWithString.h pattern) that provide -# XxxToString / XxxFromString free functions instead of operator<< / parseXxx. -XMACRO_ENUM_TYPES = { - "PlaybackSound", -} - - -def element_class_name(elem_name: str) -> str: - """Return the C++ class name for an element, consulting overrides first.""" - return ELEMENT_CLASS_NAME_OVERRIDE.get(elem_name, pascal(elem_name)) - - -SKIP_ELEMENTS = { - # score-partwise, score-timewise: handled by shared bespoke generator - # _emit_score_wrapper_family, parameterized via SCORE_WRAPPER_FLAVOR_CONFIG. - # Each emits {Outer, set holder, music-data holder} + their attrs structs. - # part, measure: claimed by the score-wrapper-family handler (both - # partwise and timewise dispatch entries claim each name under a - # different class prefix). Listed in BESPOKE_FAMILY_OWNED rather than - # SKIP_ELEMENTS because they ARE fully generated, just not by their - # own dispatch entry. - # directive: handled via anonymous_type path (text-with-attrs, anon CT) - # part-list: handled by bespoke generator (PartGroupOrScorePart) - # credit: handled by bespoke generator (CreditChoice + CreditWordsGroup) - # key: handled by tree-based generation - # lyric: handled by bespoke generator (LyricTextChoice + SyllabicTextGroup - # + ElisionSyllabicTextGroup + ElisionSyllabicGroup) - # notations, ornaments: handled by tree-based generation - # part-abbreviation-display, part-name-display: handled by tree-based generation - # score-instrument: handled by tree-based generation (SoloOrEnsembleChoice) - # score-part: handled via UNBOUNDED_SEQUENCE_AS_GROUP -> MidiDeviceInstrumentGroup - # time-modification: handled via synthetic NormalTypeNormalDotGroup - # (anonymous nested optional sequence promoted to a group class) -} - -# Elements whose code is emitted by some other bespoke handler as part of a -# family (e.g. score-partwise's family handler emits PartwisePart and -# PartwiseMeasure too). The main discovery loop must skip these so the default -# path doesn't try to generate competing files, but they are NOT counted as -# skipped because they ARE fully generated -- just not by their own dispatch -# entry. Distinct from SKIP_ELEMENTS which represents elements with no -# generator coverage at all. -BESPOKE_FAMILY_OWNED = { - "part", # PartwisePart (partwise) + TimewisePart (timewise) - "measure", # PartwiseMeasure (partwise) + TimewiseMeasure (timewise) -} - -TREE_ELEMENTS = { - "bend", - "group-abbreviation-display", - "group-name-display", - "harmonic", - "key", - "metronome", - "notations", - "notehead-text", - "ornaments", - "part-abbreviation-display", - "part-name-display", - "play", - "score-instrument", -} - -TREE_ELEMENT_CONFIG = { - "group-abbreviation-display": { - "choice_class": "DisplayTextOrAccidentalText", - }, - "group-name-display": { - "choice_class": "DisplayTextOrAccidentalText", - }, - "harmonic": { - "inline_choices": [ - {"choice_class": "HarmonicTypeChoice"}, - {"choice_class": "HarmonicInfoChoice"}, - ], - }, - "part-abbreviation-display": { - "choice_class": "DisplayTextOrAccidentalText", - }, - "part-name-display": { - "choice_class": "DisplayTextOrAccidentalText", - "always_has_contents": True, - }, - "notehead-text": { - "choice_class": "NoteheadTextChoice", - # HEAD seeds the choice set with one default item (displayText) so that - # hasContents() returns true and the element serialises with content. - "always_has_contents": True, - "seed_choice_set": True, - }, - "play": { - "inlined_choice": True, - }, - "score-instrument": { - "choice_class": "SoloOrEnsembleChoice", - }, - "metronome": { - "choice_class": "BeatUnitPerOrNoteRelationNoteChoice", - "container_names": { - 0: "BeatUnitPer", - 1: "NoteRelationNote", - }, - "branch_enum_names": { - 0: "beatUnitPer", - 1: "noteRelationNote", - }, - }, - # Issues E/F: route choice-group parsing through private member functions - # on the parent element (e.g. Key::importTraditionalKey, - # Key::importNonTraditionalKey) instead of through public importGroup(...) - # overloads in FromXElement.cpp. Each group branch in the parent's choice - # produces one private member: bool import(...). The parent's - # fromXElementImpl dispatches by calling those members; the choice's - # setChoice(...) is performed inside the member body. - "key": { - "parent_imports_choice_groups": True, - }, -} - -# Populated dynamically by XsdModel._synthesize_optional_group as we discover -# anonymous inside parent sequences. The names -# stored here are the lowercase-hyphenated form (e.g. "normal-type-normal-dot") -# which round-trips through pascal() to produce the synthetic group class -# (e.g. "NormalTypeNormalDotGroup"). -SYNTHETIC_OPTIONAL_GROUPS: set = set() - -# Populated dynamically by XsdModel._synthesize_unbounded_group when we -# discover an anonymous -# inside a parent sequence. The original codegen promoted some of these -# shapes to wrapper group classes used as Sets on the parent -# (e.g. score-part's midi-device + midi-instrument repeating sequence -# becomes MidiDeviceInstrumentGroup, held as a *Set on ScorePart). -SYNTHETIC_UNBOUNDED_GROUPS: set = set() - -# Opt-in: complex types whose anonymous nested -# should be promoted to a synthetic group rather than flattened. The XSD -# permits the same shape in several places (e.g. page-layout), but the -# original codegen only chose to promote it in specific spots. The value is -# the hyphenated-lowercase ref name used as the synthetic group's element_name. -NESTED_OPTIONAL_SEQUENCE_AS_GROUP: dict = { - "time-modification": "normal-type-normal-dot", -} - -# Opt-in: when an extending complexType inherits a synthetic optional group -# from its base, the default behavior is to flatten the group's members into -# the extending type. For specific extending types the original codegen -# instead kept the group as a *separately-named wrapper sub-element* with -# its own getHas/setHas accessors. The mapping is -# extending_type_name -> { base_synthetic_group_name -> renamed_wrapper_group_name } -# The renamed group's class name omits the usual "Group" suffix (see -# SUPPRESS_GROUP_SUFFIX), so a child reference to it renders as a regular -# wrapper element on the parent. Its members are still parsed inline like any -# other synthetic optional group (the original hand-written MetronomeTuplet.cpp -# parsed the wrapper with a no-op importElement and dropped normal-type / -# normal-dot on round-trip; that was a bug). -EXTENSION_OPTIONAL_GROUP_RENAME: dict = { - "metronome-tuplet": { - "normal-type-normal-dot": "time-modification-normal-type-normal-dot", - }, -} - -# Group names whose generated class name omits the trailing "Group" suffix. -SUPPRESS_GROUP_SUFFIX: set = set() - - -def group_class_name(group_name: str) -> str: - if group_name in SUPPRESS_GROUP_SUFFIX: - return pascal(group_name) - return pascal(group_name) + "Group" - -# Opt-in: complex types whose anonymous should be promoted to a synthetic unbounded group. -# Mapping parent_type_name -> hyphenated-lowercase synthetic group ref. -UNBOUNDED_SEQUENCE_AS_GROUP: dict = { - "score-part": "midi-device-instrument", -} - -# Element names whose generated synthetic-unbounded-group parser body should -# emit an additional importGroup(messsage, iter, endIter, isSuccess, elemPtr) -# call after parsing that element. The original codegen produced this call -# for midi-instrument (a no-op in practice because importGroup(MidiInstrument) -# inspects only sibling iterators that have already been consumed). Kept to -# minimize diff against committed. -SYNTHETIC_UNBOUNDED_GROUP_IMPORT_GROUP_AFTER = { - "midi-instrument", -} - -GENERATE_GROUPS = { - "beat-unit", "display-step-octave", "editorial", "editorial-voice", - "editorial-voice-direction", "layout", "score-header", - # full-note: EXC - real code has FullNoteTypeChoice class - # time-signature: EXC - real code adds Interchangeable not in XSD group - # harmony-chord: EXC - real code has Choice logic not in XSD group def - # music-data: EXC - real code wraps choice in MusicDataChoice class -} - -WRAPPING_STREAMCONTENTS = { - "defaults", "grouping", "identification", "part-group", "print", -} - -TYPE_DEFAULT_VALUE = { - "AccidentalValue": "AccidentalValue::natural", - "ArrowDirectionEnum": "ArrowDirectionEnum::up", - "ArrowStyleEnum": "ArrowStyleEnum::single", - "BarStyleEnum": "BarStyleEnum::regular", - "BeamValue": "BeamValue::begin", - "BeaterValue": "BeaterValue::snareStick", - "BreathMarkValue": "BreathMarkValue::emptystring", - "CircularArrowEnum": "CircularArrowEnum::clockwise", - "ClefSign": "ClefSign::g", - "DegreeTypeValue": "DegreeTypeValue::add", - "EffectEnum": "EffectEnum::anvil", - "FermataShape": "FermataShape::normal", - "GlassEnum": "GlassEnum::windChimes", - "GroupBarlineValue": "GroupBarlineValue::yes", - "GroupSymbolValue": "GroupSymbolValue::none", - "HandbellValue": "HandbellValue::damp", - "HoleClosedValue": "HoleClosedValue::no", - "KindValue": "KindValue::none", - "MeasureNumberingValue": "MeasureNumberingValue::none", - "MembraneEnum": "MembraneEnum::snareDrum", - "MetalEnum": "MetalEnum::bell", - "MuteEnum": "MuteEnum::off", - "NoteTypeValue": "NoteTypeValue::eighth", - "NoteheadValue": "NoteheadValue::normal", - "PitchedEnum": "PitchedEnum::xylophone", - "SemiPitchedEnum": "SemiPitchedEnum::medium", - "StaffTypeEnum": "StaffTypeEnum::regular", - "StemValue": "StemValue::none", - "StepEnum": "StepEnum::a", - "StickLocationEnum": "StickLocationEnum::center", - "StickMaterialEnum": "StickMaterialEnum::medium", - "StickTypeEnum": "StickTypeEnum::yarn", - "SyllabicEnum": "SyllabicEnum::begin", - "TimeRelationEnum": "TimeRelationEnum::equals", - "WoodEnum": "WoodEnum::claves", - "PlaybackSound": "PlaybackSound::keyboardPiano", -} - -ELEMENT_DEFAULT_VALUE = { - "type": "NoteTypeValue::quarter", - "duration": "1.0", - "tremolo": "3", - "metronome-relation": '"equals"', -} - - -def generate_group_h(group_name: str, children: list, model: XsdModel) -> str: - class_name = group_class_name(group_name) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - child_classes = [child_class_name(c) for c in children] - for cc in sorted(set(child_classes)): - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"\n /* _________ {cc} minOccurs = {child.min_occurs}, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - elif child.min_occurs == 0: - lines.append(f"\n /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f"\n /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if child.min_occurs == 0: - lines.append(f" bool myHas{cc};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_group_cpp(group_name: str, children: list, model: XsdModel) -> str: - class_name = group_class_name(group_name) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - child_includes = sorted(set( - f'#include "mx/core/elements/{child_class_name(c)}.h"' - for c in children - )) - for inc in child_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - init_parts.append(f"my{cc}Set()") - else: - init_parts.append(f"my{cc}(make{cc}())") - if child.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - parts = [] - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - parts.append(f"my{cc}Set.size() > 0") - elif child.min_occurs == 0: - parts.append(f"myHas{cc}") - else: - parts.append("true") - if any("true" == p for p in parts): - lines.append(" return true;") - elif parts: - lines.append(f" return {' || '.join(parts)};") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - all_optional = all(c.min_occurs == 0 and c.max_occurs == 1 for c in children) - if all_optional: - lines.append(" bool firstItem = true;") - lines.append(" isOneLineOnly = true;") - for child in children: - cc = child_class_name(child) - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" if (!firstItem)") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" firstItem = false;") - lines.append(" }") - else: - lines.append(" bool isFirst = true;") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" }") - else: - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" isOneLineOnly = !hasContents();") - lines.append(" return os;") - lines.append("}\n") - - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - elif child.min_occurs == 0: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - # fromXElementImpl - most groups use the UNUSED macro since they are - # imported via importGroup helpers in the parent. score-header is the - # exception: ScorePartwise/ScoreTimewise call myScoreHeaderGroup->fromXElement - # directly, so it needs a real parsing body. - if _group_needs_real_from_x(group_name): - _emit_group_real_from_x_impl(lines, class_name, children) - else: - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -GROUPS_WITH_REAL_FROM_X_ELEMENT = { - "score-header", - # ArrowGroup is the inline-group branch of the inline-choice element - # (INLINE_CHOICE_CONFIG["arrow"]). Arrow::fromXElementImpl dispatches its - # group branch via myArrowGroup->fromXElement(message, xelement), so the - # group needs a real parsing body. - "arrow", -} - - -def _group_needs_real_from_x(group_name: str) -> bool: - # The original codegen emits a real fromXElementImpl body for the - # synthetic optional groups (e.g. NormalTypeNormalDotGroup), even though - # they are never invoked directly by the parent (which inlines its own - # parsing). Preserve that behavior to minimize diff against committed. - return (group_name in GROUPS_WITH_REAL_FROM_X_ELEMENT - or group_name in SYNTHETIC_OPTIONAL_GROUPS) - - -def _emit_group_real_from_x_impl(lines: list, class_name: str, children: list) -> None: - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - - required = [c for c in children if c.min_occurs >= 1 and c.max_occurs == 1] - for child in required: - cc = child_class_name(child) - lines.append(f" bool is{cc}Found = false;") - - unbounded = [c for c in children if c.max_occurs != 1] - for child in unbounded: - cc = child_class_name(child) - lines.append(f" bool isFirst{cc}Added = false;") - - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append("") - for idx, child in enumerate(children): - cc = child_class_name(child) - elem_name = child.element_name - cond = "if" if idx == 0 else "else if" - lines.append(f' {cond} (elementName == "{elem_name}")') - lines.append(" {") - if child.max_occurs != 1: - local = camel(elem_name) - lines.append(f" auto {local} = make{cc}();") - lines.append(f" isSuccess &= {local}->fromXElement(message, *it);") - lines.append("") - lines.append(f" if (!isFirst{cc}Added && my{cc}Set.size() == 1)") - lines.append(" {") - lines.append(f" *(my{cc}Set.begin()) = {local};") - lines.append(f" isFirst{cc}Added = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{cc}Set.push_back({local});") - lines.append(f" isFirst{cc}Added = true;") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" myHas{cc} = true;") - lines.append(f" isSuccess &= my{cc}->fromXElement(message, *it);") - else: - lines.append(f" is{cc}Found = true;") - lines.append(f" isSuccess &= my{cc}->fromXElement(message, *it);") - lines.append(" }") - - lines.append(" else") - lines.append(" {") - if required: - first_required = required[0] - rcc = child_class_name(first_required) - rname = first_required.element_name - lines.append(f" if (!is{rcc}Found)") - lines.append(" {") - lines.append(f' message << "{class_name}: a \'{rname}\' element is required but was not found" << std::endl;') - lines.append(" return false;") - lines.append(" }") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - -DYNAMICS_MARKS = { - "p", "pp", "ppp", "pppp", "ppppp", "pppppp", - "f", "ff", "fff", "ffff", "fffff", "ffffff", - "mp", "mf", "sf", "sfp", "sfpp", "fp", "rf", "rfz", "sfz", "sffz", "fz", - "other-dynamics", -} - -CHOICE_SKIP = set() - -ENUM_VALUE_CHOICE_CONFIG = { - "dynamics": { - "value_type": "DynamicsValue", - "enum_type": "DynamicsEnum", - "other_variant": "otherDynamics", - "other_xml_name": "other-dynamics", - }, -} - -INLINE_CHOICE_CONFIG = { - "arrow": { - "branches": [ - { - "enum_name": "arrowGroup", - "class_name": "ArrowGroup", - "is_group": True, - "children": [ - {"name": "arrow-direction", "min": 1, "max": 1}, - {"name": "arrow-style", "min": 0, "max": 1}, - ], - }, - { - "enum_name": "circularArrow", - "class_name": "CircularArrow", - "is_group": False, - "element_name": "circular-arrow", - }, - ], - "enum_start": 1, - }, -} - -CHOICE_ELEMENT_CONFIG = { - "articulations": { - "choice_class": "ArticulationsChoice", "is_set": True, "enum_start": 1, - "choice_from_x": "manual", - "choice_stream_start": "mx_unused", "choice_stream_end": None, - "choice_indent_offset": 0, "choice_braces": True, - "parent_from_x": "simple_loop", "parent_return": "bare", - "parent_else_iol": "true", "parent_if_iol": True, - }, - "technical": { - "choice_class": "TechnicalChoice", "is_set": True, "enum_start": 1, - "choice_from_x": "unused", - "choice_stream_start": None, "choice_stream_end": "is_one_line", - "choice_indent_offset": 0, "choice_braces": True, - "parent_from_x": "dispatch", "parent_return": "macro", - "parent_else_iol": "false", "parent_if_iol": False, - }, - "encoding": { - "choice_class": "EncodingChoice", "is_set": True, "enum_start": 1, - "choice_from_x": "macro", - "choice_stream_start": "endl", "choice_stream_end": "is_one_line", - "choice_indent_offset": 1, "choice_braces": True, - "parent_from_x": "simple_loop", "parent_return": "macro", - "parent_else_iol": None, "parent_if_iol": False, - "parent_stream_style": "is_first", - "parent_method_order": "remove_add", - "parent_no_get": True, - }, - "percussion": { - "choice_class": "PercussionChoice", "is_set": False, "enum_start": 1, - "choice_is_set": True, - "choice_qualified_ctor": True, - "choice_from_x": "manual_bad", - "choice_stream_start": None, "choice_stream_end": "is_one_line", - "choice_indent_offset": 0, "choice_braces": True, - # The percussion choice (glass | metal | wood | ...) is selected by the - # *child* element of , so the parent must iterate its - # children and hand each to PercussionChoice (which dispatches on the - # child name). "delegate" would pass the element itself to - # the choice, which then rejects 'percussion' as unrecognized. - "parent_from_x": "child_loop", "parent_return": "macro", - "extra_children": ["stick-type", "stick-material"], - "extra_children_after": "stick", - }, - "measure-style": { - "choice_class": "MeasureStyleChoice", "is_set": False, "enum_start": 0, - "choice_from_x": "macro", - "choice_stream_start": "is_one_line", "choice_stream_end": None, - "choice_indent_offset": 1, "choice_braces": False, - "parent_from_x": "for_loop", "parent_return": "macro", - "parent_stream_iol_first": True, - "parent_stream_indent_offset": 0, - }, - "direction-type": { - "choice_class": "DirectionType", "is_set": True, "enum_start": 1, - "skip_parent": True, - "choice_from_x": "unused", - "choice_stream_start": "is_one_line", "choice_stream_end": "is_one_line_endl", - "choice_indent_offset": 1, "choice_braces": True, - }, - "time": { - "choice_class": "TimeChoice", "is_set": False, "enum_start": 0, - "bespoke_choice": True, - "parent_from_x": "time_group", - "parent_stream_iol_last": True, - "first_var_name": "TimeSignature", - }, -} - - -# --------------------------------------------------------------------------- -# Choice Class Generation -# --------------------------------------------------------------------------- - - -def generate_choice_class_h(choice_class: str, choice_children: list, - is_set: bool, enum_start: int, - parent_name: str, model: XsdModel) -> str: - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - child_classes = sorted(set(pascal(c.element_name) for c in choice_children)) - for cc in child_classes: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})\n") - - lines.append(f"inline {choice_class}Ptr make{choice_class}()") - lines.append("{") - lines.append(f" return std::make_shared<{choice_class}>();") - lines.append("}") - - lines.append(f"\nclass {choice_class} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, child in enumerate(choice_children): - cc_camel = camel(child.element_name) - val = i + enum_start - comma = "," if i < len(choice_children) - 1 else "" - lines.append(f" {cc_camel} = {val}{comma}") - lines.append(" };") - lines.append(f" {choice_class}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - if is_set: - lines.append(f"\n /* _________ Choice minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {choice_class}::Choice getChoice() const;") - lines.append(f" void setChoice(const {choice_class}::Choice value);") - else: - lines.append(f" Choice getChoice() const;") - lines.append(f" void setChoice(const Choice value);") - - for child in choice_children: - cc = pascal(child.element_name) - min_occurs = getattr(child, "min_occurs", 1) - max_occurs = getattr(child, "max_occurs", 1) - max_occurs_text = "unbounded" if max_occurs == -1 else str(max_occurs) - lines.append(f"\n /* _________ {cc} minOccurs = {min_occurs}, maxOccurs = {max_occurs_text} _________ */") - if max_occurs != 1: - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - else: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - for child in choice_children: - cc = pascal(child.element_name) - if getattr(child, "max_occurs", 1) != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_choice_class_cpp(choice_class: str, choice_children: list, - is_set: bool, enum_start: int, - parent_name: str, model: XsdModel, - config: dict = None) -> str: - if config is None: - config = {} - choice_from_x = config.get("choice_from_x", "manual") - stream_start = config.get("choice_stream_start", "mx_unused" if is_set else "is_one_line") - stream_end = config.get("choice_stream_end") - indent_offset = config.get("choice_indent_offset", 0 if is_set else 1) - use_braces = config.get("choice_braces", is_set) - # When skip_parent is True the choice class is itself the wrapper element - # (its streamName emits the tag), so each case branch must insert a leading - # newline before the child's toStream. When skip_parent is False the - # parent element handles inter-child newlines before calling - # choice->streamContents, so case branches must NOT emit a leading endl - # (doing so produces a spurious blank line + misindented child). - branch_leading_endl = bool(config.get("skip_parent")) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{choice_class}.h"') - lines.append('#include "mx/core/FromXElement.h"') - child_includes = sorted(set( - f'#include "mx/core/elements/{pascal(c.element_name)}.h"' - for c in choice_children - )) - for inc in child_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - first_child_camel = camel(choice_children[0].element_name) - qualify_ctor = config.get("choice_qualified_ctor", not is_set) - choice_prefix = f"{choice_class}::" if qualify_ctor else "" - init_parts = [f"myChoice({choice_prefix}Choice::{first_child_camel})"] - branch_set_children = [] - for child in choice_children: - cc = pascal(child.element_name) - if getattr(child, "max_occurs", 1) != 1: - init_parts.append(f"my{cc}Set()") - branch_set_children.append(child) - else: - init_parts.append(f"my{cc}(make{cc}())") - _emit_ctor_init(lines, f"{choice_class}::{choice_class}()", init_parts) - lines.append("{") - for child in branch_set_children: - cc = pascal(child.element_name) - min_occurs = getattr(child, "min_occurs", 1) - lines.append(f" while (my{cc}Set.size() < {min_occurs})") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append(" }") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamName(std::ostream &os) const") - lines.append("{") - if is_set: - lines.append(f' os << "{parent_name}";') - lines.append(" return os;") - else: - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - indent_expr = "indentLevel" if indent_offset == 0 else f"indentLevel + {indent_offset}" - - lines.append(f"std::ostream &{choice_class}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - if stream_start == "mx_unused": - lines.append(" MX_UNUSED(isOneLineOnly);\n") - elif stream_start == "endl": - lines.append(" os << std::endl;") - elif stream_start == "is_one_line": - lines.append(" isOneLineOnly = false;") - - lines.append(" switch (myChoice)") - lines.append(" {") - for child in choice_children: - cc = pascal(child.element_name) - cc_camel = camel(child.element_name) - branch_is_set = getattr(child, "max_occurs", 1) != 1 - if use_braces: - lines.append(f" case Choice::{cc_camel}: {{") - if branch_is_set: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" x->toStream(os, {indent_expr});") - lines.append(" }") - else: - if branch_leading_endl: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, {indent_expr});") - lines.append(" }") - lines.append(" break;") - else: - lines.append(f" case Choice::{cc_camel}:") - if branch_is_set: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" x->toStream(os, {indent_expr});") - lines.append(" }") - else: - if branch_leading_endl: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, {indent_expr});") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - if stream_end == "is_one_line": - lines.append(" isOneLineOnly = false;") - elif stream_end == "is_one_line_endl": - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"{choice_class}::Choice {choice_class}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - - if is_set: - lines.append(f"void {choice_class}::setChoice(const {choice_class}::Choice value)") - else: - lines.append(f"void {choice_class}::setChoice(const {choice_class}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - for child in choice_children: - cc = pascal(child.element_name) - min_occurs = getattr(child, "min_occurs", 1) - if getattr(child, "max_occurs", 1) != 1: - lines.append(f"const {cc}Set &{choice_class}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {choice_class}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" if (my{cc}Set.size() > {min_occurs})") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append(f" while (my{cc}Set.size() < {min_occurs})") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append(" }") - lines.append("}\n") - lines.append(f"{cc}Ptr {choice_class}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {choice_class}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {choice_class}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - if choice_from_x == "unused": - lines.append(f"MX_FROM_XELEMENT_UNUSED({choice_class});") - elif choice_from_x == "macro": - lines.append(f"bool {choice_class}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - for child in choice_children: - cc = pascal(child.element_name) - cc_camel = camel(child.element_name) - lines.append(f' MX_CHOICE_IF({cc_camel}, "{child.element_name}", {cc});') - lines.append(f" MX_BAD_ELEMENT_FAILURE({choice_class});") - lines.append("}\n") - elif choice_from_x == "manual_bad": - lines.append(f"bool {choice_class}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - for child in choice_children: - cc = pascal(child.element_name) - cc_camel = camel(child.element_name) - lines.append(f' if (xelement.getName() == "{child.element_name}")') - lines.append(" {") - lines.append(f" myChoice = Choice::{cc_camel};") - lines.append(f" return get{cc}()->fromXElement(message, xelement);") - lines.append(" }\n") - lines.append(f" MX_BAD_ELEMENT_FAILURE({choice_class});") - lines.append("}\n") - else: - lines.append(f"bool {choice_class}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - for child in choice_children: - cc = pascal(child.element_name) - cc_camel = camel(child.element_name) - lines.append(f' if (xelement.getName() == "{child.element_name}")') - lines.append(" {") - lines.append(f" myChoice = Choice::{cc_camel};") - lines.append(f" return get{cc}()->fromXElement(message, xelement);") - lines.append(" }\n") - lines.append(f' message << "{choice_class}: \'" << xelement.getName() << "\' is not allowed" << std::endl;') - lines.append(" return false;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Choice Parent Element Generation -# --------------------------------------------------------------------------- - - -def generate_choice_parent_h(elem_name: str, class_name: str, choice_class: str, - is_set: bool, has_attrs: bool, - attrs_name: Optional[str], model: XsdModel, - config: dict = None) -> str: - if config is None: - config = {} - parent_no_get = config.get("parent_no_get", False) - lines = [LICENSE, "#pragma once\n"] - project_includes = ['"mx/core/ElementInterface.h"', '"mx/core/ForwardDeclare.h"'] - if has_attrs and attrs_name: - if attrs_name in CORE_ROOT_ATTRS: - project_includes.append(f'"mx/core/{attrs_name}.h"') - else: - project_includes.append(f'"mx/core/elements/{attrs_name}.h"') - for inc in sorted(project_includes): - lines.append(f"#include {inc}") - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - if has_attrs and attrs_name: - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - - if is_set: - lines.append(f"\n /* _________ {choice_class} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {choice_class}Set &get{choice_class}Set() const;") - lines.append(f" void add{choice_class}(const {choice_class}Ptr &value);") - lines.append(f" void remove{choice_class}(const {choice_class}SetIterConst &value);") - lines.append(f" void clear{choice_class}Set();") - if not parent_no_get: - lines.append(f" {choice_class}Ptr get{choice_class}(const {choice_class}SetIterConst &setIterator) const;") - else: - lines.append(f" {choice_class}Ptr get{choice_class}() const;") - lines.append(f" void set{choice_class}(const {choice_class}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr myAttributes;") - if is_set: - lines.append(f" {choice_class}Set my{choice_class}Set;") - else: - lines.append(f" {choice_class}Ptr myChoice;") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_choice_parent_cpp(elem_name: str, class_name: str, choice_class: str, - is_set: bool, has_attrs: bool, - attrs_name: Optional[str], stream_name: str, - model: XsdModel, config: dict = None, - choice_children: list = None) -> str: - if config is None: - config = {} - parent_from_x = config.get("parent_from_x", "simple_loop" if is_set else "delegate") - parent_return = config.get("parent_return", "bare") - parent_else_iol = config.get("parent_else_iol", "true") - parent_if_iol = config.get("parent_if_iol", True) - parent_stream_style = config.get("parent_stream_style", "standard" if is_set else "single") - parent_method_order = config.get("parent_method_order", "add_remove") - parent_no_get = config.get("parent_no_get", False) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - - if parent_from_x == "dispatch" and choice_children: - child_includes = sorted(set( - f'#include "mx/core/elements/{pascal(c.element_name)}.h"' - for c in choice_children - )) - for inc in child_includes: - lines.append(inc) - - if parent_from_x == "time_group": - group_class = config.get("group_class", "TimeSignatureGroup") - single_class = config.get("single_class", "SenzaMisura") - lines.append(f'#include "mx/core/elements/{single_class}.h"') - - lines.append(f'#include "mx/core/elements/{choice_class}.h"') - - if parent_from_x == "time_group": - group_class = config.get("group_class", "TimeSignatureGroup") - lines.append(f'#include "mx/core/elements/{group_class}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - if has_attrs and attrs_name: - init_parts.append(f"myAttributes(std::make_shared<{attrs_name}>())") - if is_set: - init_parts.append(f"my{choice_class}Set()") - else: - init_parts.append(f"myChoice(make{choice_class}())") - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->hasValues();") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->toStream(os);") - else: - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{stream_name}";') - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - if is_set: - lines.append(f" return my{choice_class}Set.size() > 0;") - else: - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - if is_set: - if parent_stream_style == "is_first": - lines.append(" bool isFirst = true;") - lines.append(f" for (auto x : my{choice_class}Set)") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" isFirst = false;") - lines.append(" }") - lines.append(" x->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - lines.append(" if (hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" isOneLineOnly = !hasContents();") - else: - lines.append(" if (hasContents())") - lines.append(" {") - lines.append(f" for (auto x : my{choice_class}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(" os << std::endl;") - if parent_if_iol: - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - if parent_else_iol is not None: - lines.append(f" isOneLineOnly = {'true' if parent_else_iol == 'true' else 'false'};") - lines.append(" }") - else: - p_indent_off = config.get("parent_stream_indent_offset", 1) - p_indent = "indentLevel" if p_indent_off == 0 else f"indentLevel + {p_indent_off}" - if config.get("parent_stream_iol_first"): - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(f" myChoice->streamContents(os, {p_indent}, isOneLineOnly);") - lines.append(" os << std::endl;") - elif config.get("parent_stream_iol_last"): - lines.append(" os << std::endl;") - lines.append(f" myChoice->streamContents(os, {p_indent}, isOneLineOnly);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - else: - lines.append(" os << std::endl;") - lines.append(f" myChoice->streamContents(os, {p_indent}, isOneLineOnly);") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" return os;") - lines.append("}\n") - - if has_attrs and attrs_name: - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - if is_set: - cc = choice_class - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - - if parent_method_order == "remove_add": - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - else: - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - if not parent_no_get: - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - else: - cc = choice_class - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myChoice = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - if has_attrs: - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - - if is_set and parent_from_x == "dispatch" and choice_children: - lines.append(f" ::ezxml::XElementIterator end = xelement.end();\n") - lines.append(" for (auto it = xelement.begin(); it != end; ++it)") - lines.append(" {") - lines.append(' const std::string elementName = it->getName();') - lines.append(f" auto choice = make{choice_class}();\n") - first = True - for child in choice_children: - cc = pascal(child.element_name) - cc_camel = camel(child.element_name) - prefix = " if" if first else " else if" - lines.append(f'{prefix} (elementName == "{child.element_name}")') - lines.append(" {") - lines.append(f" choice->setChoice({choice_class}::Choice::{cc_camel});") - lines.append(f" isSuccess &= choice->get{cc}()->fromXElement(message, *it);") - lines.append(f" my{choice_class}Set.push_back(choice);") - lines.append(" continue;") - lines.append(" }") - first = False - lines.append(" else") - lines.append(" {") - lines.append(f' message << "{class_name}: unexpected element \'" << elementName << "\' encountered" << std::endl;') - lines.append(" isSuccess = false;") - lines.append(" }") - lines.append(" }\n") - lines.append(" MX_RETURN_IS_SUCCESS;") - elif is_set: - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - lines.append(f" auto item = make{choice_class}();") - lines.append(" isSuccess &= item->fromXElement(message, *it);") - lines.append(f" my{choice_class}Set.push_back(item);") - lines.append(" }\n") - if parent_return == "macro": - lines.append(" MX_RETURN_IS_SUCCESS;") - else: - lines.append(" return isSuccess;") - elif parent_from_x == "time_group": - group_class = config.get("group_class", "TimeSignatureGroup") - single_elem = config.get("single_element", "senza-misura") - single_camel = config.get("single_camel", "senzaMisura") - single_class = config.get("single_class", "SenzaMisura") - group_camel = config.get("group_camel", "timeSignature") - first_var = config.get("first_var_name", "TimeSignature") - lines.append(f" bool isFirst{first_var}Added = false;") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append("") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - lines.append(f' if (it->getName() == "{single_elem}")') - lines.append(" {") - lines.append(f" myChoice->setChoice({choice_class}::Choice::{single_camel});") - lines.append(f" isSuccess &= myChoice->get{single_class}()->fromXElement(message, *it);") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" myChoice->setChoice({choice_class}::Choice::{group_camel});") - lines.append(f" auto timeSignature = make{group_class}();") - lines.append(" importGroup(message, it, endIter, isSuccess, timeSignature);") - lines.append("") - lines.append(f" if (!isFirst{first_var}Added && myChoice->get{group_class}Set().size() == 1)") - lines.append(" {") - lines.append(f" myChoice->add{group_class}(timeSignature);") - lines.append(f" myChoice->remove{group_class}(myChoice->get{group_class}Set().cbegin());") - lines.append(f" isFirst{first_var}Added = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" myChoice->add{group_class}(timeSignature);") - lines.append(f" isFirst{first_var}Added = true;") - lines.append(" }") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" return isSuccess;") - elif parent_from_x == "delegate": - lines.append(f" isSuccess &= myChoice->fromXElement(message, xelement);") - lines.append(" MX_RETURN_IS_SUCCESS;") - else: - # "child_loop" (and the implicit default): the choice is selected by the - # child element, so iterate the parent's children and dispatch each to - # the choice's fromXElement. - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" isSuccess &= myChoice->fromXElement(message, *it);") - lines.append(" }\n") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Bespoke: Time Element -# --------------------------------------------------------------------------- - - -TIME_SIGNATURE_GROUP_CHILDREN = [ - XsdChildRef(element_name="beats", min_occurs=1, max_occurs=1), - XsdChildRef(element_name="beat-type", min_occurs=1, max_occurs=1), - XsdChildRef(element_name="interchangeable", min_occurs=0, max_occurs=1), -] - - -def generate_time_choice_h() -> str: - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.append("MX_FORWARD_DECLARE_ELEMENT(SenzaMisura)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(TimeSignatureGroup)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(TimeChoice)\n") - lines.append("inline TimeChoicePtr makeTimeChoice()") - lines.append("{") - lines.append(" return std::make_shared();") - lines.append("}") - lines.append("\nclass TimeChoice : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - lines.append(" timeSignature = 0,") - lines.append(" senzaMisura = 1") - lines.append(" };") - lines.append(" TimeChoice();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(" Choice getChoice() const;") - lines.append(" void setChoice(const Choice value);") - lines.append("") - lines.append(" /* _________ TimeSignature minOccurs = 1, maxOccurs = unbounded _________ */") - lines.append(" const TimeSignatureGroupSet &getTimeSignatureGroupSet() const;") - lines.append(" void addTimeSignatureGroup(const TimeSignatureGroupPtr &value);") - lines.append(" void removeTimeSignatureGroup(const TimeSignatureGroupSetIterConst &value);") - lines.append(" void clearTimeSignatureGroupSet();") - lines.append(" TimeSignatureGroupPtr getTimeSignatureGroup(const TimeSignatureGroupSetIterConst &setIterator) const;") - lines.append("") - lines.append(" SenzaMisuraPtr getSenzaMisura() const;") - lines.append(" void setSenzaMisura(const SenzaMisuraPtr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - lines.append(" TimeSignatureGroupSet myTimeSignatureGroupSet;") - lines.append(" SenzaMisuraPtr mySenzaMisura;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_time_choice_cpp() -> str: - lines = [LICENSE] - lines.append('#include "mx/core/elements/TimeChoice.h"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append('#include "mx/core/elements/BeatType.h"') - lines.append('#include "mx/core/elements/Beats.h"') - lines.append('#include "mx/core/elements/Interchangeable.h"') - lines.append('#include "mx/core/elements/SenzaMisura.h"') - lines.append('#include "mx/core/elements/TimeSignatureGroup.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - lines.append("TimeChoice::TimeChoice() : myChoice(Choice::timeSignature), myTimeSignatureGroupSet(), mySenzaMisura(makeSenzaMisura())") - lines.append("{") - lines.append(" myTimeSignatureGroupSet.push_back(makeTimeSignatureGroup());") - lines.append("}\n") - lines.append("bool TimeChoice::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append("std::ostream &TimeChoice::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append("std::ostream &TimeChoice::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append("bool TimeChoice::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - lines.append("std::ostream &TimeChoice::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - lines.append(" case Choice::timeSignature: {") - lines.append(" for (auto x : myTimeSignatureGroupSet)") - lines.append(" {") - lines.append(" x->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - lines.append(" }") - lines.append(" break;") - lines.append(" case Choice::senzaMisura: {") - lines.append(" mySenzaMisura->toStream(os, indentLevel);") - lines.append(" }") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - lines.append("TimeChoice::Choice TimeChoice::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - lines.append("void TimeChoice::setChoice(const Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - lines.append("const TimeSignatureGroupSet &TimeChoice::getTimeSignatureGroupSet() const") - lines.append("{") - lines.append(" return myTimeSignatureGroupSet;") - lines.append("}\n") - lines.append("void TimeChoice::removeTimeSignatureGroup(const TimeSignatureGroupSetIterConst &value)") - lines.append("{") - lines.append(" if (value != myTimeSignatureGroupSet.cend())") - lines.append(" {") - lines.append(" myTimeSignatureGroupSet.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append("void TimeChoice::addTimeSignatureGroup(const TimeSignatureGroupPtr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myTimeSignatureGroupSet.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append("void TimeChoice::clearTimeSignatureGroupSet()") - lines.append("{") - lines.append(" myTimeSignatureGroupSet.clear();") - lines.append(" myTimeSignatureGroupSet.push_back(makeTimeSignatureGroup());") - lines.append("}\n") - lines.append("TimeSignatureGroupPtr TimeChoice::getTimeSignatureGroup(const TimeSignatureGroupSetIterConst &setIterator) const") - lines.append("{") - lines.append(" if (setIterator != myTimeSignatureGroupSet.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(" return TimeSignatureGroupPtr();") - lines.append("}\n") - lines.append("SenzaMisuraPtr TimeChoice::getSenzaMisura() const") - lines.append("{") - lines.append(" return mySenzaMisura;") - lines.append("}\n") - lines.append("void TimeChoice::setSenzaMisura(const SenzaMisuraPtr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" mySenzaMisura = value;") - lines.append(" }") - lines.append("}\n") - lines.append("MX_FROM_XELEMENT_UNUSED(TimeChoice);") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Inline Choice Generation -# --------------------------------------------------------------------------- - - -def generate_inline_choice_h(elem_name: str, class_name: str, - config: dict, has_attrs: bool, - attrs_name: Optional[str]) -> str: - branches = config["branches"] - enum_start = config.get("enum_start", 1) - - lines = [LICENSE, "#pragma once\n"] - project_includes = ['"mx/core/ElementInterface.h"', '"mx/core/ForwardDeclare.h"'] - if has_attrs and attrs_name: - if attrs_name in CORE_ROOT_ATTRS: - project_includes.append(f'"mx/core/{attrs_name}.h"') - else: - project_includes.append(f'"mx/core/elements/{attrs_name}.h"') - for inc in sorted(project_includes): - lines.append(f"#include {inc}") - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - if has_attrs and attrs_name: - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for branch in branches: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({branch['class_name']})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, branch in enumerate(branches): - val = i + enum_start - comma = "," if i < len(branches) - 1 else "" - lines.append(f" {branch['enum_name']} = {val}{comma}") - lines.append(" };") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - - lines.append(f"\n /* _________ Choice _________ */") - lines.append(f" {class_name}::Choice getChoice() const;") - lines.append(f" void setChoice(const {class_name}::Choice value);") - - for branch in branches: - bc = branch["class_name"] - lines.append(f"\n /* _________ {bc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {bc}Ptr get{bc}() const;") - lines.append(f" void set{bc}(const {bc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr myAttributes;") - for branch in branches: - bc = branch["class_name"] - lines.append(f" {bc}Ptr my{bc};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_inline_choice_cpp(elem_name: str, class_name: str, - config: dict, has_attrs: bool, - attrs_name: Optional[str], - stream_name: str) -> str: - branches = config["branches"] - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - branch_includes = sorted(set( - f'#include "mx/core/elements/{b["class_name"]}.h"' - for b in branches - )) - for inc in branch_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - first_branch = branches[0] - init_parts = [f"myChoice(Choice::{first_branch['enum_name']})"] - if has_attrs and attrs_name: - init_parts.append(f"myAttributes(std::make_shared<{attrs_name}>())") - for branch in branches: - bc = branch["class_name"] - init_parts.append(f"my{bc}(make{bc}())") - - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->hasValues();") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->toStream(os);") - else: - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{stream_name}";') - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - for branch in branches: - bc = branch["class_name"] - lines.append(f" case Choice::{branch['enum_name']}: {{") - lines.append(" os << std::endl;") - if branch.get("is_group"): - lines.append(f" my{bc}->streamContents(os, indentLevel + 1, isOneLineOnly);") - else: - lines.append(f" my{bc}->toStream(os, indentLevel + 1);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - - if has_attrs and attrs_name: - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"{class_name}::Choice {class_name}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - lines.append(f"void {class_name}::setChoice(const {class_name}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - for branch in branches: - bc = branch["class_name"] - lines.append(f"{bc}Ptr {class_name}::get{bc}() const") - lines.append("{") - lines.append(f" return my{bc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{bc}(const {bc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{bc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - if has_attrs: - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);\n") - - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - - element_branches = [b for b in branches if not b.get("is_group")] - group_branches = [b for b in branches if b.get("is_group")] - - for eb in element_branches: - bc = eb["class_name"] - lines.append(f' if (it->getName() == "{eb["element_name"]}")') - lines.append(" {") - lines.append(f" myChoice = Choice::{eb['enum_name']};") - lines.append(f" isSuccess &= my{bc}->fromXElement(message, *it);") - # Once a child matches an element branch, the choice is decided; the - # group branch below must not also run (it parses the parent as the - # group and would overwrite the choice, then fail on the missing - # required group member). Move on to the next child. - lines.append(" continue;") - lines.append(" }") - - if group_branches: - gb = group_branches[0] - bc = gb["class_name"] - lines.append(f" myChoice = Choice::{gb['enum_name']};") - lines.append(f" isSuccess = my{bc}->fromXElement(message, xelement);") - - lines.append(" }\n") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_enum_value_choice_h(elem_name: str, class_name: str, - value_type: str, has_attrs: bool, - attrs_name: Optional[str]) -> str: - lines = [LICENSE, "#pragma once\n"] - project_includes = ['"mx/core/ElementInterface.h"', '"mx/core/Enums.h"', - '"mx/core/ForwardDeclare.h"'] - if has_attrs and attrs_name: - project_includes.append(f'"mx/core/elements/{attrs_name}.h"') - for inc in sorted(project_includes): - lines.append(f"#include {inc}") - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - if has_attrs and attrs_name: - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\ninline {class_name}Ptr make{class_name}(const {value_type} &value)") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>(value);") - lines.append("}") - lines.append(f"\ninline {class_name}Ptr make{class_name}({value_type} &&value)") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>(std::move(value));") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}(const {value_type} &value);") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name} &attributes);") - lines.append(f" {value_type} getValue() const;") - lines.append(f" void setValue(const {value_type} &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {value_type} myValue;") - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_enum_value_choice_cpp(elem_name: str, class_name: str, - value_type: str, enum_type: str, - other_variant: str, other_xml_name: str, - has_attrs: bool, - attrs_name: Optional[str]) -> str: - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_value = "myValue(value)" - init_default = "myValue()" - if has_attrs and attrs_name: - attrs_init = f"myAttributes(std::make_shared<{attrs_name}>())" - init_value += f", {attrs_init}" - init_default += f", {attrs_init}" - - lines.append(f"{class_name}::{class_name}(const {value_type} &value) : {init_value}") - lines.append("{") - lines.append("}\n") - - lines.append(f"{class_name}::{class_name}() : {init_default}") - lines.append("{") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' return os << "{elem_name}";') - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(f" if (myValue.getValue() == {enum_type}::{other_variant})") - lines.append(" {") - lines.append(' indent(os, indentLevel + 1);') - lines.append(' os << "<";') - lines.append(" core::toStream(os, myValue.getValue());") - lines.append(' os << ">";') - lines.append(" os << myValue;") - lines.append(' os << "";') - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(' indent(os, indentLevel + 1);') - lines.append(' os << "<";') - lines.append(" core::toStream(os, myValue.getValue());") - lines.append(' os << "/>";') - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" return os;") - lines.append("}\n") - - if has_attrs and attrs_name: - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - if has_attrs and attrs_name: - lines.append(" return myAttributes->hasValues();") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - if has_attrs and attrs_name: - lines.append(" return myAttributes->toStream(os);") - else: - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"{value_type} {class_name}::getValue() const") - lines.append("{") - lines.append(" return myValue;") - lines.append("}\n") - - lines.append(f"void {class_name}::setValue(const {value_type} &value)") - lines.append("{") - lines.append(" myValue = value;") - lines.append("}\n") - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - if has_attrs and attrs_name: - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append("") - lines.append(" auto b = xelement.begin();") - lines.append(" auto e = xelement.end();") - lines.append("") - lines.append(" if (b != e)") - lines.append(" {") - lines.append(f' if (b->getName() == "{other_xml_name}")') - lines.append(" {") - lines.append(" myValue.setValue(b->getValue());") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" myValue.setValue(b->getName());") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(f' message << "{class_name}: parsing failed" << std::endl;') - lines.append(" return false;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Tree-Based Generation (for elements with nested choice/sequence structure) -# --------------------------------------------------------------------------- - - -@dataclass -class TreeChoiceBranch: - enum_name: str - class_name: str - is_set: bool - is_group: bool - group_name: str = "" - xml_name: str = "" - is_container: bool = False - # XML element names that trigger this container branch (computed from first - # child of the container sequence). Empty for non-container branches. - trigger_names: list = field(default_factory=list) - - -@dataclass -class ContainerMember: - class_name: str - var_name: str - is_set: bool = False - is_optional: bool = False - use_stream_contents: bool = False - element_name: str = "" - - -@dataclass -class InlineChoice: - choice_class: str = "" - branches: list = field(default_factory=list) - is_set: bool = False - is_optional: bool = False - - -@dataclass -class TreeContainer: - class_name: str - members: list = field(default_factory=list) - - -@dataclass -class TreeGenPlan: - choice_class: str = "" - choice_branches: list = field(default_factory=list) - choice_is_set: bool = False - choice_is_optional: bool = False - groups_to_generate: list = field(default_factory=list) - leading_groups: list = field(default_factory=list) - leading_children: list = field(default_factory=list) - trailing_children: list = field(default_factory=list) - containers_to_generate: list = field(default_factory=list) - nested_choices_to_generate: list = field(default_factory=list) - optional_groups_to_generate: list = field(default_factory=list) - inline_choices: list = field(default_factory=list) - - -def _tree_has_nested_structure(content_tree) -> bool: - if isinstance(content_tree, SequenceNode): - for child in content_tree.children: - if isinstance(child, ChoiceNode): - return True - elif isinstance(content_tree, ChoiceNode): - return True - return False - - -def _get_tree_config(parent_name: str) -> dict: - return TREE_ELEMENT_CONFIG.get(parent_name, {}) - - -def _container_trigger_names(seq_node, model: "XsdModel") -> list: - """Return the XML element names that can appear first in seq_node. - - Used to identify which container branch to activate when iterating the - parent's child elements. Inspects the first child of seq_node: - - ElementRefNode: returns [element_name] - - GroupRefNode: returns the element_names of the group's children - """ - if not seq_node.children: - return [] - first = seq_node.children[0] - if isinstance(first, ElementRefNode): - return [first.element_name] - if isinstance(first, GroupRefNode): - group_children = model.groups.get(first.group_name, []) - names = [] - for gc in group_children: - if hasattr(gc, "element_name") and gc.element_name: - names.append(gc.element_name) - return names - return [] - - -def _derive_container_name(seq_node, parent_name: str, branch_idx: int) -> str: - config = _get_tree_config(parent_name) - container_names = config.get("container_names", {}) - if branch_idx in container_names: - return container_names[branch_idx] - - first = seq_node.children[0] if seq_node.children else None - if isinstance(first, GroupRefNode): - return pascal(first.group_name) + "Sequence" - elif isinstance(first, ElementRefNode): - return pascal(first.element_name) + "Sequence" - return pascal(parent_name) + f"Sequence{branch_idx + 1}" - - -def _derive_nested_choice_name(choice_node) -> str: - parts = [] - for branch in choice_node.branches: - if isinstance(branch, ElementRefNode): - parts.append(pascal(branch.element_name)) - elif isinstance(branch, GroupRefNode): - parts.append(pascal(branch.group_name)) - elif isinstance(branch, SequenceNode) and branch.children: - first = branch.children[0] - if isinstance(first, ElementRefNode): - parts.append(pascal(first.element_name)) - elif isinstance(first, GroupRefNode): - parts.append(pascal(first.group_name)) - return "Or".join(parts) + "Choice" if parts else "NestedChoice" - - -def _derive_optional_group_name(seq_node) -> str: - first = seq_node.children[0] if seq_node.children else None - if isinstance(first, ElementRefNode): - return pascal(first.element_name) + "Group" - elif isinstance(first, GroupRefNode): - return pascal(first.group_name) + "Group" - return "OptionalGroup" - - -def _analyze_sequence_members(seq_node, model: XsdModel, plan: TreeGenPlan, - parent_name: str) -> list: - members = [] - for child in seq_node.children: - if isinstance(child, ElementRefNode): - cls = pascal(child.element_name) - members.append(ContainerMember( - class_name=cls, - var_name=camel(child.element_name), - is_set=child.max_occurs != 1, - is_optional=child.min_occurs == 0, - use_stream_contents=False, - element_name=child.element_name, - )) - elif isinstance(child, GroupRefNode): - if child.group_name in GENERATE_GROUPS: - cls = pascal(child.group_name) + "Group" - vname = camel(child.group_name) + "Group" - else: - cls = pascal(child.group_name) - vname = camel(child.group_name) - group_children = model.groups.get(child.group_name, []) - plan.groups_to_generate.append((cls, group_children)) - members.append(ContainerMember( - class_name=cls, - var_name=vname, - is_set=child.max_occurs != 1, - use_stream_contents=True, - )) - elif isinstance(child, ChoiceNode): - nested_name = _derive_nested_choice_name(child) - nested_branches = [] - for nb in child.branches: - if isinstance(nb, ElementRefNode): - nb_cls = pascal(nb.element_name) - nested_branches.append(TreeChoiceBranch( - enum_name=camel(nb.element_name), - class_name=nb_cls, - is_set=nb.max_occurs != 1, - is_group=False, - xml_name=nb.element_name, - )) - elif isinstance(nb, GroupRefNode): - if nb.group_name in GENERATE_GROUPS: - nb_cls = pascal(nb.group_name) + "Group" - nb_enum = camel(nb.group_name) + "Group" - else: - nb_cls = pascal(nb.group_name) - nb_enum = camel(nb.group_name) - group_children = model.groups.get(nb.group_name, []) - plan.groups_to_generate.append((nb_cls, group_children)) - nested_branches.append(TreeChoiceBranch( - enum_name=nb_enum, - class_name=nb_cls, - is_set=nb.max_occurs != 1, - is_group=True, - group_name=nb.group_name, - )) - plan.nested_choices_to_generate.append( - (nested_name, nested_branches, parent_name)) - members.append(ContainerMember( - class_name=nested_name, - var_name=nested_name[0].lower() + nested_name[1:], - use_stream_contents=True, - )) - elif isinstance(child, SequenceNode) and child.min_occurs == 0: - group_name = _derive_optional_group_name(child) - group_members = [] - for gc in child.children: - if isinstance(gc, ElementRefNode): - gc_cls = pascal(gc.element_name) - group_members.append(XsdChildRef( - element_name=gc.element_name, - min_occurs=1, - max_occurs=1, - )) - plan.optional_groups_to_generate.append( - (group_name, group_members)) - members.append(ContainerMember( - class_name=group_name, - var_name=group_name[0].lower() + group_name[1:], - is_optional=True, - use_stream_contents=True, - )) - return members - - -def analyze_tree(elem_name: str, content_tree, model: XsdModel) -> Optional[TreeGenPlan]: - if not _tree_has_nested_structure(content_tree): - return None - - plan = TreeGenPlan() - tree_config = _get_tree_config(elem_name) - - if isinstance(content_tree, SequenceNode): - root_unbounded = content_tree.max_occurs != 1 - seen_choice = False - choice_index = 0 - inline_choice_configs = tree_config.get("inline_choices", []) - for child in content_tree.children: - if isinstance(child, ChoiceNode): - if inline_choice_configs and choice_index < len(inline_choice_configs): - cc_name = inline_choice_configs[choice_index]["choice_class"] - elif choice_index == 0: - cc_name = tree_config.get("choice_class", pascal(elem_name) + "Choice") - else: - cc_name = pascal(elem_name) + f"Choice{choice_index + 1}" - is_set = (child.max_occurs != 1) or root_unbounded - is_optional = ( - child.min_occurs == 0 and child.max_occurs == 1 and not root_unbounded - ) - branches = [] - for branch in child.branches: - if isinstance(branch, GroupRefNode): - cls = pascal(branch.group_name) - branches.append(TreeChoiceBranch( - enum_name=camel(branch.group_name), - class_name=cls, - is_set=branch.max_occurs != 1, - is_group=True, - group_name=branch.group_name, - )) - group_children = model.groups.get(branch.group_name, []) - plan.groups_to_generate.append((cls, group_children)) - elif isinstance(branch, ElementRefNode): - cls = pascal(branch.element_name) - branches.append(TreeChoiceBranch( - enum_name=camel(branch.element_name), - class_name=cls, - is_set=branch.max_occurs != 1, - is_group=False, - xml_name=branch.element_name, - )) - plan.inline_choices.append(InlineChoice( - choice_class=cc_name, branches=branches, - is_set=is_set, is_optional=is_optional, - )) - if choice_index == 0: - plan.choice_class = cc_name - plan.choice_is_set = is_set - plan.choice_is_optional = is_optional - plan.choice_branches = branches - choice_index += 1 - seen_choice = True - elif isinstance(child, GroupRefNode): - if child.group_name in GENERATE_GROUPS: - cls = pascal(child.group_name) + "Group" - else: - cls = pascal(child.group_name) - group_children = model.groups.get(child.group_name, []) - plan.groups_to_generate.append((cls, group_children)) - plan.leading_groups.append((cls, child)) - elif isinstance(child, ElementRefNode): - ref = XsdChildRef( - element_name=child.element_name, - min_occurs=child.min_occurs, - max_occurs=child.max_occurs, - ) - if seen_choice: - plan.trailing_children.append(ref) - else: - plan.leading_children.append(ref) - - elif isinstance(content_tree, ChoiceNode): - plan.choice_class = tree_config.get("choice_class", pascal(elem_name) + "Choice") - for i, branch in enumerate(content_tree.branches): - if isinstance(branch, SequenceNode) and len(branch.children) > 1: - container_name = _derive_container_name( - branch, elem_name, i) - members = _analyze_sequence_members( - branch, model, plan, elem_name) - plan.containers_to_generate.append( - TreeContainer(class_name=container_name, members=members)) - branch_enum_names = tree_config.get("branch_enum_names", {}) - enum_name = branch_enum_names.get( - i, camel(container_name.replace("Sequence", ""))) - trigger_names = _container_trigger_names(branch, model) - plan.choice_branches.append(TreeChoiceBranch( - enum_name=enum_name, - class_name=container_name, - is_set=branch.max_occurs != 1, - is_group=False, - is_container=True, - trigger_names=trigger_names, - )) - elif isinstance(branch, SequenceNode) and len(branch.children) == 1: - child = branch.children[0] - if isinstance(child, ElementRefNode): - cls = pascal(child.element_name) - plan.choice_branches.append(TreeChoiceBranch( - enum_name=camel(child.element_name), - class_name=cls, - is_set=child.max_occurs != 1, - is_group=False, - )) - elif isinstance(child, GroupRefNode): - if child.group_name in GENERATE_GROUPS: - cls = pascal(child.group_name) + "Group" - else: - cls = pascal(child.group_name) - plan.choice_branches.append(TreeChoiceBranch( - enum_name=camel(child.group_name), - class_name=cls, - is_set=child.max_occurs != 1, - is_group=True, - group_name=child.group_name, - )) - elif isinstance(branch, ElementRefNode): - cls = pascal(branch.element_name) - plan.choice_branches.append(TreeChoiceBranch( - enum_name=camel(branch.element_name), - class_name=cls, - is_set=branch.max_occurs != 1, - is_group=False, - )) - elif isinstance(branch, GroupRefNode): - if branch.group_name in GENERATE_GROUPS: - cls = pascal(branch.group_name) + "Group" - else: - cls = pascal(branch.group_name) - plan.choice_branches.append(TreeChoiceBranch( - enum_name=camel(branch.group_name), - class_name=cls, - is_set=branch.max_occurs != 1, - is_group=True, - group_name=branch.group_name, - )) - - return plan if plan.choice_class else None - - -def generate_tree_group_h(class_name: str, children: list, model: XsdModel) -> str: - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - child_classes = [child_class_name(c) for c in children] - for cc in sorted(set(child_classes)): - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - elif child.min_occurs == 0: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if child.min_occurs == 0: - lines.append(f" bool myHas{cc};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_tree_group_cpp(class_name: str, children: list, model: XsdModel) -> str: - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - child_includes = sorted(set( - f'#include "mx/core/elements/{child_class_name(c)}.h"' - for c in children - )) - for inc in child_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - init_parts.append(f"my{cc}Set()") - else: - init_parts.append(f"my{cc}(make{cc}())") - if child.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - parts = [] - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - parts.append(f"my{cc}Set.size() > 0") - elif child.min_occurs == 0: - parts.append(f"myHas{cc}") - else: - parts.append("true") - if any("true" == p for p in parts): - lines.append(" return true;") - elif parts: - lines.append(f" return {' || '.join(parts)};") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" isOneLineOnly = false;") - lines.append(" bool isFirst = true;") - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" }") - else: - lines.append(" if (!isFirst)") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" return os;") - lines.append("}\n") - - for child in children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - elif child.min_occurs == 0: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_container_h(container: TreeContainer) -> str: - cn = container.class_name - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - fwd = [] - for m in container.members: - fwd.append(m.class_name) - for f in sorted(set(fwd)): - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cn})\n") - - lines.append(f"inline {cn}Ptr make{cn}()") - lines.append("{") - lines.append(f" return std::make_shared<{cn}>();") - lines.append("}") - - lines.append(f"\nclass {cn} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {cn}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - for m in container.members: - cc = m.class_name - if m.is_set: - lines.append(f"\n /* _________ {cc} minOccurs = 1, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &setIterator);") - lines.append(f" void clear{cc}Set();") - elif m.is_optional: - lines.append(f"\n /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f"\n /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for m in container.members: - cc = m.class_name - if m.is_set: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if m.is_optional: - lines.append(f" bool myHas{cc};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_container_cpp(container: TreeContainer) -> str: - cn = container.class_name - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{cn}.h"') - lines.append('#include "mx/core/FromXElement.h"') - child_includes = sorted(set( - f'#include "mx/core/elements/{m.class_name}.h"' - for m in container.members - )) - for inc in child_includes: - lines.append(inc) - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - for m in container.members: - cc = m.class_name - if m.is_set: - init_parts.append(f"my{cc}Set()") - else: - init_parts.append(f"my{cc}(make{cc}())") - if m.is_optional: - init_parts.append(f"myHas{cc}(false)") - - _emit_ctor_init(lines, f"{cn}::{cn}()", init_parts) - lines.append("{") - set_inits = [m for m in container.members if m.is_set] - for m in set_inits: - cc = m.class_name - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append("}\n") - - lines.append(f"bool {cn}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{cn}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{cn}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {cn}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{cn}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - first = True - for m in container.members: - cc = m.class_name - if m.is_set: - lines.append(f" for (auto it = my{cc}Set.cbegin(); it != my{cc}Set.cend(); ++it)") - lines.append(" {") - lines.append(f" if (it != my{cc}Set.cbegin())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" (*it)->toStream(os, indentLevel);") - lines.append(" }") - first = False - elif m.is_optional: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - if m.use_stream_contents: - lines.append(f" my{cc}->streamContents(os, indentLevel, isOneLineOnly);") - else: - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" }") - first = False - elif m.use_stream_contents: - if not first: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->streamContents(os, indentLevel, isOneLineOnly);") - first = False - else: - if not first: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - first = False - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - for m in container.members: - cc = m.class_name - if m.is_set: - lines.append(f"const {cc}Set &{cn}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {cn}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {cn}::remove{cc}(const {cc}SetIterConst &setIterator)") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" if (my{cc}Set.size() > 1)") - lines.append(" {") - lines.append(f" my{cc}Set.erase(setIterator);") - lines.append(" }") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {cn}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append("}\n") - elif m.is_optional: - lines.append(f"{cc}Ptr {cn}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {cn}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {cn}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {cn}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {cn}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {cn}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({cn});\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_tree_choice_h(choice_class: str, branches: list, - parent_name: str) -> str: - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - for b in branches: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({b.class_name})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})\n") - - lines.append(f"inline {choice_class}Ptr make{choice_class}()") - lines.append("{") - lines.append(f" return std::make_shared<{choice_class}>();") - lines.append("}") - - lines.append(f"\nclass {choice_class} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, b in enumerate(branches): - comma = "," if i < len(branches) - 1 else "" - lines.append(f" {b.enum_name} = {i}{comma}") - lines.append(" };") - lines.append(f" {choice_class}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" Choice getChoice() const;") - lines.append(f" void setChoice(const Choice value);") - - for b in branches: - if b.is_set: - lines.append(f"\n const {b.class_name}Set &get{b.class_name}Set() const;") - lines.append(f" void remove{b.class_name}(const {b.class_name}SetIterConst &value);") - lines.append(f" void add{b.class_name}(const {b.class_name}Ptr &value);") - lines.append(f" void clear{b.class_name}Set();") - else: - lines.append(f" {b.class_name}Ptr get{b.class_name}() const;") - lines.append(f" void set{b.class_name}(const {b.class_name}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - for b in branches: - if b.is_set: - lines.append(f" {b.class_name}Set my{b.class_name}Set;") - else: - lines.append(f" {b.class_name}Ptr my{b.class_name};") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_tree_choice_cpp(choice_class: str, branches: list, - parent_name: str) -> str: - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{choice_class}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for b in branches: - lines.append(f'#include "mx/core/elements/{b.class_name}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - first_enum = branches[0].enum_name - init_parts = [f"myChoice(Choice::{first_enum})"] - for b in branches: - if b.is_set: - init_parts.append(f"my{b.class_name}Set()") - else: - init_parts.append(f"my{b.class_name}(make{b.class_name}())") - _emit_ctor_init(lines, f"{choice_class}::{choice_class}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - for b in branches: - lines.append(f" if (myChoice == Choice::{b.enum_name})") - lines.append(" {") - if b.is_set: - lines.append(f" for (auto it = my{b.class_name}Set.cbegin(); it != my{b.class_name}Set.cend(); ++it)") - lines.append(" {") - lines.append(f" if (it != my{b.class_name}Set.cbegin())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" (*it)->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - lines.append(f" if (my{b.class_name}Set.size() > 1)") - lines.append(" {") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - else: - if b.is_group: - lines.append(f" if (my{b.class_name})") - lines.append(" {") - lines.append(f" my{b.class_name}->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - elif b.is_container: - lines.append(f" my{b.class_name}->streamContents(os, indentLevel, isOneLineOnly);") - else: - lines.append(f" my{b.class_name}->toStream(os, indentLevel);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"{choice_class}::Choice {choice_class}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - - lines.append(f"void {choice_class}::setChoice(const Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - for b in branches: - if b.is_set: - lines.append(f"const {b.class_name}Set &{choice_class}::get{b.class_name}Set() const") - lines.append("{") - lines.append(f" return my{b.class_name}Set;") - lines.append("}\n") - lines.append(f"void {choice_class}::remove{b.class_name}(const {b.class_name}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{b.class_name}Set.cend())") - lines.append(" {") - lines.append(f" my{b.class_name}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::add{b.class_name}(const {b.class_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{b.class_name}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::clear{b.class_name}Set()") - lines.append("{") - lines.append(f" my{b.class_name}Set.clear();") - lines.append("}\n") - else: - lines.append(f"{b.class_name}Ptr {choice_class}::get{b.class_name}() const") - lines.append("{") - lines.append(f" return my{b.class_name};") - lines.append("}\n") - lines.append(f"void {choice_class}::set{b.class_name}(const {b.class_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{b.class_name} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({choice_class});\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_tree_parent_h(elem_name: str, class_name: str, - choice_class: str, trailing: list, - has_attrs: bool, attrs_name: Optional[str], - model: XsdModel, - choice_is_set: bool = False, - leading_groups: list = None, - leading_children: list = None, - choice_is_optional: bool = False, - inline_choices: list = None, - choice_branches: list = None) -> str: - if leading_groups is None: - leading_groups = [] - if leading_children is None: - leading_children = [] - if inline_choices is None: - inline_choices = [] - multi_choice = len(inline_choices) > 1 - lines = [LICENSE, "#pragma once\n"] - project_includes = ['"mx/core/ElementInterface.h"', '"mx/core/ForwardDeclare.h"'] - if has_attrs and attrs_name: - if attrs_name in CORE_ROOT_ATTRS: - project_includes.append(f'"mx/core/{attrs_name}.h"') - else: - project_includes.append(f'"mx/core/elements/{attrs_name}.h"') - for inc in sorted(project_includes): - lines.append(f"#include {inc}") - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - if multi_choice: - if not any(ic.is_set for ic in inline_choices): - lines.append("namespace ezxml\n{\nclass XElementIterator;\n}") - lines.append("") - elif not choice_is_set: - lines.append("namespace ezxml\n{\nclass XElementIterator;\n}") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - - if has_attrs and attrs_name: - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for lg_cls, lg_ref in leading_groups: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({lg_cls})") - for child in leading_children: - cc = child_class_name(child) - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - if multi_choice: - for ic in inline_choices: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({ic.choice_class})") - else: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})") - for child in trailing: - cc = child_class_name(child) - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({cc})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - - for lg_cls, lg_ref in leading_groups: - lines.append(f"\n /* _________ {lg_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {lg_cls}Ptr get{lg_cls}() const;") - lines.append(f" void set{lg_cls}(const {lg_cls}Ptr &value);") - - for child in leading_children: - cc = child_class_name(child) - max_o = "unbounded" if child.max_occurs == -1 else str(child.max_occurs) - lines.append(f"\n /* _________ {cc} minOccurs = {child.min_occurs}, maxOccurs = {max_o} _________ */") - if child.max_occurs != 1: - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - elif child.min_occurs == 0: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - if ic.is_optional and not ic.is_set: - lines.append(f"\n /* _________ {icc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {icc}Ptr get{icc}() const;") - lines.append(f" void set{icc}(const {icc}Ptr &value);") - lines.append(f" bool getHas{icc}() const;") - lines.append(f" void setHas{icc}(const bool value);") - elif ic.is_set: - lines.append(f"\n /* _________ {icc} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {icc}Set &get{icc}Set() const;") - lines.append(f" void add{icc}(const {icc}Ptr &value);") - lines.append(f" void remove{icc}(const {icc}SetIterConst &value);") - lines.append(f" void clear{icc}Set();") - else: - lines.append(f" {icc}Ptr get{icc}() const;") - lines.append(f" void set{icc}(const {icc}Ptr &value);") - else: - if choice_is_optional and not choice_is_set: - lines.append(f"\n /* _________ {choice_class} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {choice_class}Ptr get{choice_class}() const;") - lines.append(f" void set{choice_class}(const {choice_class}Ptr &value);") - lines.append(f" bool getHas{choice_class}() const;") - lines.append(f" void setHas{choice_class}(const bool value);") - elif choice_is_set: - lines.append(f"\n /* _________ {choice_class} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {choice_class}Set &get{choice_class}Set() const;") - lines.append(f" void add{choice_class}(const {choice_class}Ptr &value);") - lines.append(f" void remove{choice_class}(const {choice_class}SetIterConst &value);") - lines.append(f" void clear{choice_class}Set();") - lines.append(f" {choice_class}Ptr get{choice_class}(const {choice_class}SetIterConst &setIterator) const;") - else: - lines.append(f" {choice_class}Ptr get{choice_class}() const;") - lines.append(f" void set{choice_class}(const {choice_class}Ptr &value);") - - for child in trailing: - cc = child_class_name(child) - max_o = "unbounded" if child.max_occurs == -1 else str(child.max_occurs) - if leading_children: - lines.append(f"\n /* _________ {cc} minOccurs = {child.min_occurs}, maxOccurs = {max_o} _________ */") - if child.max_occurs != 1: - if choice_is_set: - if not leading_children: - lines.append(f"\n /* _________ {cc} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - else: - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void clear{cc}Set();") - elif child.min_occurs == 0: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - else: - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - if has_attrs and attrs_name: - lines.append(f" {attrs_name}Ptr myAttributes;") - for lg_cls, lg_ref in leading_groups: - lines.append(f" {lg_cls}Ptr my{lg_cls};") - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if child.min_occurs == 0: - lines.append(f" bool myHas{cc};") - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - if ic.is_set: - lines.append(f" {icc}Set my{icc}Set;") - else: - lines.append(f" {icc}Ptr my{icc};") - if ic.is_optional: - lines.append(f" bool myHas{icc};") - else: - if choice_is_set: - lines.append(f" {choice_class}Set my{choice_class}Set;") - else: - lines.append(f" {choice_class}Ptr my{choice_class};") - if choice_is_optional: - lines.append(f" bool myHas{choice_class};") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" {cc}Set my{cc}Set;") - else: - lines.append(f" {cc}Ptr my{cc};") - if child.min_occurs == 0: - lines.append(f" bool myHas{cc};") - - # Private import helpers for parent-imported choice groups (Issues E/F). - if not multi_choice and _get_tree_config(elem_name).get( - "parent_imports_choice_groups", False) and choice_branches: - lines.append("") - for b in choice_branches: - if not b.is_group: - continue - lines.append(f" bool import{b.class_name}(std::ostream &message, ::ezxml::XElementIterator &iter,") - lines.append(f" {' ' * len(b.class_name)}::ezxml::XElementIterator &endIter, bool &isSuccess);") - - # Private import helpers for container branches (is_container=True). - if not multi_choice and choice_branches: - container_branches = [b for b in choice_branches if b.is_container and b.trigger_names] - if container_branches: - lines.append("") - for b in container_branches: - fn_name = f"importContainer{b.class_name}" - lines.append(f" bool {fn_name}(std::ostream &message, ::ezxml::XElementIterator &it,") - lines.append(f" {' ' * (len(fn_name) + 6)}::ezxml::XElementIterator &endIter, bool &isSuccess);") - - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_choice_set_add(lines, choice_class, var, seed_choice_set): - """Append a parsed choice `var` to my{choice_class}Set. - - When the set is seeded with a default in the constructor (seed_choice_set), - the first parsed item replaces that default instead of appending after it; - otherwise it is a plain push_back. - """ - set_name = f"my{choice_class}Set" - if seed_choice_set: - lines.append(f" if (!isFirstItemAdded && {set_name}.size() == 1)") - lines.append(" {") - lines.append(f" *{set_name}.begin() = {var};") - lines.append(" isFirstItemAdded = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" {set_name}.push_back({var});") - lines.append(" isFirstItemAdded = true;") - lines.append(" }") - else: - lines.append(f" {set_name}.push_back({var});") - - -def generate_tree_parent_cpp(elem_name: str, class_name: str, - choice_class: str, trailing: list, - has_attrs: bool, attrs_name: Optional[str], - plan: TreeGenPlan, model: XsdModel) -> str: - choice_is_set = plan.choice_is_set - choice_is_optional = plan.choice_is_optional - leading_groups = plan.leading_groups or [] - leading_children = plan.leading_children or [] - inline_choices = plan.inline_choices or [] - multi_choice = len(inline_choices) > 1 - always_has_contents = _get_tree_config(elem_name).get("always_has_contents", False) - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - - all_includes = set() - if multi_choice: - for ic in inline_choices: - all_includes.add(ic.choice_class) - for b in ic.branches: - all_includes.add(b.class_name) - if b.is_group: - group_children = model.groups.get(b.group_name, []) - for gc in group_children: - all_includes.add(child_class_name(gc)) - else: - for b in plan.choice_branches: - all_includes.add(b.class_name) - if b.is_group: - group_children = model.groups.get(b.group_name, []) - for gc in group_children: - all_includes.add(child_class_name(gc)) - if b.is_container: - # Include headers for all members of the container, and for - # nested choice branch classes so the importContainer method - # can reference them. - container = next( - (c for c in plan.containers_to_generate if c.class_name == b.class_name), - None, - ) - if container: - for m in container.members: - all_includes.add(m.class_name) - if m.use_stream_contents and not m.element_name and not m.is_optional: - # Nested choice: add its branch class names too - nc_entry = next( - ( - (nc_class, nc_branches) - for (nc_class, nc_branches, _) in plan.nested_choices_to_generate - if nc_class == m.class_name - ), - None, - ) - if nc_entry: - _, nc_branches = nc_entry - for nb in nc_branches: - all_includes.add(nb.class_name) - if nb.is_group: - gc_list = model.groups.get(nb.group_name, []) - for gc in gc_list: - all_includes.add(child_class_name(gc)) - all_includes.add(choice_class) - for lg_cls, lg_ref in leading_groups: - all_includes.add(lg_cls) - if isinstance(lg_ref, GroupRefNode): - group_children = model.groups.get(lg_ref.group_name, []) - for gc in group_children: - all_includes.add(child_class_name(gc)) - for child in leading_children: - all_includes.add(child_class_name(child)) - for child in trailing: - all_includes.add(child_class_name(child)) - for inc in sorted(all_includes): - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - if has_attrs and attrs_name: - init_parts.append(f"myAttributes(std::make_shared<{attrs_name}>())") - for lg_cls, lg_ref in leading_groups: - init_parts.append(f"my{lg_cls}(make{lg_cls}())") - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - init_parts.append(f"my{cc}Set()") - else: - init_parts.append(f"my{cc}(make{cc}())") - if child.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - if ic.is_set: - init_parts.append(f"my{icc}Set()") - else: - init_parts.append(f"my{icc}(make{icc}())") - if ic.is_optional: - init_parts.append(f"myHas{icc}(false)") - else: - if choice_is_set: - init_parts.append(f"my{choice_class}Set()") - else: - init_parts.append(f"my{choice_class}(std::make_shared<{choice_class}>())") - if choice_is_optional: - init_parts.append(f"myHas{choice_class}(false)") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - init_parts.append(f"my{cc}Set()") - else: - init_parts.append(f"my{cc}(make{cc}())") - if child.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - # If the config requests that the choice set be pre-seeded with one default - # member (matching HEAD's pattern for notehead-text), emit the push_back. - if choice_is_set and _get_tree_config(elem_name).get("seed_choice_set", False): - lines.append(f" my{choice_class}Set.push_back(make{choice_class}());") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->hasValues();") - else: - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - if has_attrs: - lines.append(" return myAttributes->toStream(os);") - else: - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{elem_name}";') - lines.append(" return os;") - lines.append("}\n") - - has_contents_parts = [] - for lg_cls, lg_ref in leading_groups: - has_contents_parts.append(f"my{lg_cls}->hasContents()") - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - has_contents_parts.append(f"my{cc}Set.size() > 0") - elif child.min_occurs == 0: - has_contents_parts.append(f"myHas{cc}") - else: - has_contents_parts.append("true") - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - if ic.is_set: - has_contents_parts.append(f"my{icc}Set.size() > 0") - elif ic.is_optional: - has_contents_parts.append(f"myHas{icc}") - else: - has_contents_parts.append(f"my{icc}->hasContents()") - else: - if choice_is_set: - has_contents_parts.append(f"my{choice_class}Set.size() > 0") - elif choice_is_optional: - has_contents_parts.append(f"myHas{choice_class}") - else: - has_contents_parts.append(f"my{choice_class}->hasContents()") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - has_contents_parts.append(f"my{cc}Set.size() > 0") - elif child.min_occurs == 0: - has_contents_parts.append(f"myHas{cc}") - else: - has_contents_parts.append("true") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - if always_has_contents or "true" in has_contents_parts: - lines.append(" return true;") - else: - lines.append(f" return {' || '.join(has_contents_parts)};") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - if multi_choice: - has_required = any( - (child.min_occurs > 0 and child.max_occurs == 1) - for child in list(leading_children) + list(trailing) - ) or any(not ic.is_optional and not ic.is_set for ic in inline_choices) - if not has_required: - lines.append(" if (hasContents())") - lines.append(" {") - indent = " " - else: - indent = " " - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"{indent}for (auto x : my{cc}Set)") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - elif child.min_occurs == 0: - lines.append(f"{indent}if (myHas{cc})") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{cc}->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - else: - lines.append(f"{indent}os << std::endl;") - lines.append(f"{indent}my{cc}->toStream(os, indentLevel + 1);") - for ic in inline_choices: - icc = ic.choice_class - if ic.is_optional: - lines.append(f"{indent}if (myHas{icc})") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{icc}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent}}}") - elif ic.is_set: - lines.append(f"{indent}for (auto x : my{icc}Set)") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent}}}") - else: - lines.append(f"{indent}if (my{icc}->hasContents())") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{icc}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f"{indent}}}") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"{indent}for (auto x : my{cc}Set)") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} x->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - elif child.min_occurs == 0: - lines.append(f"{indent}if (myHas{cc})") - lines.append(f"{indent}{{") - lines.append(f"{indent} os << std::endl;") - lines.append(f"{indent} my{cc}->toStream(os, indentLevel + 1);") - lines.append(f"{indent}}}") - else: - lines.append(f"{indent}os << std::endl;") - lines.append(f"{indent}my{cc}->toStream(os, indentLevel + 1);") - lines.append(f"{indent}os << std::endl;") - lines.append(f"{indent}isOneLineOnly = false;") - if not has_required: - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - elif choice_is_set: - if leading_groups: - lines.append(" if (hasContents())") - lines.append(" {") - for lg_cls, lg_ref in leading_groups: - lines.append(f" if (my{lg_cls}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{lg_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(f" for (auto x : my{choice_class}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - elif always_has_contents and not trailing: - lines.append(f" if (my{choice_class}Set.size() > 0)") - lines.append(" {") - lines.append(f" for (auto x : my{choice_class}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - else: - for lg_cls, lg_ref in leading_groups: - lines.append(f" if (my{lg_cls}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{lg_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(f" for (auto x : my{choice_class}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - elif leading_children or choice_is_optional: - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" }") - else: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - if choice_is_optional: - lines.append(f" if (myHas{choice_class})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{choice_class}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - else: - lines.append(f" if (my{choice_class}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{choice_class}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" }") - else: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - else: - lines.append(f" if (my{choice_class}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" if (my{choice_class})") - lines.append(" {") - lines.append(f" my{choice_class}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" }") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" for (auto it = my{cc}Set.cbegin(); it != my{cc}Set.cend(); ++it)") - lines.append(" {") - lines.append(f" if (it == my{cc}Set.cbegin())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" (*it)->toStream(os, indentLevel + 1);") - lines.append(" os << std::endl;") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(f" if (my{choice_class}->hasContents())") - lines.append(" {") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f" if (my{cc}Set.size() > 0)") - lines.append(" {") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - - if has_attrs and attrs_name: - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - for lg_cls, lg_ref in leading_groups: - lines.append(f"{lg_cls}Ptr {class_name}::get{lg_cls}() const") - lines.append("{") - lines.append(f" return my{lg_cls};") - lines.append("}\n") - lines.append(f"void {class_name}::set{lg_cls}(const {lg_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{lg_cls} = value;") - lines.append(" }") - lines.append("}\n") - - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - if child.min_occurs == 0: - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - if ic.is_set: - lines.append(f"const {icc}Set &{class_name}::get{icc}Set() const") - lines.append("{") - lines.append(f" return my{icc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{icc}(const {icc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{icc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{icc}(const {icc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{icc}Set.cend())") - lines.append(" {") - lines.append(f" my{icc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{icc}Set()") - lines.append("{") - lines.append(f" my{icc}Set.clear();") - lines.append("}\n") - else: - lines.append(f"{icc}Ptr {class_name}::get{icc}() const") - lines.append("{") - lines.append(f" return my{icc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{icc}(const {icc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{icc} = value;") - lines.append(" }") - lines.append("}\n") - if ic.is_optional: - lines.append(f"bool {class_name}::getHas{icc}() const") - lines.append("{") - lines.append(f" return myHas{icc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{icc}(const bool value)") - lines.append("{") - lines.append(f" myHas{icc} = value;") - lines.append("}\n") - else: - if choice_is_set: - lines.append(f"const {choice_class}Set &{class_name}::get{choice_class}Set() const") - lines.append("{") - lines.append(f" return my{choice_class}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{choice_class}(const {choice_class}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{choice_class}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{choice_class}(const {choice_class}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{choice_class}Set.cend())") - lines.append(" {") - lines.append(f" my{choice_class}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{choice_class}Set()") - lines.append("{") - lines.append(f" my{choice_class}Set.clear();") - if _get_tree_config(elem_name).get("seed_choice_set", False): - lines.append(f" my{choice_class}Set.push_back(make{choice_class}());") - lines.append("}\n") - lines.append(f"{choice_class}Ptr {class_name}::get{choice_class}(const {choice_class}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{choice_class}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {choice_class}Ptr();") - lines.append("}\n") - else: - lines.append(f"{choice_class}Ptr {class_name}::get{choice_class}() const") - lines.append("{") - lines.append(f" return my{choice_class};") - lines.append("}\n") - lines.append(f"void {class_name}::set{choice_class}(const {choice_class}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{choice_class} = value;") - lines.append(" }") - lines.append("}\n") - if choice_is_optional: - lines.append(f"bool {class_name}::getHas{choice_class}() const") - lines.append("{") - lines.append(f" return myHas{choice_class};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{choice_class}(const bool value)") - lines.append("{") - lines.append(f" myHas{choice_class} = value;") - lines.append("}\n") - - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - if choice_is_set: - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - else: - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - elif child.min_occurs == 0: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - else: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - if has_attrs: - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs == 1 and child.min_occurs == 1: - lines.append(f" bool is{cc}Found = false;") - # When the choice set is seeded with a default item in the constructor (so - # hasContents() is true), the first parsed item must replace that default - # rather than append after it -- otherwise the round-trip emits a spurious - # empty leading element. - seed_choice_set = choice_is_set and _get_tree_config(elem_name).get("seed_choice_set", False) - if seed_choice_set: - lines.append(" bool isFirstItemAdded = false;") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - - for child in leading_children: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f' if (importElementSet(message, it, endIter, isSuccess, "{child.element_name}", my{cc}Set))') - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - elif child.min_occurs == 0: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, myHas{cc}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - else: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, is{cc}Found))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - - for lg_cls, lg_ref in leading_groups: - if isinstance(lg_ref, GroupRefNode): - # A leading group ref (e.g. the editorial group: footnote, level) - # is consumed by importGroup, which leaves the iterator pointing at - # the next unconsumed sibling (or, at parent end, the last consumed - # member). It must NOT be guarded by a name check followed by - # `continue`: the `continue` would advance past the element the - # iterator was left on, dropping the first member of whatever - # follows the group (e.g. the articulations after a notations - # editorial group). Emit a bare importGroup, matching the idiom in - # Forward/Barline/Direction, and let the choice/trailing branches - # below re-inspect the current element. importGroup is a no-op when - # the iterator does not point at a group member. - lines.append(f" importGroup(message, it, endIter, isSuccess, my{lg_cls});") - - if multi_choice: - for ic in inline_choices: - icc = ic.choice_class - for b in ic.branches: - if b.is_group: - group_children = model.groups.get(b.group_name, []) - elem_names = [gc.element_name for gc in group_children] - cond = " || ".join(f'it->getName() == "{n}"' for n in elem_names) - lines.append(f" if ({cond})") - lines.append(" {") - lines.append(f" my{icc}->setChoice({icc}::Choice::{b.enum_name});") - lines.append(f" auto groupPtr = my{icc}->get{b.class_name}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, groupPtr);") - if ic.is_optional: - lines.append(f" myHas{icc} = true;") - lines.append(" continue;") - lines.append(" }") - elif b.xml_name: - lines.append(f' if (it->getName() == "{b.xml_name}")') - lines.append(" {") - lines.append(f" my{icc}->setChoice({icc}::Choice::{b.enum_name});") - lines.append(f" isSuccess &= my{icc}->get{b.class_name}()->fromXElement(message, *it);") - if ic.is_optional: - lines.append(f" myHas{icc} = true;") - lines.append(" continue;") - lines.append(" }") - else: - parent_imports_groups = _get_tree_config(elem_name).get( - "parent_imports_choice_groups", False) - for b in plan.choice_branches: - if b.is_group and parent_imports_groups: - # Route group parsing through a private member function on the - # parent class (see Issues E/F). The body is emitted after - # fromXElementImpl below. - lines.append(f" if (import{b.class_name}(message, it, endIter, isSuccess))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - elif b.is_group: - group_children = model.groups.get(b.group_name, []) - elem_names = [gc.element_name for gc in group_children] - cond = " || ".join(f'it->getName() == "{n}"' for n in elem_names) - lines.append(f" if ({cond})") - lines.append(" {") - if choice_is_set: - lines.append(f" auto choice = make{choice_class}();") - lines.append(f" choice->setChoice({choice_class}::Choice::{b.enum_name});") - lines.append(f" auto groupPtr = choice->get{b.class_name}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, groupPtr);") - _emit_choice_set_add(lines, choice_class, "choice", seed_choice_set) - else: - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{b.enum_name});") - if b.is_set: - lines.append(f" auto item = make{b.class_name}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, item);") - lines.append(f" my{choice_class}->add{b.class_name}(item);") - else: - lines.append(f" auto groupPtr = my{choice_class}->get{b.class_name}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, groupPtr);") - lines.append(" continue;") - lines.append(" }") - elif not b.is_group and b.xml_name: - xml_name_kebab = b.xml_name - if choice_is_set: - lines.append(f' if (it->getName() == "{xml_name_kebab}")') - lines.append(" {") - lines.append(f" auto choice = make{choice_class}();") - lines.append(f" choice->setChoice({choice_class}::Choice::{b.enum_name});") - lines.append(f" isSuccess &= choice->get{b.class_name}()->fromXElement(message, *it);") - _emit_choice_set_add(lines, choice_class, "choice", seed_choice_set) - lines.append(" continue;") - lines.append(" }") - else: - lines.append(f' if (it->getName() == "{xml_name_kebab}")') - lines.append(" {") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{b.enum_name});") - lines.append(f" isSuccess &= my{choice_class}->get{b.class_name}()->fromXElement(message, *it);") - if choice_is_optional: - lines.append(f" myHas{choice_class} = true;") - lines.append(" continue;") - lines.append(" }") - elif b.is_container and b.trigger_names: - # Dispatch to a private importContainer method when any of - # the container's trigger element names is seen. The method - # handles parsing all container members in order and leaves - # the iterator pointing at the last consumed element. - cond = " || ".join(f'it->getName() == "{n}"' for n in b.trigger_names) - lines.append(f" if ({cond})") - lines.append(" {") - lines.append(f" if (importContainer{b.class_name}(message, it, endIter, isSuccess))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" }") - - for child in trailing: - cc = child_class_name(child) - if child.max_occurs != 1: - lines.append(f' if (importElementSet(message, it, endIter, isSuccess, "{child.element_name}", my{cc}Set))') - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - elif child.min_occurs == 0: - if leading_children: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, myHas{cc}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - else: - lines.append(f' importElement(message, "{child.element_name}", isSuccess, *it, myHas{cc}, my{cc});') - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - # Emit private member-function bodies for parent-imported choice groups - # (Issues E/F). Routed through here when - # TREE_ELEMENT_CONFIG[elem_name]["parent_imports_choice_groups"] is True. - if not multi_choice and _get_tree_config(elem_name).get( - "parent_imports_choice_groups", False): - for b in plan.choice_branches: - if not b.is_group: - continue - group_children = model.groups.get(b.group_name, []) - elem_names = [gc.element_name for gc in group_children] - mismatch_cond = " && ".join(f'iter->getName() != "{n}"' for n in elem_names) - match_cond = " || ".join(f'iter->getName() == "{n}"' for n in elem_names) - - lines.append(f"bool {class_name}::import{b.class_name}(std::ostream &message, ::ezxml::XElementIterator &iter,") - lines.append(f" {' ' * len(class_name)}::ezxml::XElementIterator &endIter, bool &isSuccess)") - lines.append("{") - lines.append(" if (iter == endIter)") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(f" if ({mismatch_cond})") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(" bool isIterIncremented = false;") - if b.is_set: - lines.append(" bool isFirstItemAdded = false;") - lines.append("") - lines.append(f" while (iter != endIter &&") - lines.append(f" ({match_cond}))") - lines.append(" {") - lines.append(f" auto item = make{b.class_name}();") - lines.append(f" const auto &items = my{choice_class}->get{b.class_name}Set();") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{b.enum_name});") - lines.append("") - for gc in group_children: - gcc = child_class_name(gc) - lines.append(f' if (iter != endIter && iter->getName() == "{gc.element_name}")') - lines.append(" {") - if gc.min_occurs == 0: - lines.append(f" item->setHas{gcc}(true);") - lines.append(f" isSuccess &= item->get{gcc}()->fromXElement(message, *iter);") - lines.append(" isIterIncremented = true;") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" if (!isFirstItemAdded && items.size() == 1)") - lines.append(" {") - lines.append(f" my{choice_class}->add{b.class_name}(item);") - lines.append(f" my{choice_class}->remove{b.class_name}(items.cbegin());") - lines.append(" isFirstItemAdded = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{choice_class}->add{b.class_name}(item);") - lines.append(" isFirstItemAdded = true;") - lines.append(" }") - lines.append(" }") - lines.append("") - else: - lines.append("") - for gc in group_children: - gcc = child_class_name(gc) - lines.append(f' if (iter != endIter && iter->getName() == "{gc.element_name}")') - lines.append(" {") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{b.enum_name});") - if gc.min_occurs == 0: - lines.append(f" my{choice_class}->get{b.class_name}()->setHas{gcc}(true);") - lines.append(f" isSuccess &= my{choice_class}->get{b.class_name}()->get{gcc}()->fromXElement(message, *iter);") - lines.append(" isIterIncremented = true;") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" if (isIterIncremented)") - lines.append(" {") - lines.append(" --iter;") - lines.append(" }") - lines.append("") - lines.append(" return true;") - lines.append("}\n") - - # Emit private member-function bodies for container branches (is_container=True). - # Each container branch gets an importContainer method that parses the - # container's members in order from the shared iterator. - if not multi_choice: - # Build a lookup: nested choice class_name -> list of TreeChoiceBranch - nested_choice_map = {} - for (nc_class, nc_branches, _nc_parent) in plan.nested_choices_to_generate: - nested_choice_map[nc_class] = nc_branches - - for b in plan.choice_branches: - if not b.is_container or not b.trigger_names: - continue - - # Find the container definition - container = next( - (c for c in plan.containers_to_generate if c.class_name == b.class_name), - None, - ) - if container is None: - continue - - mismatch_cond = " && ".join(f'it->getName() != "{n}"' for n in b.trigger_names) - fn_name = f"importContainer{b.class_name}" - indent = " " * (len(class_name) + len(fn_name) + 7) - lines.append(f"bool {class_name}::{fn_name}(std::ostream &message, ::ezxml::XElementIterator &it,") - lines.append(f" {indent}::ezxml::XElementIterator &endIter, bool &isSuccess)") - lines.append("{") - lines.append(" if (it == endIter)") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(f" if ({mismatch_cond})") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{b.enum_name});") - lines.append(f" auto containerPtr = my{choice_class}->get{b.class_name}();") - lines.append(" bool isIterIncremented = false;") - lines.append("") - - for m in container.members: - cc = m.class_name - if m.is_set and m.element_name: - # Repeating element - use a while loop - lines.append(f' while (it != endIter && it->getName() == "{m.element_name}")') - lines.append(" {") - lines.append(f" auto item = make{cc}();") - lines.append(f" isSuccess &= item->fromXElement(message, *it);") - lines.append(f" const auto &items = containerPtr->get{cc}Set();") - lines.append(" if (items.size() == 1 && !isIterIncremented)") - lines.append(" {") - lines.append(f" containerPtr->add{cc}(item);") - lines.append(f" containerPtr->remove{cc}(items.cbegin());") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" containerPtr->add{cc}(item);") - lines.append(" }") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - elif m.is_optional and m.use_stream_contents and not m.element_name: - # Optional group (e.g. MetronomeRelationGroup): call importGroup - lines.append(f" bool has{cc} = false;") - lines.append(f" auto {camel(cc)} = containerPtr->get{cc}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, {camel(cc)}, has{cc});") - lines.append(f" if (has{cc})") - lines.append(" {") - lines.append(f" containerPtr->setHas{cc}(true);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - elif m.use_stream_contents and not m.element_name and not m.is_optional: - if cc in nested_choice_map: - # Nested choice: dispatch each branch - nc_branches = nested_choice_map[cc] - lines.append(f" auto {camel(cc)} = containerPtr->get{cc}();") - first_branch = True - for nb in nc_branches: - kw = "if" if first_branch else "else if" - if nb.is_group: - group_children = model.groups.get(nb.group_name, []) - elem_names = [gc.element_name for gc in group_children - if hasattr(gc, "element_name") and gc.element_name] - if not elem_names: - continue - ncond = " || ".join(f'it->getName() == "{n}"' for n in elem_names) - lines.append(f" {kw} (it != endIter && ({ncond}))") - lines.append(" {") - lines.append(f" {camel(cc)}->setChoice({cc}::Choice::{nb.enum_name});") - lines.append(f" auto groupPtr = {camel(cc)}->get{nb.class_name}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, groupPtr);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - first_branch = False - elif nb.xml_name: - lines.append(f' {kw} (it != endIter && it->getName() == "{nb.xml_name}")') - lines.append(" {") - lines.append(f" {camel(cc)}->setChoice({cc}::Choice::{nb.enum_name});") - lines.append(f" isSuccess &= {camel(cc)}->get{nb.class_name}()->fromXElement(message, *it);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - first_branch = False - lines.append("") - else: - # Plain group (use importGroup) - lines.append(f" if (it != endIter)") - lines.append(" {") - lines.append(f" auto groupPtr = containerPtr->get{cc}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, groupPtr);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - elif not m.is_optional and not m.is_set and m.element_name: - # Required single element - lines.append(f' if (it != endIter && it->getName() == "{m.element_name}")') - lines.append(" {") - lines.append(f" isSuccess &= containerPtr->get{cc}()->fromXElement(message, *it);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - elif m.is_optional and m.element_name: - # Optional single element - lines.append(f' if (it != endIter && it->getName() == "{m.element_name}")') - lines.append(" {") - lines.append(f" containerPtr->setHas{cc}(true);") - lines.append(f" isSuccess &= containerPtr->get{cc}()->fromXElement(message, *it);") - lines.append(" isIterIncremented = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - - lines.append(" if (isIterIncremented)") - lines.append(" {") - lines.append(" --it;") - lines.append(" }") - lines.append("") - lines.append(" return true;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Bespoke generator: credit -# --------------------------------------------------------------------------- -# -# The credit element has a structure not produced by any other element: -# -# sequence -# credit-type* (leading optional-unbounded) -# link* (leading optional-unbounded) -# bookmark* (leading optional-unbounded) -# choice -# credit-image -- single element branch -# sequence -# credit-words -- always-present singleton -# sequence min=0 max=unbounded -# link* -# bookmark* -# credit-words -- always-present singleton inside the inner pair -# -# The original codegen promoted: -# - the outer choice to CreditChoice (enum branches = creditImage, -# creditWords; the creditWords branch encompasses both the singleton -# credit-words and a CreditWordsGroupSet for the inner repeating sequence). -# - the inner unbounded sequence to CreditWordsGroup (one instance per -# iteration; holds LinkSet, BookmarkSet, and a single CreditWordsPtr). -# -# Names below are derived from the parsed XSD content_tree so that spec -# changes (added/renamed attributes, additional leading children, additional -# group members) propagate automatically. - - -def _extract_credit_structure(ct): - """Walk credit's content_tree and return (leading, single_branch, - pair_first, group_children) where: - - leading: list[ElementRefNode] preceding the choice - - single_branch: ElementRefNode (the credit-image-style branch) - - pair_first: ElementRefNode (the credit-words singleton) - - group_children: list[ElementRefNode] inside the unbounded inner seq - """ - tree = ct.content_tree - assert isinstance(tree, SequenceNode) - leading = [] - choice_node = None - for c in tree.children: - if isinstance(c, ChoiceNode): - choice_node = c - break - if isinstance(c, ElementRefNode): - leading.append(c) - assert choice_node is not None, "credit: expected a choice in content_tree" - - single_branch = None - pair_first = None - group_children = [] - for b in choice_node.branches: - if isinstance(b, ElementRefNode): - single_branch = b - elif isinstance(b, SequenceNode): - # pair branch: first child is the singleton, second is the inner unbounded seq - pair_first = b.children[0] - inner = b.children[1] - assert isinstance(inner, SequenceNode) - group_children = [c for c in inner.children if isinstance(c, ElementRefNode)] - assert single_branch is not None and pair_first is not None - return leading, single_branch, pair_first, group_children - - -def generate_credit_choice_h(choice_class, single_branch, pair_first, group_children): - single_cls = pascal(single_branch.element_name) # CreditImage - pair_cls = pascal(pair_first.element_name) # CreditWords - group_cls = pascal(pair_first.element_name) + "Group" # CreditWordsGroup - - # Forward-declare order matches the original codegen: branch[0] class, - # the synthetic group class for the pair branch, then the pair branch's - # singleton class. - fwds = [single_cls, group_cls, pair_cls] - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})\n") - lines.append(f"inline {choice_class}Ptr make{choice_class}()") - lines.append("{") - lines.append(f" return std::make_shared<{choice_class}>();") - lines.append("}") - lines.append(f"\nclass {choice_class} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - lines.append(f" {camel(single_branch.element_name)} = 1,") - lines.append(f" {camel(pair_first.element_name)} = 2") - lines.append(" };") - lines.append(f" {choice_class}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(" /* _________ Choice minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {choice_class}::Choice getChoice() const;") - lines.append(f" void setChoice(const {choice_class}::Choice value);") - lines.append("") - lines.append(f" /* _________ {single_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {single_cls}Ptr get{single_cls}() const;") - lines.append(f" void set{single_cls}(const {single_cls}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {pair_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {pair_cls}Ptr get{pair_cls}() const;") - lines.append(f" void set{pair_cls}(const {pair_cls}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {group_cls} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {group_cls}Set &get{group_cls}Set() const;") - lines.append(f" void add{group_cls}(const {group_cls}Ptr &value);") - lines.append(f" void remove{group_cls}(const {group_cls}SetIterConst &value);") - lines.append(f" void clear{group_cls}Set();") - lines.append(f" {group_cls}Ptr get{group_cls}(const {group_cls}SetIterConst &setIterator) const;") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - lines.append(f" {single_cls}Ptr my{single_cls};") - lines.append(f" {pair_cls}Ptr my{pair_cls};") - lines.append(f" {group_cls}Set my{group_cls}Set;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_credit_choice_cpp(choice_class, single_branch, pair_first, group_children): - single_cls = pascal(single_branch.element_name) # CreditImage - pair_cls = pascal(pair_first.element_name) # CreditWords - group_cls = pascal(pair_first.element_name) + "Group" # CreditWordsGroup - - # Includes match the original: union of all element classes referenced by - # the choice + its group (incl. transitive Link/Bookmark from the group). - inc_classes = {single_cls, pair_cls, group_cls} - for gc in group_children: - inc_classes.add(pascal(gc.element_name)) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{choice_class}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in sorted(inc_classes): - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - # Default branch = the pair branch (creditWords), matching original codegen. - init_parts = [ - f"myChoice(Choice::{camel(pair_first.element_name)})", - f"my{single_cls}(make{single_cls}())", - f"my{pair_cls}(make{pair_cls}())", - f"my{group_cls}Set()", - ] - _emit_ctor_init(lines, f"{choice_class}::{choice_class}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{choice_class}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - lines.append(f" case Choice::{camel(single_branch.element_name)}:") - lines.append(f" my{single_cls}->toStream(os, indentLevel);") - lines.append(" break;") - lines.append(f" case Choice::{camel(pair_first.element_name)}: {{") - lines.append(f" my{pair_cls}->toStream(os, indentLevel);") - lines.append("") - lines.append(f" if (my{group_cls}Set.size() > 0)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append("") - lines.append(f" for (auto x : my{group_cls}Set)") - lines.append(" {") - lines.append(" x->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - lines.append("") - lines.append(" break;") - lines.append(" }") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"{choice_class}::Choice {choice_class}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - - lines.append(f"void {choice_class}::setChoice(const {choice_class}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - for cls in (single_cls, pair_cls): - lines.append(f"{cls}Ptr {choice_class}::get{cls}() const") - lines.append("{") - lines.append(f" return my{cls};") - lines.append("}\n") - lines.append(f"void {choice_class}::set{cls}(const {cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cls} = value;") - lines.append(" }") - lines.append("}\n") - - # group set accessors: order is getSet, remove, add, clear, get(setIterator) - lines.append(f"const {group_cls}Set &{choice_class}::get{group_cls}Set() const") - lines.append("{") - lines.append(f" return my{group_cls}Set;") - lines.append("}\n") - lines.append(f"void {choice_class}::remove{group_cls}(const {group_cls}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{group_cls}Set.cend())") - lines.append(" {") - lines.append(f" my{group_cls}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::add{group_cls}(const {group_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{group_cls}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {choice_class}::clear{group_cls}Set()") - lines.append("{") - lines.append(f" my{group_cls}Set.clear();") - lines.append("}\n") - lines.append(f"{group_cls}Ptr {choice_class}::get{group_cls}(const {group_cls}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{group_cls}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {group_cls}Ptr();") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({choice_class});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_credit_words_group_h(group_cls, group_children, singleton_name): - # group_children includes link*, bookmark*, credit-words(single) - # singleton_name is 'credit-words' (the always-present last member) - set_members = [c for c in group_children if c.element_name != singleton_name] - singleton_cls = pascal(singleton_name) - - fwds = sorted({pascal(c.element_name) for c in group_children}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({group_cls})\n") - lines.append(f"inline {group_cls}Ptr make{group_cls}()") - lines.append("{") - lines.append(f" return std::make_shared<{group_cls}>();") - lines.append("}") - lines.append(f"\nclass {group_cls} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {group_cls}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - # The committed CreditWordsGroup.h has a small inconsistency: its first - # set's comment header is "LinkSet" but the second is "Bookmark". The - # general project convention (matching other group headers like - # Appearance, Barline) uses {cc} without the Set suffix; we use that - # convention. The single residual line is an EXC documented in state.md. - for c in set_members: - cc = pascal(c.element_name) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - lines.append("") - lines.append(f" /* _________ {singleton_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {singleton_cls}Ptr get{singleton_cls}() const;") - lines.append(f" void set{singleton_cls}(const {singleton_cls}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for c in set_members: - cc = pascal(c.element_name) - lines.append(f" {cc}Set my{cc}Set;") - lines.append(f" {singleton_cls}Ptr my{singleton_cls};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_credit_words_group_cpp(group_cls, group_children, singleton_name): - set_members = [c for c in group_children if c.element_name != singleton_name] - singleton_cls = pascal(singleton_name) - inc_classes = sorted({pascal(c.element_name) for c in group_children}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{group_cls}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [f"my{pascal(c.element_name)}Set()" for c in set_members] - init_parts.append(f"my{singleton_cls}(make{singleton_cls}())") - _emit_ctor_init(lines, f"{group_cls}::{group_cls}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {group_cls}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {group_cls}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - # streamContents: bespoke isFirst pattern across sets, then singleton at the end - lines.append(f"std::ostream &{group_cls}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" bool isFirst = true;") - for c in set_members: - cc = pascal(c.element_name) - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" if (!isFirst)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(" x->toStream(os, indentLevel);") - lines.append(" isFirst = false;") - lines.append(" }") - lines.append(" if (!isFirst)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(f" my{singleton_cls}->toStream(os, indentLevel);") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - # Per-set accessors: order is getSet, add, remove, clear, get(setIterator). - # This matches the committed CreditWordsGroup.cpp (add-before-remove), an - # outlier vs the rest of the codebase. See iter-33 gotcha for the analogous - # MidiDeviceInstrumentGroupSet outlier. - for c in set_members: - cc = pascal(c.element_name) - lines.append(f"const {cc}Set &{group_cls}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {group_cls}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {group_cls}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {group_cls}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {group_cls}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - - lines.append(f"{singleton_cls}Ptr {group_cls}::get{singleton_cls}() const") - lines.append("{") - lines.append(f" return my{singleton_cls};") - lines.append("}\n") - lines.append(f"void {group_cls}::set{singleton_cls}(const {singleton_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{singleton_cls} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({group_cls});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_credit_h(class_name, leading, choice_class, attrs_name): - fwds = sorted({pascal(c.element_name) for c in leading} | {choice_class}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace ezxml\n{\nclass XElementIterator;\n}") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - for c in leading: - cc = pascal(c.element_name) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = {c.min_occurs}, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - lines.append("") - lines.append(f" /* _________ {choice_class} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {choice_class}Ptr get{choice_class}() const;") - lines.append(f" void set{choice_class}(const {choice_class}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - for c in leading: - cc = pascal(c.element_name) - lines.append(f" {cc}Set my{cc}Set;") - lines.append(f" {choice_class}Ptr my{choice_class};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_credit_cpp(elem_name, class_name, leading, single_branch, pair_first, - group_children, choice_class, group_cls, attrs_name): - single_cls = pascal(single_branch.element_name) - pair_cls = pascal(pair_first.element_name) - singleton_name = pair_first.element_name # credit-words - - # Includes: leading children + single_branch + pair_first + group_cls + choice_class - inc_classes = sorted({pascal(c.element_name) for c in leading} - | {single_cls, pair_cls, group_cls, choice_class}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [f"myAttributes(std::make_shared<{attrs_name}>())"] - for c in leading: - cc = pascal(c.element_name) - init_parts.append(f"my{cc}Set()") - init_parts.append(f"my{choice_class}(make{choice_class}())") - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return myAttributes->toStream(os);") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{elem_name}";') - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - for c in leading: - cc = pascal(c.element_name) - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(f" my{choice_class}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - # Attributes accessors - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - # Set accessors for leading children: getSet, remove, add, clear, get - for c in leading: - cc = pascal(c.element_name) - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - - # Choice accessors - lines.append(f"{choice_class}Ptr {class_name}::get{choice_class}() const") - lines.append("{") - lines.append(f" return my{choice_class};") - lines.append("}\n") - lines.append(f"void {class_name}::set{choice_class}(const {choice_class}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{choice_class} = value;") - lines.append(" }") - lines.append("}\n") - - # MX_CREDIT_RETURN_IF_END macro and fromXElementImpl - # All names here are derived from the parsed children so spec changes - # (e.g. a renamed choice element) will propagate. - single_name = single_branch.element_name # e.g. credit-image - macro_name = "MX_CREDIT_RETURN_IF_END" - found_flag = f"is{single_cls}Or{pair_cls}Found" - err_msg = (f"\"{class_name}: neither '{singleton_name}' nor '{single_name}' was present" - f" - one of these is required\"") - lines.append(f"#ifndef {macro_name}") - lines.append(f"#define {macro_name} \\") - lines.append(" if (i == endIter) \\") - lines.append(" { \\") - lines.append(f" if (!{found_flag}) \\") - lines.append(" { \\") - lines.append(f" message << {err_msg} \\") - lines.append(" << std::endl; \\") - lines.append(" isSuccess = false; \\") - lines.append(" } \\") - lines.append(" return isSuccess; \\") - lines.append(" }") - lines.append("#endif") - lines.append("") - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append(f" bool {found_flag} = false;") - lines.append("") - lines.append(" ::ezxml::XElementIterator i = xelement.begin();") - lines.append(" ::ezxml::XElementIterator endIter = xelement.end();") - lines.append(f" {macro_name};") - lines.append("") - for c in leading: - cc = pascal(c.element_name) - cname = c.element_name - lines.append(f' if (i->getName() == "{cname}")') - lines.append(" {") - lines.append(f' while (i != endIter && i->getName() == "{cname}")') - lines.append(" {") - lines.append(f" auto item = make{cc}();") - lines.append(" isSuccess &= item->fromXElement(message, *i);") - lines.append(f" add{cc}(item);") - lines.append(" ++i;") - lines.append(" }") - lines.append(" }") - lines.append(f" {macro_name};") - lines.append("") - lines.append(f' if (i->getName() == "{single_name}" || i->getName() == "{singleton_name}")') - lines.append(" {") - lines.append(f" {found_flag} = true;") - lines.append(f' if (i->getName() == "{single_name}")') - lines.append(" {") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{camel(single_name)});") - lines.append(f" isSuccess &= my{choice_class}->get{single_cls}()->fromXElement(message, *i);") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - lines.append("") - lines.append(f' if (i->getName() == "{singleton_name}")') - lines.append(" {") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{camel(singleton_name)});") - lines.append(f" isSuccess &= my{choice_class}->get{pair_cls}()->fromXElement(message, *i);") - lines.append(" ++i;") - lines.append(f" {macro_name};") - lines.append("") - set_members = [c for c in group_children if c.element_name != singleton_name] - set_names_or = " || ".join(f'(i->getName() == "{c.element_name}")' for c in set_members) - set_names_or += f' || (i->getName() == "{singleton_name}")' - lines.append(f" auto creditWordsGroup = make{group_cls}();") - lines.append(f" while (i != endIter &&") - lines.append(f" ({set_names_or}))") - lines.append(" {") - for c in set_members: - cc = pascal(c.element_name) - cname = c.element_name - local = camel(c.element_name) - lines.append(f' while (i != endIter && i->getName() == "{cname}")') - lines.append(" {") - lines.append(f" auto {local} = make{cc}();") - lines.append(f" isSuccess &= {local}->fromXElement(message, xelement);") - lines.append(f" creditWordsGroup->add{cc}({local});") - lines.append(" ++i;") - lines.append(" }") - lines.append(f" {macro_name};") - lines.append("") - lines.append(f' if (i->getName() == "{singleton_name}")') - lines.append(" {") - lines.append(f" isSuccess &= creditWordsGroup->get{pair_cls}()->fromXElement(message, *i);") - lines.append(f" my{choice_class}->add{group_cls}(creditWordsGroup);") - lines.append(f" creditWordsGroup = make{group_cls}();") - lines.append(" ++i;") - lines.append(" }") - lines.append(f" {macro_name};") - lines.append(" }") - lines.append(" }") - lines.append(" }") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_credit_family(elem_name, elem, ct, model, generated_attrs, stats): - leading, single_branch, pair_first, group_children = _extract_credit_structure(ct) - - class_name = element_class_name(elem_name) - choice_class = pascal(elem_name) + "Choice" # CreditChoice - group_cls = pascal(pair_first.element_name) + "Group" # CreditWordsGroup - type_name = elem.type_name or "" - - # 1. Attrs struct via the standard generator - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - else: - attrs_name = None - - # 2. CreditChoice - h = generate_credit_choice_h(choice_class, single_branch, pair_first, group_children) - c = generate_credit_choice_cpp(choice_class, single_branch, pair_first, group_children) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.cpp"), c) - - # 3. CreditWordsGroup - h = generate_credit_words_group_h(group_cls, group_children, pair_first.element_name) - c = generate_credit_words_group_cpp(group_cls, group_children, pair_first.element_name) - write_file(os.path.join(ELEM_DIR, f"{group_cls}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{group_cls}.cpp"), c) - - # 4. Credit - h = generate_credit_h(class_name, leading, choice_class, attrs_name) - c = generate_credit_cpp(elem_name, class_name, leading, single_branch, pair_first, - group_children, choice_class, group_cls, attrs_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -# --------------------------------------------------------------------------- -# Lyric (bespoke, schema-driven) -# -# Lyric's complex type is structured as: -# sequence -# choice: -# sequence: syllabic? text (sequence)*[unbounded] extend? -# inner sequence: (sequence?: elision syllabic?) text -# extend -# laughing -# humming -# end-line? -# end-paragraph? -# group ref editorial -# -# This produces four hand-written-shaped classes in the original codegen: -# - LyricTextChoice (the outer required choice) -# - SyllabicTextGroup (the sequence branch promoted to a group) -# - ElisionSyllabicTextGroup(the unbounded inner sequence promoted to a group) -# - ElisionSyllabicGroup (the optional inner sub-sequence promoted to a group) -# -# All names that come from the XSD (element refs, attribute refs, etc.) are -# read from the parsed model so that spec changes propagate automatically. -# The class names of the synthetic groups are derived from the names of the -# first two children of each synthetic group plus a "Group" suffix. - - -def _synth_group_name_from_children(children): - """Compose a synthetic group name from the first two children, then - append 'Group'. For an ElementRefNode child the stem is - pascal(element_name); for an already-named synthetic group child whose - name ends with 'Group' the stem is the name with 'Group' stripped. - """ - stems = [] - for c in children[:2]: - if isinstance(c, ElementRefNode): - stems.append(pascal(c.element_name)) - elif isinstance(c, str): - stems.append(c[:-5] if c.endswith("Group") else c) - else: - raise AssertionError(f"unexpected child for group naming: {c!r}") - return "".join(stems) + "Group" - - -def _extract_lyric_structure(ct): - """Walk lyric's content_tree and return a dict describing the structure. - - Returns a dict with these keys (all element references are - ElementRefNode pulled directly from the parsed XSD): - - choice: ChoiceNode (the required outer choice) - - seq_branch_pre: elements in the sequence branch before the - inner unbounded sequence (e.g. syllabic?, text) - - seq_branch_post: elements after the inner unbounded sequence - (e.g. extend?) - - inner_seq: the unbounded SequenceNode inside the seq branch - - inner_text: ElementRefNode for the text inside inner_seq - - inner_optional_seq: the optional SequenceNode inside inner_seq - - inner_optional_elems: elements inside the optional sub-sequence - (e.g. elision, syllabic?) - - singleton_branches: other branches of the choice (extend, laughing, - humming) as a list of ElementRefNode - - trailing_elems: ElementRefNodes after the choice (end-line?, - end-paragraph?) - - editorial_group: GroupRefNode for the trailing group ref - """ - tree = ct.content_tree - assert isinstance(tree, SequenceNode) - - choice = None - trailing_elems = [] - editorial_group = None - for c in tree.children: - if isinstance(c, ChoiceNode): - assert choice is None - choice = c - elif isinstance(c, ElementRefNode): - trailing_elems.append(c) - elif isinstance(c, GroupRefNode): - editorial_group = c - else: - raise AssertionError(f"unexpected top-level child: {type(c).__name__}") - assert choice is not None - - seq_branch = None - singleton_branches = [] - for b in choice.branches: - if isinstance(b, SequenceNode): - assert seq_branch is None - seq_branch = b - elif isinstance(b, ElementRefNode): - singleton_branches.append(b) - else: - raise AssertionError(f"unexpected choice branch: {type(b).__name__}") - assert seq_branch is not None - - seq_branch_pre = [] - inner_seq = None - seq_branch_post = [] - for c in seq_branch.children: - if isinstance(c, SequenceNode): - assert inner_seq is None - inner_seq = c - elif isinstance(c, ElementRefNode): - if inner_seq is None: - seq_branch_pre.append(c) - else: - seq_branch_post.append(c) - else: - raise AssertionError(f"unexpected seq branch child: {type(c).__name__}") - assert inner_seq is not None - - inner_optional_seq = None - inner_text = None - for c in inner_seq.children: - if isinstance(c, SequenceNode): - assert inner_optional_seq is None - inner_optional_seq = c - elif isinstance(c, ElementRefNode): - assert inner_text is None - inner_text = c - else: - raise AssertionError(f"unexpected inner seq child: {type(c).__name__}") - assert inner_optional_seq is not None and inner_text is not None - - inner_optional_elems = [] - for c in inner_optional_seq.children: - assert isinstance(c, ElementRefNode) - inner_optional_elems.append(c) - - return { - "choice": choice, - "seq_branch_pre": seq_branch_pre, - "seq_branch_post": seq_branch_post, - "inner_seq": inner_seq, - "inner_text": inner_text, - "inner_optional_seq": inner_optional_seq, - "inner_optional_elems": inner_optional_elems, - "singleton_branches": singleton_branches, - "trailing_elems": trailing_elems, - "editorial_group": editorial_group, - } - - -def generate_elision_syllabic_group_h(group_cls, elems): - """elems = [required_elision, optional_syllabic]""" - fwds = sorted({pascal(e.element_name) for e in elems}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({group_cls})\n") - lines.append(f"inline {group_cls}Ptr make{group_cls}()") - lines.append("{") - lines.append(f" return std::make_shared<{group_cls}>();") - lines.append("}") - lines.append(f"\nclass {group_cls} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {group_cls}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - for e in elems: - cc = pascal(e.element_name) - optional = e.min_occurs == 0 - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = {e.min_occurs}, maxOccurs = {e.max_occurs} _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - if optional: - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for e in elems: - cc = pascal(e.element_name) - optional = e.min_occurs == 0 - lines.append(f" {cc}Ptr my{cc};") - if optional: - lines.append(f" bool myHas{cc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_elision_syllabic_group_cpp(group_cls, elems): - inc_classes = sorted({pascal(e.element_name) for e in elems}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{group_cls}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - for e in elems: - cc = pascal(e.element_name) - init_parts.append(f"my{cc}(make{cc}())") - if e.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - _emit_ctor_init(lines, f"{group_cls}::{group_cls}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {group_cls}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {group_cls}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - # streamContents: required elements emitted plainly; optional elements - # emitted only when their HasX flag is true, and each optional emission - # flips isOneLineOnly to false and emits a leading newline. - lines.append(f"std::ostream &{group_cls}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" isOneLineOnly = true;") - for e in elems: - cc = pascal(e.element_name) - if e.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" }") - else: - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" return os;") - lines.append("}\n") - - for e in elems: - cc = pascal(e.element_name) - lines.append(f"{cc}Ptr {group_cls}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {group_cls}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - if e.min_occurs == 0: - lines.append(f"bool {group_cls}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {group_cls}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({group_cls});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_elision_syllabic_text_group_h(group_cls, inner_group_cls, text_elem): - """ElisionSyllabicTextGroup has optional inner group + required text.""" - text_cls = pascal(text_elem.element_name) - fwds = sorted({inner_group_cls, text_cls}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({group_cls})\n") - lines.append(f"inline {group_cls}Ptr make{group_cls}()") - lines.append("{") - lines.append(f" return std::make_shared<{group_cls}>();") - lines.append("}") - lines.append(f"\nclass {group_cls} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {group_cls}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(f" /* _________ {inner_group_cls} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {inner_group_cls}Ptr get{inner_group_cls}() const;") - lines.append(f" void set{inner_group_cls}(const {inner_group_cls}Ptr &value);") - lines.append(f" bool getHas{inner_group_cls}() const;") - lines.append(f" void setHas{inner_group_cls}(const bool value);") - lines.append("") - lines.append(f" /* _________ {text_cls} minOccurs = {text_elem.min_occurs}, maxOccurs = {text_elem.max_occurs} _________ */") - lines.append(f" {text_cls}Ptr get{text_cls}() const;") - lines.append(f" void set{text_cls}(const {text_cls}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {inner_group_cls}Ptr my{inner_group_cls};") - lines.append(f" bool myHas{inner_group_cls};") - lines.append(f" {text_cls}Ptr my{text_cls};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_elision_syllabic_text_group_cpp(group_cls, inner_group_cls, text_elem): - text_cls = pascal(text_elem.element_name) - inc_classes = sorted({inner_group_cls, text_cls}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{group_cls}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [ - f"my{inner_group_cls}(make{inner_group_cls}())", - f"myHas{inner_group_cls}(false)", - f"my{text_cls}(make{text_cls}())", - ] - _emit_ctor_init(lines, f"{group_cls}::{group_cls}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {group_cls}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {group_cls}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - # streamContents: optional inner group, then required text. - # The committed file streams the inner group via streamContents (not - # toStream) because the inner group represents a flattened sub-sequence, - # not a self-contained XML element. - lines.append(f"std::ostream &{group_cls}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" isOneLineOnly = true;") - lines.append(f" if (myHas{inner_group_cls})") - lines.append(" {") - lines.append(f" my{inner_group_cls}->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(f" my{text_cls}->toStream(os, indentLevel);") - lines.append(" return os;") - lines.append("}\n") - - # Accessors: inner group (optional), then text (required) - lines.append(f"{inner_group_cls}Ptr {group_cls}::get{inner_group_cls}() const") - lines.append("{") - lines.append(f" return my{inner_group_cls};") - lines.append("}\n") - lines.append(f"void {group_cls}::set{inner_group_cls}(const {inner_group_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{inner_group_cls} = value;") - lines.append(" }") - lines.append("}\n") - lines.append(f"bool {group_cls}::getHas{inner_group_cls}() const") - lines.append("{") - lines.append(f" return myHas{inner_group_cls};") - lines.append("}\n") - lines.append(f"void {group_cls}::setHas{inner_group_cls}(const bool value)") - lines.append("{") - lines.append(f" myHas{inner_group_cls} = value;") - lines.append("}\n") - lines.append(f"{text_cls}Ptr {group_cls}::get{text_cls}() const") - lines.append("{") - lines.append(f" return my{text_cls};") - lines.append("}\n") - lines.append(f"void {group_cls}::set{text_cls}(const {text_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{text_cls} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({group_cls});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_syllabic_text_group_h(group_cls, pre_elems, inner_group_cls, - inner_set_name, post_elems): - """SyllabicTextGroup. pre_elems = [syllabic?, text]; inner_group_cls = - 'ElisionSyllabicTextGroup' (used as a set); post_elems = [extend?].""" - elem_classes = {pascal(e.element_name) for e in pre_elems + post_elems} - fwds = sorted(elem_classes | {inner_group_cls}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({group_cls})\n") - lines.append(f"inline {group_cls}Ptr make{group_cls}()") - lines.append("{") - lines.append(f" return std::make_shared<{group_cls}>();") - lines.append("}") - lines.append(f"\nclass {group_cls} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {group_cls}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - - for e in pre_elems: - cc = pascal(e.element_name) - optional = e.min_occurs == 0 - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = {e.min_occurs}, maxOccurs = {e.max_occurs} _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - if optional: - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - - # ElisionSyllabicTextGroup* (unbounded set) - lines.append("") - lines.append(f" /* _________ {inner_group_cls} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {inner_group_cls}Set &get{inner_group_cls}Set() const;") - lines.append(f" void add{inner_group_cls}(const {inner_group_cls}Ptr &value);") - lines.append(f" void remove{inner_group_cls}(const {inner_group_cls}SetIterConst &value);") - lines.append(f" void clear{inner_group_cls}Set();") - lines.append(f" {inner_group_cls}Ptr get{inner_group_cls}(") - lines.append(f" const {inner_group_cls}SetIterConst &setIterator) const;") - - for e in post_elems: - cc = pascal(e.element_name) - optional = e.min_occurs == 0 - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = {e.min_occurs}, maxOccurs = {e.max_occurs} _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - if optional: - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for e in pre_elems: - cc = pascal(e.element_name) - lines.append(f" {cc}Ptr my{cc};") - if e.min_occurs == 0: - lines.append(f" bool myHas{cc};") - lines.append(f" {inner_group_cls}Set my{inner_group_cls}Set;") - for e in post_elems: - cc = pascal(e.element_name) - lines.append(f" {cc}Ptr my{cc};") - if e.min_occurs == 0: - lines.append(f" bool myHas{cc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_syllabic_text_group_cpp(group_cls, pre_elems, inner_group_cls, - post_elems): - elem_classes = sorted({pascal(e.element_name) for e in pre_elems + post_elems} - | {inner_group_cls}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{group_cls}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in elem_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [] - for e in pre_elems: - cc = pascal(e.element_name) - init_parts.append(f"my{cc}(make{cc}())") - if e.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - init_parts.append(f"my{inner_group_cls}Set()") - for e in post_elems: - cc = pascal(e.element_name) - init_parts.append(f"my{cc}(make{cc}())") - if e.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - _emit_ctor_init(lines, f"{group_cls}::{group_cls}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {group_cls}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{group_cls}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {group_cls}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - # streamContents: pre_elems (in order; optional ones emit endl AFTER if - # required text follows; the committed pattern emits the optional - # syllabic first then endl, then text). For maximal fidelity we model: - # if optional: emit elem, endl - # if required: emit elem (no surrounding endl) - # The set iterates with leading endl per element, calling streamContents. - # Trailing optional elements emit leading endl then elem. - lines.append(f"std::ostream &{group_cls}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" isOneLineOnly = true;") - for e in pre_elems: - cc = pascal(e.element_name) - if e.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" os << std::endl;") - lines.append(" }") - else: - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(f" for (auto x : my{inner_group_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - for e in post_elems: - cc = pascal(e.element_name) - if e.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" }") - else: - lines.append(f" my{cc}->toStream(os, indentLevel);") - - # final isOneLineOnly: true only if NO optional pre/post and NO set entries - cond_parts = [] - for e in pre_elems: - if e.min_occurs == 0: - cond_parts.append(f"myHas{pascal(e.element_name)}") - cond_parts.append(f"my{inner_group_cls}Set.size() > 0") - for e in post_elems: - if e.min_occurs == 0: - cond_parts.append(f"myHas{pascal(e.element_name)}") - lines.append(f" isOneLineOnly = !({' || '.join(cond_parts)});") - lines.append(" return os;") - lines.append("}\n") - - # Accessor blocks, in declaration order. For each element: get, set, - # then if optional also getHas, setHas. For the set: getSet, add, remove, - # clear, get-by-iter (add-before-remove pattern, matching - # CreditWordsGroup / MidiDeviceInstrumentGroup). - def emit_single_accessors(e): - cc = pascal(e.element_name) - optional = e.min_occurs == 0 - lines.append(f"{cc}Ptr {group_cls}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {group_cls}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - if optional: - lines.append(f"bool {group_cls}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {group_cls}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - - for e in pre_elems: - emit_single_accessors(e) - - cc = inner_group_cls - lines.append(f"const {cc}Set &{group_cls}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {group_cls}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {group_cls}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {group_cls}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {group_cls}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - - for e in post_elems: - emit_single_accessors(e) - - lines.append(f"MX_FROM_XELEMENT_UNUSED({group_cls});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_lyric_text_choice_h(choice_class, seq_group_cls, singleton_branches): - """choice_class = 'LyricTextChoice'; seq_group_cls = 'SyllabicTextGroup'; - singleton_branches = list of ElementRefNode (extend, laughing, humming).""" - classes = [seq_group_cls] + [pascal(b.element_name) for b in singleton_branches] - fwds = sorted(classes) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({choice_class})\n") - lines.append(f"inline {choice_class}Ptr make{choice_class}()") - lines.append("{") - lines.append(f" return std::make_shared<{choice_class}>();") - lines.append("}") - lines.append(f"\nclass {choice_class} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - # First enum: the sequence-branch synthetic group. Then singletons in branch order. - seq_group_camel = camel(seq_group_cls[:1].lower() + seq_group_cls[1:]) - # the seq group's identifier is just camel of the class name's first segment - # but committed uses 'syllabicTextGroup' = camelCase of the class name. - seq_group_choice_enum = seq_group_cls[:1].lower() + seq_group_cls[1:] - enum_lines = [f" {seq_group_choice_enum} = 1"] - for i, b in enumerate(singleton_branches, start=2): - enum_lines.append(f" {camel(b.element_name)} = {i}") - lines.append(",\n".join(enum_lines)) - lines.append(" };") - lines.append(f" {choice_class}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {choice_class}::Choice getChoice() const;") - lines.append(f" void setChoice(const {choice_class}::Choice value);") - lines.append("") - lines.append(f" /* _________ {seq_group_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {seq_group_cls}Ptr get{seq_group_cls}() const;") - lines.append(f" void set{seq_group_cls}(const {seq_group_cls}Ptr &value);") - for b in singleton_branches: - cc = pascal(b.element_name) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - lines.append(f" {seq_group_cls}Ptr my{seq_group_cls};") - for b in singleton_branches: - cc = pascal(b.element_name) - lines.append(f" {cc}Ptr my{cc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_lyric_text_choice_cpp(choice_class, seq_group_cls, singleton_branches): - inc_classes = sorted({seq_group_cls} | {pascal(b.element_name) for b in singleton_branches}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{choice_class}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - seq_group_choice_enum = seq_group_cls[:1].lower() + seq_group_cls[1:] - init_parts = [ - f"myChoice(Choice::{seq_group_choice_enum})", - f"my{seq_group_cls}(make{seq_group_cls}())", - ] - for b in singleton_branches: - cc = pascal(b.element_name) - init_parts.append(f"my{cc}(make{cc}())") - _emit_ctor_init(lines, f"{choice_class}::{choice_class}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {choice_class}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{choice_class}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{choice_class}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - # hasContents: returns true ONLY when choice is the sequence branch. - # This is a committed quirk: the singletons emit via toStream which - # write their own XML wrapping, so the parent treats the choice as - # "contentless" in those cases. See state.md gotcha. - lines.append(f"bool {choice_class}::hasContents() const") - lines.append("{") - lines.append(f" return myChoice == Choice::{seq_group_choice_enum};") - lines.append("}\n") - - # streamContents: switch on choice; sequence branch uses streamContents, - # singletons use toStream. - lines.append(f"std::ostream &{choice_class}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - lines.append(f" case Choice::{seq_group_choice_enum}: {{") - lines.append(f" my{seq_group_cls}->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" }") - lines.append(" break;") - for b in singleton_branches: - cc = pascal(b.element_name) - lines.append(f" case Choice::{camel(b.element_name)}: {{") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" }") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - - lines.append(f"{choice_class}::Choice {choice_class}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - lines.append(f"void {choice_class}::setChoice(const {choice_class}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - # Accessors per member, in declaration order. For each: get, set (no - # 'has' since each member is always present in the choice). - members = [seq_group_cls] + [pascal(b.element_name) for b in singleton_branches] - for cc in members: - lines.append(f"{cc}Ptr {choice_class}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {choice_class}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({choice_class});\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_lyric_h(class_name, attrs_name, choice_class, trailing_elems, - editorial_group_cls): - fwds = sorted({choice_class, editorial_group_cls} - | {pascal(e.element_name) for e in trailing_elems}) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {choice_class} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {choice_class}Ptr get{choice_class}() const;") - lines.append(f" void set{choice_class}(const {choice_class}Ptr &value);") - for e in trailing_elems: - cc = pascal(e.element_name) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = {e.min_occurs}, maxOccurs = {e.max_occurs} _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - if e.min_occurs == 0: - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - lines.append("") - lines.append(f" /* _________ {editorial_group_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {editorial_group_cls}Ptr get{editorial_group_cls}() const;") - lines.append(f" void set{editorial_group_cls}(const {editorial_group_cls}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(f" {choice_class}Ptr my{choice_class};") - for e in trailing_elems: - cc = pascal(e.element_name) - lines.append(f" {cc}Ptr my{cc};") - if e.min_occurs == 0: - lines.append(f" bool myHas{cc};") - lines.append(f" {editorial_group_cls}Ptr my{editorial_group_cls};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_lyric_cpp(elem_name, class_name, attrs_name, choice_class, - seq_group_cls, singleton_branches, trailing_elems, - editorial_group_cls, seq_branch_starters): - # Includes: choice class, editorial group, trailing elems, plus the - # singletons + seq-group because fromXElement uses them. - inc_classes = sorted({choice_class, editorial_group_cls, seq_group_cls} - | {pascal(b.element_name) for b in singleton_branches} - | {pascal(e.element_name) for e in trailing_elems}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [ - f"myAttributes(std::make_shared<{attrs_name}>())", - f"my{choice_class}(make{choice_class}())", - ] - for e in trailing_elems: - cc = pascal(e.element_name) - init_parts.append(f"my{cc}(make{cc}())") - if e.min_occurs == 0: - init_parts.append(f"myHas{cc}(false)") - init_parts.append(f"my{editorial_group_cls}(make{editorial_group_cls}())") - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" if (myAttributes->hasValues())") - lines.append(" {") - lines.append(" myAttributes->toStream(os);") - lines.append(" }") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' return os << "{elem_name}";') - lines.append("}\n") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - # Attribute accessors before streamContents (matches committed Lyric.cpp). - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}\n") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}\n") - - # streamContents: leading endl, choice streamContents, trailing optional - # elements (each guarded by has-flag), then editorial group (with leading - # endl only when it has contents), then trailing endl. - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" os << std::endl;") - lines.append(f" my{choice_class}->streamContents(os, indentLevel + 1, isOneLineOnly);") - for e in trailing_elems: - cc = pascal(e.element_name) - if e.min_occurs == 0: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" }") - else: - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(f" if (my{editorial_group_cls}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(f" my{editorial_group_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - # Choice accessors - lines.append(f"{choice_class}Ptr {class_name}::get{choice_class}() const") - lines.append("{") - lines.append(f" return my{choice_class};") - lines.append("}\n") - lines.append(f"void {class_name}::set{choice_class}(const {choice_class}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{choice_class} = value;") - lines.append(" }") - lines.append("}\n") - - for e in trailing_elems: - cc = pascal(e.element_name) - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - if e.min_occurs == 0: - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}\n") - - lines.append(f"{editorial_group_cls}Ptr {class_name}::get{editorial_group_cls}() const") - lines.append("{") - lines.append(f" return my{editorial_group_cls};") - lines.append("}\n") - lines.append(f"void {class_name}::set{editorial_group_cls}(const {editorial_group_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{editorial_group_cls} = value;") - lines.append(" }") - lines.append("}\n") - - # fromXElementImpl: iterate xelement; for each child: - # - try checkSetChoiceMember for each singleton branch - # - else: set choice to seq-group, get the seq-group ptr, importGroup - # - then importElement for each trailing optional - # - then importGroup for editorial - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - # The committed Lyric.cpp has deliberate blank lines around each - # checkSetChoiceMember block (after the opening brace, before/after - # continue, and between the if/else-if branches). `make fmt` preserves - # those blank lines, so we emit them explicitly. - lines.append("") - seq_group_enum = seq_group_cls[:1].lower() + seq_group_cls[1:] - for i, b in enumerate(singleton_branches): - cc = pascal(b.element_name) - cname = b.element_name - guard = "if" if i == 0 else "else if" - lines.append(f' {guard} (checkSetChoiceMember(message, *it, isSuccess, my{choice_class}, "{cname}",') - lines.append(f' &{choice_class}::get{cc},') - lines.append(f' static_cast({choice_class}::Choice::{camel(cname)})))') - lines.append(" {") - lines.append("") - lines.append(" continue;") - lines.append(" }") - lines.append("") - # The sequence-branch (syllabicTextGroup) is only entered when the current - # element can *start* that group (e.g. syllabic, text). importGroup consumes - # the whole group in one call, so later siblings (end-line, end-paragraph, - # footnote, level) must NOT re-enter this branch -- a bare `else` would feed - # them to importGroup, which then reports the next required element as - # missing. Guarding on the starter names lets those siblings fall through to - # their own handlers below. - starter_cond = " || ".join( - f'it->getName() == "{e.element_name}"' for e in seq_branch_starters) - lines.append(f" else if ({starter_cond})") - lines.append(" {") - lines.append(f" my{choice_class}->setChoice({choice_class}::Choice::{seq_group_enum});") - lines.append(f" {seq_group_cls}Ptr ptr = my{choice_class}->get{seq_group_cls}();") - lines.append(f" importGroup(message, it, endIter, isSuccess, ptr);") - lines.append(" }") - for e in trailing_elems: - cc = pascal(e.element_name) - if e.min_occurs == 0: - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, myHas{cc}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - else: - lines.append(f" importElement(message, *it, isSuccess, *my{cc});") - lines.append(f" importGroup(message, it, endIter, isSuccess, my{editorial_group_cls});") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_lyric_family(elem_name, elem, ct, model, generated_attrs, stats): - s = _extract_lyric_structure(ct) - - class_name = element_class_name(elem_name) # Lyric - # The choice class name is bespoke: 'LyricTextChoice'. It is not - # pascal(elem_name) + 'Choice' (which would be 'LyricChoice'). The - # original codegen named it after the "text" sense of its branches. - choice_class = pascal(elem_name) + "TextChoice" - - # Synthetic group class names are derived bottom-up from the first two - # children of each synthetic group plus a 'Group' suffix. - inner_optional_group = _synth_group_name_from_children(s["inner_optional_elems"]) - inner_seq_group = _synth_group_name_from_children([inner_optional_group, s["inner_text"]]) - seq_branch_group = _synth_group_name_from_children(s["seq_branch_pre"]) - - type_name = elem.type_name or "" - - # 1. Attrs struct - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - else: - attrs_name = None - - # 2. Innermost group: ElisionSyllabicGroup - h = generate_elision_syllabic_group_h(inner_optional_group, s["inner_optional_elems"]) - c = generate_elision_syllabic_group_cpp(inner_optional_group, s["inner_optional_elems"]) - write_file(os.path.join(ELEM_DIR, f"{inner_optional_group}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{inner_optional_group}.cpp"), c) - - # 3. Middle group: ElisionSyllabicTextGroup - h = generate_elision_syllabic_text_group_h(inner_seq_group, inner_optional_group, s["inner_text"]) - c = generate_elision_syllabic_text_group_cpp(inner_seq_group, inner_optional_group, s["inner_text"]) - write_file(os.path.join(ELEM_DIR, f"{inner_seq_group}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{inner_seq_group}.cpp"), c) - - # 4. Outer sequence-branch group: SyllabicTextGroup - h = generate_syllabic_text_group_h(seq_branch_group, s["seq_branch_pre"], - inner_seq_group, None, s["seq_branch_post"]) - c = generate_syllabic_text_group_cpp(seq_branch_group, s["seq_branch_pre"], - inner_seq_group, s["seq_branch_post"]) - write_file(os.path.join(ELEM_DIR, f"{seq_branch_group}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{seq_branch_group}.cpp"), c) - - # 5. LyricTextChoice - h = generate_lyric_text_choice_h(choice_class, seq_branch_group, s["singleton_branches"]) - c = generate_lyric_text_choice_cpp(choice_class, seq_branch_group, s["singleton_branches"]) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.cpp"), c) - - # 6. Editorial group class name (already generated elsewhere via GENERATE_GROUPS) - editorial_group_cls = pascal(s["editorial_group"].group_name) + "Group" - - # 7. Lyric - h = generate_lyric_h(class_name, attrs_name, choice_class, s["trailing_elems"], - editorial_group_cls) - c = generate_lyric_cpp(elem_name, class_name, attrs_name, choice_class, - seq_branch_group, s["singleton_branches"], s["trailing_elems"], - editorial_group_cls, s["seq_branch_pre"]) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -# --------------------------------------------------------------------------- -# PartList (bespoke, schema-driven) -# -# part-list's complex type is: -# sequence: -# group ref part-group (0..unbounded) -# group ref score-part (1..1) -# choice (0..unbounded): -# group ref part-group -# group ref score-part -# -# In MusicXML each xs:group of this form wraps a same-named element, so the -# group references resolve to element references with identical names. -# Two classes are produced: -# - PartGroupOrScorePart: the (trailing) choice promoted to a class. It -# is a simple 2-branch choice (each branch is a single element ref), -# with no attributes and no fromXElement logic (MX_FROM_XELEMENT_UNUSED). -# - PartList: with myPartGroupSet, myScorePart, myPartGroupOrScorePartSet, -# and a custom fromXElementImpl that uses a 'isFirstScorePartEncountered' -# state machine to split incoming children before/after the first -# score-part. -# -# All element names come from the parsed XSD model so that spec changes -# (renamed elements, etc.) propagate. The bespoke choice class name -# 'PartGroupOrScorePart' is composed from the two branch element names: -# pascal(branch[0]) + 'Or' + pascal(branch[1]). - - -def pascal_to_camel(pascal_name: str) -> str: - """Convert a PascalCase identifier to camelCase by lowercasing only - the first character. Used for variable names derived from class names - that have no hyphen/underscore separators.""" - if not pascal_name: - return pascal_name - return pascal_name[0].lower() + pascal_name[1:] - - -def _extract_part_list_structure(ct): - """Walk part-list's content_tree and return a dict describing the structure. - - Returns a dict with these keys (all element names come from the parsed XSD): - - leading_unbounded: element name of the leading unbounded group-ref - (e.g. 'part-group') - - singleton: element name of the middle required group-ref - (e.g. 'score-part') - - choice_branches: list of element names of the trailing choice's - branches (e.g. ['part-group', 'score-part']) - """ - tree = ct.content_tree - assert isinstance(tree, SequenceNode) - assert len(tree.children) == 3, ( - f"part-list: expected 3 top-level children, got {len(tree.children)}") - - leading_node = tree.children[0] - middle_node = tree.children[1] - choice_node = tree.children[2] - - def _grouplike_name(n): - if isinstance(n, GroupRefNode): - return n.group_name - if isinstance(n, ElementRefNode): - return n.element_name - raise AssertionError(f"unexpected node: {type(n).__name__}") - - assert isinstance(leading_node, GroupRefNode) and leading_node.max_occurs == -1, ( - "part-list: expected leading unbounded group ref") - assert isinstance(middle_node, GroupRefNode) and middle_node.max_occurs == 1, ( - "part-list: expected middle singleton group ref") - assert isinstance(choice_node, ChoiceNode) and choice_node.max_occurs == -1, ( - "part-list: expected trailing unbounded choice") - - return { - "leading_unbounded": _grouplike_name(leading_node), - "singleton": _grouplike_name(middle_node), - "choice_branches": [_grouplike_name(b) for b in choice_node.branches], - } - - -def generate_part_group_or_score_part_h(class_name, branches): - """branches = list of element names (e.g. ['part-group', 'score-part']). - Emits the simple 2-branch element-ref choice class header. - """ - branch_cls = [pascal(n) for n in branches] - fwds = sorted(set(branch_cls)) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, n in enumerate(branches): - comma = "" if i == len(branches) - 1 else "," - lines.append(f" {camel(n)} = {i + 1}{comma}") - lines.append(" };") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(" /* _________ Choice minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {class_name}::Choice getChoice() const;") - lines.append(f" void setChoice(const {class_name}::Choice value);") - # Bespoke quirk to match committed: the PartGroup accessor block uses - # 'maxPartGroupOccurs' in its comment header rather than 'maxOccurs'. - # See state.md gotcha. - for i, n in enumerate(branches): - cc = pascal(n) - lines.append("") - if i == 0: - lines.append(f" /* _________ {cc} minOccurs = 1, max{cc}Occurs = 1 _________ */") - else: - lines.append(f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - for n in branches: - cc = pascal(n) - lines.append(f" {cc}Ptr my{cc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_part_group_or_score_part_cpp(class_name, branches): - branch_cls = [pascal(n) for n in branches] - inc_classes = sorted(set(branch_cls)) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - # Constructor: default choice is the first branch. - init_parts = [f"myChoice(Choice::{camel(branches[0])})"] - for n in branches: - cc = pascal(n) - init_parts.append(f"my{cc}(make{cc}())") - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - - # hasContents: any one of the chosen branches has contents - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - cond_parts = [] - for n in branches: - cc = pascal(n) - cond_parts.append(f"(myChoice == Choice::{camel(n)} && my{cc}->hasContents())") - lines.append(" return " + (" ||\n ".join(cond_parts)) + ";") - lines.append("}\n") - - # streamContents: switch on the choice, call toStream on the selected branch - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - for n in branches: - cc = pascal(n) - lines.append(f" case Choice::{camel(n)}:") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}\n") - - # Choice accessors - lines.append(f"{class_name}::Choice {class_name}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}\n") - lines.append(f"void {class_name}::setChoice(const {class_name}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}\n") - - # Per-branch accessors: getter then setter - for n in branches: - cc = pascal(n) - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}\n") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}\n") - - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name})\n") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_part_list_h(class_name, leading_name, singleton_name, choice_class_name): - leading_cls = pascal(leading_name) - singleton_cls = pascal(singleton_name) - fwds = sorted({leading_cls, choice_class_name, singleton_cls}) + [class_name] - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(f" /* _________ {leading_cls} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {leading_cls}Set &get{leading_cls}Set() const;") - lines.append(f" void add{leading_cls}(const {leading_cls}Ptr &value);") - lines.append(f" void remove{leading_cls}(const {leading_cls}SetIterConst &value);") - lines.append(f" void clear{leading_cls}Set();") - lines.append(f" {leading_cls}Ptr get{leading_cls}(const {leading_cls}SetIterConst &setIterator) const;") - lines.append("") - lines.append(f" /* _________ {singleton_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {singleton_cls}Ptr get{singleton_cls}() const;") - lines.append(f" void set{singleton_cls}(const {singleton_cls}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {choice_class_name} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {choice_class_name}Set &get{choice_class_name}Set() const;") - lines.append(f" void add{choice_class_name}(const {choice_class_name}Ptr &value);") - lines.append(f" void remove{choice_class_name}(const {choice_class_name}SetIterConst &value);") - lines.append(f" void clear{choice_class_name}Set();") - lines.append(f" {choice_class_name}Ptr get{choice_class_name}(const {choice_class_name}SetIterConst &setIterator) const;") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {leading_cls}Set my{leading_cls}Set;") - lines.append(f" {singleton_cls}Ptr my{singleton_cls};") - lines.append(f" {choice_class_name}Set my{choice_class_name}Set;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_part_list_cpp(elem_name, class_name, leading_name, singleton_name, - choice_class_name, choice_branches): - leading_cls = pascal(leading_name) - singleton_cls = pascal(singleton_name) - inc_classes = sorted({leading_cls, choice_class_name, singleton_cls}) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_classes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include \n") - lines.append("namespace mx\n{\nnamespace core\n{") - - init_parts = [ - f"my{leading_cls}Set()", - f"my{singleton_cls}(make{singleton_cls}())", - f"my{choice_class_name}Set()", - ] - _emit_ctor_init(lines, f"{class_name}::{class_name}()", init_parts) - lines.append("{") - lines.append("}\n") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}\n") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{elem_name}";') - lines.append(" return os;") - lines.append("}\n") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}\n") - - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(f" for (auto x : my{leading_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(f" my{singleton_cls}->toStream(os, indentLevel + 1);") - lines.append(f" for (auto x : my{choice_class_name}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" return os;") - lines.append("}\n") - - # leading set accessors (order: getSet, add, remove, clear, get(setIterator)) - lines.append(f"const {leading_cls}Set &{class_name}::get{leading_cls}Set() const") - lines.append("{") - lines.append(f" return my{leading_cls}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{leading_cls}(const {leading_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{leading_cls}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{leading_cls}(const {leading_cls}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{leading_cls}Set.cend())") - lines.append(" {") - lines.append(f" my{leading_cls}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{leading_cls}Set()") - lines.append("{") - lines.append(f" my{leading_cls}Set.clear();") - lines.append("}\n") - lines.append(f"{leading_cls}Ptr {class_name}::get{leading_cls}(const {leading_cls}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{leading_cls}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {leading_cls}Ptr();") - lines.append("}\n") - - # Singleton accessors - lines.append(f"{singleton_cls}Ptr {class_name}::get{singleton_cls}() const") - lines.append("{") - lines.append(f" return my{singleton_cls};") - lines.append("}\n") - lines.append(f"void {class_name}::set{singleton_cls}(const {singleton_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{singleton_cls} = value;") - lines.append(" }") - lines.append("}\n") - - # Trailing choice-set accessors (order: getSet, add, remove, clear, get) - cc = choice_class_name - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}\n") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}\n") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}\n") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}\n") - - # Custom fromXElementImpl with isFirstScorePartEncountered state machine. - # leading_name e.g. 'part-group', singleton_name e.g. 'score-part'. - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(f" bool isFirst{singleton_cls}Encountered = false;") - lines.append("") - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append(f' if (elementName != "{leading_name}" && elementName != "{singleton_name}")') - lines.append(" {") - lines.append(f' message << "{class_name}: fromXElement encountered unexpected element \'" << elementName << "\'" << std::endl;') - lines.append(" }") - lines.append(f" if (!isFirst{singleton_cls}Encountered)") - lines.append(" {") - lines.append(f' if (elementName == "{leading_name}")') - lines.append(" {") - lines.append(f" auto {camel(leading_name)} = make{leading_cls}();") - lines.append(f" isSuccess &= {camel(leading_name)}->fromXElement(message, *it);") - lines.append(f" my{leading_cls}Set.push_back({camel(leading_name)});") - lines.append(" }") - lines.append(f' else if (elementName == "{singleton_name}")') - lines.append(" {") - lines.append(f" isFirst{singleton_cls}Encountered = true;") - lines.append(f" isSuccess &= my{singleton_cls}->fromXElement(message, *it);") - lines.append(" }") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" auto {pascal_to_camel(choice_class_name)} = make{choice_class_name}();") - lines.append("") - cc_local = pascal_to_camel(choice_class_name) - for i, n in enumerate(choice_branches): - cc_b = pascal(n) - prefix = "if" if i == 0 else "else if" - lines.append(f' {prefix} (elementName == "{n}")') - lines.append(" {") - lines.append(f" {cc_local}->setChoice({choice_class_name}::Choice::{camel(n)});") - lines.append(f" {cc_local}->get{cc_b}()->fromXElement(message, *it);") - lines.append(" }") - lines.append("") - lines.append(f" my{choice_class_name}Set.push_back({cc_local});") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(f" if (!isFirst{singleton_cls}Encountered)") - lines.append(" {") - lines.append(f' message << "{class_name}: at least one \'{singleton_name}\' elements are required but none were found" << std::endl;') - lines.append(" isSuccess = false;") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}\n") - - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Harmony (bespoke, schema-driven) -# -# harmony's complex type: -# sequence: -# group ref harmony-chord (1..unbounded) -> emits HarmonyChordGroup -# element frame (0..1) -# element offset (0..1) -# group ref editorial (1..1) -# group ref staff (0..1) -# plus attribute groups -> HarmonyAttributes -# -# The harmony-chord xs:group internal structure (not exposed via -# model.groups because the inner xs:choice is dropped by _parse_groups, -# and not exposed via complex_types because it's a group, not a type): -# sequence: -# choice: -# element root -# element function (type style-text) -# element kind -# element inversion (0..1) -# element bass (0..1) -# element degree (0..unbounded) -# -# The bespoke handler walks model.root directly to recover the harmony-chord -# inner choice. All names (group, choice branches, elements) come from the -# parsed XSD. -# -# Output classes: -# - HarmonyChordGroup: synthetic group with internal Choice (root|function), -# required kind, optional inversion/bass, unbounded degree set. The -# class is invoked via streamContents (not toStream) from the parent, -# and is MX_FROM_XELEMENT_UNUSED -- the parent Harmony's fromXElementImpl -# state-machine builds the chord groups itself. -# - Harmony: with the HarmonyChordGroup set (always has >=1 entry, ctor -# pushes one), optional frame/offset, required EditorialGroup, -# optional Staff, plus the HarmonyAttributes. Includes a custom -# fromXElementImpl that consumes incoming root/function as the start of -# a new chord group, then required kind, then optional inversion/bass, -# then unbounded degree, then falls through to import frame/offset/ -# editorial/staff. A private addGroup helper replaces the default- -# constructed first entry on first add, then appends subsequently. - - -def _extract_harmony_chord_group_structure(model): - """Walk model.root to find the harmony-chord xs:group and recover its - full structure (the inner xs:choice that model.groups['harmony-chord'] - drops). - - Returns a dict: - - choice_branches: list[str] of element names (e.g. ['root', 'function']) - - kind: str (the required singleton element name) - - optionals: list[str] (optional singleton element names, in order) - - unbounded: str (the trailing unbounded element name) - """ - grp = None - for g in model.root.iter(f"{XS}group"): - if g.get("name") == "harmony-chord": - grp = g - break - assert grp is not None, "harmony-chord group not found in XSD" - - seq = grp.find(f"{XS}sequence") - assert seq is not None, "harmony-chord: expected sequence" - - choice_branches = [] - singletons = [] # (name, min_occurs) - unbounded = None - - for child in seq: - if child.tag == f"{XS}choice": - for b in child: - if b.tag == f"{XS}element": - nm = b.get("ref") or b.get("name") - if nm: - choice_branches.append(nm) - elif child.tag == f"{XS}element": - nm = child.get("ref") or child.get("name") - if not nm: - continue - min_o = int(child.get("minOccurs", "1")) - max_o = child.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - if max_o == -1: - unbounded = nm - else: - singletons.append((nm, min_o)) - - assert choice_branches, "harmony-chord: expected at least one choice branch" - assert singletons, "harmony-chord: expected at least the required kind element" - kind = singletons[0][0] - assert singletons[0][1] == 1, "harmony-chord: first singleton must be required" - optionals = [n for (n, mo) in singletons[1:] if mo == 0] - assert unbounded is not None, "harmony-chord: expected an unbounded trailing element" - - return { - "choice_branches": choice_branches, - "kind": kind, - "optionals": optionals, - "unbounded": unbounded, - } - - -def _extract_harmony_structure(ct, model): - """Walk harmony's content_tree to identify the leading harmony-chord - group ref, the optional frame/offset elements, the editorial group ref, - and the optional staff group ref. Returns a dict.""" - tree = ct.content_tree - assert isinstance(tree, SequenceNode) - - chord_group = None - optional_elems = [] # list[ElementRefNode] - frame, offset - editorial_group = None - staff_group = None - - for c in tree.children: - if isinstance(c, GroupRefNode) and c.max_occurs == -1: - chord_group = c - elif isinstance(c, ElementRefNode) and c.min_occurs == 0: - optional_elems.append(c) - elif isinstance(c, GroupRefNode) and c.group_name == "staff": - staff_group = c - elif isinstance(c, GroupRefNode): - editorial_group = c - - assert chord_group is not None, "harmony: expected leading unbounded chord group ref" - assert editorial_group is not None, "harmony: expected editorial group ref" - - inner = _extract_harmony_chord_group_structure(model) - - return { - "chord_group_name": chord_group.group_name, # 'harmony-chord' - "optional_elems": [c.element_name for c in optional_elems], - "editorial_group": editorial_group.group_name, # 'editorial' - "staff_group": staff_group.group_name if staff_group else None, - "inner": inner, - } - - -def generate_harmony_chord_group_h(class_name, inner): - """class_name e.g. 'HarmonyChordGroup'. inner = dict from - _extract_harmony_chord_group_structure.""" - branches = inner["choice_branches"] - kind = inner["kind"] - optionals = inner["optionals"] - unbounded = inner["unbounded"] - - branch_cls = [pascal(n) for n in branches] - kind_cls = pascal(kind) - optional_cls = [pascal(n) for n in optionals] - unbounded_cls = pascal(unbounded) - - fwds = sorted(set(branch_cls + [kind_cls] + optional_cls + [unbounded_cls])) - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for fwd in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})\n") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append(f"\nclass {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, n in enumerate(branches): - comma = "" if i == len(branches) - 1 else "," - lines.append(f" {camel(n)} = {i + 1}{comma}") - lines.append(" };") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {class_name}::Choice getChoice() const;") - lines.append(f" void setChoice(const {class_name}::Choice value);") - # branch accessors - for n in branches: - cc = pascal(n) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - # kind (required singleton) - lines.append("") - lines.append(f" /* _________ {kind_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {kind_cls}Ptr get{kind_cls}() const;") - lines.append(f" void set{kind_cls}(const {kind_cls}Ptr &value);") - # optionals - for n in optionals: - cc = pascal(n) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - # unbounded set - cc = unbounded_cls - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - for n in branches: - cc = pascal(n) - lines.append(f" {cc}Ptr my{cc};") - lines.append(f" {kind_cls}Ptr my{kind_cls};") - for n in optionals: - cc = pascal(n) - lines.append(f" {cc}Ptr my{cc};") - lines.append(f" bool myHas{cc};") - lines.append(f" {unbounded_cls}Set my{unbounded_cls}Set;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_harmony_chord_group_cpp(class_name, inner): - branches = inner["choice_branches"] - kind = inner["kind"] - optionals = inner["optionals"] - unbounded = inner["unbounded"] - - branch_cls = [pascal(n) for n in branches] - kind_cls = pascal(kind) - optional_cls = [pascal(n) for n in optionals] - unbounded_cls = pascal(unbounded) - - includes = sorted(set(branch_cls + [kind_cls] + optional_cls + [unbounded_cls])) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in includes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - - # Ctor - init_parts = [] - init_parts.append(f"myChoice(Choice::{camel(branches[0])})") - for n in branches: - cc = pascal(n) - init_parts.append(f"my{cc}(make{cc}())") - init_parts.append(f"my{kind_cls}(make{kind_cls}())") - for n in optionals: - cc = pascal(n) - init_parts.append(f"my{cc}(make{cc}())") - init_parts.append(f"myHas{cc}(false)") - init_parts.append(f"my{unbounded_cls}Set()") - lines.append(f"{class_name}::{class_name}()") - lines.append(" : " + ", ".join(init_parts)) - lines.append("{") - lines.append("}") - lines.append("") - - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - # streamContents - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - for n in branches: - cc = pascal(n) - lines.append(f" case Choice::{camel(n)}:") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(f" my{kind_cls}->toStream(os, indentLevel);") - for n in optionals: - cc = pascal(n) - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - lines.append(" }") - lines.append(f" for (auto x : my{unbounded_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - lines.append("") - - # getChoice / setChoice - lines.append(f"{class_name}::Choice {class_name}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setChoice(const {class_name}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}") - lines.append("") - - # branch accessors (get/set) - for n in branches: - cc = pascal(n) - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # kind accessors - cc = kind_cls - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # optional accessors - for n in optionals: - cc = pascal(n) - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}") - lines.append("") - - # unbounded set accessors - cc = unbounded_cls - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}") - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}") - lines.append("") - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_harmony_h(class_name, attrs_name, chord_group_cls, optional_elems, - editorial_group_cls, staff_cls): - """Emit Harmony.h. optional_elems is a list of element names (frame, offset).""" - optional_cls = [pascal(n) for n in optional_elems] - - fwds = [f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})"] - elem_fwds = [editorial_group_cls] + optional_cls + [chord_group_cls] - if staff_cls: - elem_fwds.append(staff_cls) - # sort alphabetically (this matches committed Harmony.h ordering of fwd decls) - for fwd in sorted(set(elem_fwds)): - fwds.append(f"MX_FORWARD_DECLARE_ELEMENT({fwd})") - fwds.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})") - - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.extend(fwds) - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - # chord group set - cc = chord_group_cls - lines.append(f" /* _________ {cc} minOccurs = 1, maxOccurs = unbounded _________ */") - lines.append(f" const {cc}Set &get{cc}Set() const;") - lines.append(f" void add{cc}(const {cc}Ptr &value);") - lines.append(f" void remove{cc}(const {cc}SetIterConst &value);") - lines.append(f" void clear{cc}Set();") - lines.append(f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;") - # optional elements - for n in optional_elems: - cc = pascal(n) - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - # editorial group (required) - cc = editorial_group_cls - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - # optional staff - if staff_cls: - cc = staff_cls - lines.append("") - lines.append(f" /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {cc}Ptr get{cc}() const;") - lines.append(f" void set{cc}(const {cc}Ptr &value);") - lines.append(f" bool getHas{cc}() const;") - lines.append(f" void setHas{cc}(const bool value);") - - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(f" {chord_group_cls}Set my{chord_group_cls}Set;") - for n in optional_elems: - cc = pascal(n) - lines.append(f" {cc}Ptr my{cc};") - lines.append(f" bool myHas{cc};") - lines.append(f" {editorial_group_cls}Ptr my{editorial_group_cls};") - if staff_cls: - lines.append(f" {staff_cls}Ptr my{staff_cls};") - lines.append(f" bool myHas{staff_cls};") - lines.append("") - lines.append(f" void addGroup({chord_group_cls}Ptr &grp, bool &isFirst);") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_harmony_cpp(elem_name, class_name, attrs_name, chord_group_cls, - inner, optional_elems, editorial_group_cls, staff_cls): - """Emit Harmony.cpp including the custom fromXElementImpl state machine.""" - branches = inner["choice_branches"] # ['root', 'function'] - kind = inner["kind"] # 'kind' - inner_optionals = inner["optionals"] # ['inversion', 'bass'] - unbounded = inner["unbounded"] # 'degree' - - branch_cls = [pascal(n) for n in branches] - kind_cls = pascal(kind) - inner_optional_cls = [pascal(n) for n in inner_optionals] - unbounded_cls = pascal(unbounded) - optional_cls = [pascal(n) for n in optional_elems] - - # Include set: parent attrs, chord-group's members (so makeXxx() calls - # resolve), the chord group class itself, the optional elements, the - # editorial group, the staff. Match the committed include layout, which - # lists every element class touched in fromXElementImpl plus the chord - # group itself. - includes = sorted(set( - branch_cls + [kind_cls] + inner_optional_cls + [unbounded_cls] - + [editorial_group_cls] + optional_cls + [chord_group_cls] - + ([staff_cls] if staff_cls else []) - )) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in includes: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - - # Constructor - init_parts = [ - f"myAttributes(std::make_shared<{attrs_name}>())", - f"my{chord_group_cls}Set()", - ] - for n in optional_elems: - cc = pascal(n) - init_parts.append(f"my{cc}(make{cc}())") - init_parts.append(f"myHas{cc}(false)") - init_parts.append(f"my{editorial_group_cls}(make{editorial_group_cls}())") - if staff_cls: - init_parts.append(f"my{staff_cls}(make{staff_cls}())") - init_parts.append(f"myHas{staff_cls}(false)") - - lines.append(f"{class_name}::{class_name}()") - lines.append(" : " + ", ".join(init_parts)) - lines.append("{") - lines.append(f" my{chord_group_cls}Set.push_back(make{chord_group_cls}());") - lines.append("}") - lines.append("") - - # hasAttributes / streamAttributes / streamName - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return myAttributes->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{elem_name}";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - - # streamContents - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(f" for (auto x : my{chord_group_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - for n in optional_elems: - cc = pascal(n) - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(f" if (my{editorial_group_cls}->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{editorial_group_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - if staff_cls: - lines.append(f" if (myHas{staff_cls})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{staff_cls}->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - lines.append("") - - # Attribute accessors - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # Chord group set accessors. Note: removeXxx for a "must have at least 1" - # set guards against removing the last element. clearXxxSet resets to a - # single default entry. - cc = chord_group_cls - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" if (my{cc}Set.size() > 1)") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append(f" my{cc}Set.push_back(make{cc}());") - lines.append("}") - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}") - lines.append("") - - # Optional element accessors (frame/offset) - for n in optional_elems: - cc = pascal(n) - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}") - lines.append("") - - # Editorial group accessor - cc = editorial_group_cls - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # Staff accessors (optional) - if staff_cls: - cc = staff_cls - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}") - lines.append("") - - # fromXElementImpl. The state machine: on each pass, if we see a choice - # branch (root|function), we start consuming the chord-group elements - # in fixed order (kind, optional inversion, optional bass, then any - # number of degree). On any other element name, we try importElement - # for the optional frame/offset, then importGroup for editorial, - # then importElement for staff. - branch_check = ' || '.join(f'elementName == "{n}"' for n in branches) - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append(" bool isChoiceFound = false;") - lines.append(f" bool isFirst{chord_group_cls}Added = false;") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append(f" if ({branch_check})") - lines.append(" {") - lines.append(" bool decrementIter = false;") - lines.append(" isChoiceFound = true;") - lines.append(f" auto item = make{chord_group_cls}();") - - # Per-branch dispatch - for i, n in enumerate(branches): - cc = pascal(n) - prefix = "if" if i == 0 else "else" - if i == 0: - lines.append(f' if (elementName == "{n}")') - elif i == len(branches) - 1: - lines.append(" else") - else: - lines.append(f' else if (elementName == "{n}")') - lines.append(" {") - lines.append(f" item->setChoice({chord_group_cls}::Choice::{camel(n)});") - lines.append(f" isSuccess &= item->get{cc}()->fromXElement(message, *it);") - lines.append(" }") - lines.append(" decrementIter = true;") - lines.append(" ++it;") - # required kind - lines.append(" if (it == endIter || it->getName() != \"" + kind + "\")") - lines.append(" {") - lines.append(f' message << "{class_name}: \'{kind}\' is a required element and is missing aborting" << std::endl;') - lines.append(" return false;") - lines.append(" }") - lines.append(f" isSuccess &= item->get{kind_cls}()->fromXElement(message, *it);") - lines.append(" decrementIter = true;") - lines.append(" ++it;") - lines.append(" if (it == endIter)") - lines.append(" {") - lines.append(f" addGroup(item, isFirst{chord_group_cls}Added);") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - # optionals - for n in inner_optionals: - cc = pascal(n) - lines.append(f' if (it->getName() == "{n}")') - lines.append(" {") - lines.append(f" isSuccess &= item->get{cc}()->fromXElement(message, *it);") - lines.append(f" item->setHas{cc}(true);") - lines.append(" decrementIter = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append(" if (it == endIter)") - lines.append(" {") - lines.append(f" addGroup(item, isFirst{chord_group_cls}Added);") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - # unbounded (degree) - lines.append("") - lines.append(f' while (it != endIter && it->getName() == "{unbounded}")') - lines.append(" {") - lines.append(f" auto {camel(unbounded)} = make{unbounded_cls}();") - lines.append(f" isSuccess &= {camel(unbounded)}->fromXElement(message, *it);") - lines.append(f" item->add{unbounded_cls}({camel(unbounded)});") - lines.append(" decrementIter = true;") - lines.append(" ++it;") - lines.append(" }") - lines.append("") - lines.append(f" addGroup(item, isFirst{chord_group_cls}Added);") - lines.append("") - lines.append(" if (decrementIter)") - lines.append(" {") - lines.append(" --it;") - lines.append(" }") - lines.append(" continue;") - lines.append(" }") - lines.append("") - # frame/offset - for n in optional_elems: - cc = pascal(n) - lines.append(f" if (importElement(message, *it, isSuccess, *my{cc}, myHas{cc}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - # editorial group - lines.append(f" importGroup(message, it, endIter, isSuccess, my{editorial_group_cls});") - # staff - if staff_cls: - lines.append(f" if (importElement(message, *it, isSuccess, *my{staff_cls}, myHas{staff_cls}))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" if (!isChoiceFound)") - lines.append(" {") - # Quoted branch names: emit ' as a backslash-escape only when the - # surrounding C++ string is wrapped in single quotes; here it's a - # double-quoted C++ string so plain ' is correct. - branch_names_quoted = " or ".join(f"'{n}'" for n in branches) - lines.append(f' message << "{class_name}: either {branch_names_quoted} is required but neither was present" << std::endl;') - lines.append(" isSuccess = false;") - lines.append(" }") - lines.append("") - lines.append(" return isSuccess;") - lines.append("}") - lines.append("") - - # addGroup helper - lines.append(f"void {class_name}::addGroup({chord_group_cls}Ptr &grp, bool &isFirst)") - lines.append("{") - lines.append(f" if (!isFirst && my{chord_group_cls}Set.size() == 1)") - lines.append(" {") - lines.append(f" *my{chord_group_cls}Set.begin() = grp;") - lines.append(" isFirst = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{chord_group_cls}Set.push_back(grp);") - lines.append(" isFirst = true;") - lines.append(" }") - lines.append("}") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_harmony_family(elem_name, elem, ct, model, generated_attrs, stats): - s = _extract_harmony_structure(ct, model) - - class_name = element_class_name(elem_name) # Harmony - chord_group_cls = pascal(s["chord_group_name"]) + "Group" # HarmonyChordGroup - editorial_group_cls = pascal(s["editorial_group"]) + "Group" # EditorialGroup - staff_cls = pascal(s["staff_group"]) if s["staff_group"] else None # Staff - - # 1. Attrs struct - type_name = elem.type_name or "" - attrs_name = None - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - # 2. HarmonyChordGroup - h = generate_harmony_chord_group_h(chord_group_cls, s["inner"]) - c = generate_harmony_chord_group_cpp(chord_group_cls, s["inner"]) - write_file(os.path.join(ELEM_DIR, f"{chord_group_cls}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{chord_group_cls}.cpp"), c) - - # 3. Harmony - h = generate_harmony_h(class_name, attrs_name, chord_group_cls, - s["optional_elems"], editorial_group_cls, staff_cls) - c = generate_harmony_cpp(elem_name, class_name, attrs_name, chord_group_cls, - s["inner"], s["optional_elems"], - editorial_group_cls, staff_cls) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -def _emit_part_list_family(elem_name, elem, ct, model, generated_attrs, stats): - s = _extract_part_list_structure(ct) - - class_name = element_class_name(elem_name) # PartList - branches = s["choice_branches"] # ['part-group', 'score-part'] - # Choice class name composed from the two branch names: - # pascal(b[0]) + 'Or' + pascal(b[1]). For part-list this is - # 'PartGroupOrScorePart', matching the committed codegen. - assert len(branches) == 2, "part-list: expected exactly 2 choice branches" - choice_class_name = pascal(branches[0]) + "Or" + pascal(branches[1]) - - # No attributes on part-list. - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, elem.type_name or "", model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - # 1. PartGroupOrScorePart - h = generate_part_group_or_score_part_h(choice_class_name, branches) - c = generate_part_group_or_score_part_cpp(choice_class_name, branches) - write_file(os.path.join(ELEM_DIR, f"{choice_class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{choice_class_name}.cpp"), c) - - # 2. PartList - h = generate_part_list_h(class_name, s["leading_unbounded"], s["singleton"], choice_class_name) - c = generate_part_list_cpp(elem_name, class_name, s["leading_unbounded"], s["singleton"], - choice_class_name, branches) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -# --------------------------------------------------------------------------- -# score-partwise / score-timewise family (bespoke, schema-driven) -# -# Both top-level wrappers live as inline anonymous complex types inside -# top-level xs:element declarations, with the same three-level shape: -# (or "score-timewise") -# -# -# -# ("measure" for timewise) -# -# -# ("part" for timewise) -# -# -# (or part-attributes) -# -# -# -# (or measure-attributes) -# -# -# -# -# -# -# -# The standard ``_parse_elements`` path captures the outer level but the -# inner XML name collisions (``part`` and ``measure`` recur across both -# wrappers) and the direct ``xs:group ref`` at CT level (no wrapping -# sequence) cause partial data. The bespoke handler walks ``model.root`` -# directly to recover the full nested structure. -# -# Structural roles (consistent across both flavors): -# outer -> ScorePartwise / ScoreTimewise -# set_holder -> PartwisePart / TimewiseMeasure (mid level; holds -# a set of music_data_holder children) -# music_data_holder-> PartwiseMeasure / TimewisePart (deepest; holds -# a MusicDataGroup with the choice classes) -# -# The hand-written original code applies a small number of per-flavor -# quirks (extra includes, JIT vs eager attrs, message wording, presence of -# MX_DEBUG_THROW_ON_PARSE_ISSUE, post-loop required-child check, ...). All -# such knobs are captured in SCORE_WRAPPER_FLAVOR_CONFIG below so the same -# shared handler can emit either family. -# --------------------------------------------------------------------------- - - -# Per-flavor knobs that capture hand-written variations between the partwise -# and timewise families. Keys are the outer XSD element names. -SCORE_WRAPPER_FLAVOR_CONFIG = { - "score-partwise": { - # ScorePartwise.cpp - "outer_extra_includes": [], - "outer_loop_uses_end_var": False, - # PartwiseMeasure (music-data holder) - "music_data_holder_attrs_jit": True, - "music_data_holder_debug_throw": True, - # PartwisePart (set holder) - "set_holder_clear_repushes_default": True, - "set_holder_remove_has_size_guard": True, - "set_holder_post_loop_required": False, - "set_holder_first_flag_name": "isFirstAdded", - "set_holder_use_return_macro": True, - # Loop body style differences (partwise variant). - "set_holder_loop_uses_element_name_var": False, - "set_holder_unexpected_order": "message_first", # message << ...; isSuccess = false; - "set_holder_unexpected_msg": "encountered_quoted", # "...: encountered an unexpected element '...'" - "set_holder_begin_deref_parens": False, # *mySet.begin() vs *(mySet.begin()) - "set_holder_from_x_before_first_check": True, - "set_holder_blank_after_first_decl": False, - "set_holder_blank_inside_else": False, - "set_holder_child_var_source": "xml_name", # "xml_name" => camel(xml); "class_name" => pascal_to_camel(cls) - }, - "score-timewise": { - # ScoreTimewise.cpp - "outer_extra_includes": [ - "ezxml/XElement.h", - "ezxml/XElementIterator.h", - ], - "outer_loop_uses_end_var": True, - # TimewisePart (music-data holder) - "music_data_holder_attrs_jit": False, - "music_data_holder_debug_throw": False, - # TimewiseMeasure (set holder) - "set_holder_clear_repushes_default": False, - "set_holder_remove_has_size_guard": False, - "set_holder_post_loop_required": True, - "set_holder_first_flag_name": "isFirstTimewisePartFound", - "set_holder_use_return_macro": False, - # Loop body style differences (timewise variant). - "set_holder_loop_uses_element_name_var": True, - "set_holder_unexpected_order": "issuccess_first", # isSuccess = false; message << ...; - "set_holder_unexpected_msg": "trailing_encountered", # "...: unexpected element '...' encountered" - "set_holder_begin_deref_parens": True, - "set_holder_from_x_before_first_check": True, - "set_holder_blank_after_first_decl": True, - "set_holder_blank_inside_else": True, - "set_holder_child_var_source": "class_name", - }, -} - - -def _extract_score_wrapper_structure(model, outer_name): - """Walk model.root to recover the full nested structure of a top-level - score wrapper. Returns a dict with role-based keys; no XML name is - hardcoded here -- every name flows from the parsed XSD.""" - outer_el = None - for el in model.root.iter(f"{XS}element"): - if el.get("name") == outer_name: - outer_el = el - break - if outer_el is None: - raise RuntimeError( - f"score wrapper '{outer_name}' not found in XSD") - - outer_ct = outer_el.find(f"{XS}complexType") - outer_seq = outer_ct.find(f"{XS}sequence") - outer_attr_group = outer_ct.find(f"{XS}attributeGroup").get("ref") - - header_group = outer_seq.find(f"{XS}group").get("ref") - - inner_el = outer_seq.find(f"{XS}element") - inner_name = inner_el.get("name") - inner_ct = inner_el.find(f"{XS}complexType") - inner_attr_group = inner_ct.find(f"{XS}attributeGroup").get("ref") - inner_seq = inner_ct.find(f"{XS}sequence") - - deepest_el = inner_seq.find(f"{XS}element") - deepest_name = deepest_el.get("name") - deepest_ct = deepest_el.find(f"{XS}complexType") - deepest_content_group = deepest_ct.find(f"{XS}group").get("ref") - deepest_attr_group = deepest_ct.find(f"{XS}attributeGroup").get("ref") - - return { - "outer_name": outer_name, - "outer_attr_group": outer_attr_group, # document-attributes - "header_group": header_group, # score-header - "inner_name": inner_name, # part - "inner_attr_group": inner_attr_group, # part-attributes - "deepest_name": deepest_name, # measure - "deepest_content_group": deepest_content_group, # music-data - "deepest_attr_group": deepest_attr_group, # measure-attributes - } - - -def _score_flavor_prefix(outer_name): - """Derive the mx/core flavor prefix from outer XSD element name. - 'score-partwise' -> 'Partwise', 'score-timewise' -> 'Timewise'.""" - if not outer_name.startswith("score-"): - raise RuntimeError( - f"_score_flavor_prefix expects 'score-X', got '{outer_name}'") - return pascal(outer_name[len("score-"):]) - - -def _generate_score_outer_h(class_name, attrs_name, header_group_cls, - child_cls, fwd_order): - """Emit ScorePartwise.h (or ScoreTimewise.h with appropriate inputs).""" - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append("#pragma once") - lines.append("") - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append("") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for n in fwd_order: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({n})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {header_group_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {header_group_cls}Ptr get{header_group_cls}() const;") - lines.append(f" void set{header_group_cls}(const {header_group_cls}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {child_cls} minOccurs = 1, maxOccurs = unbounded _________ */") - lines.append(f" const {child_cls}Set &get{child_cls}Set() const;") - lines.append(f" void add{child_cls}(const {child_cls}Ptr &value);") - lines.append(f" void remove{child_cls}(const {child_cls}SetIterConst &value);") - lines.append(f" void clear{child_cls}Set();") - lines.append(f" {child_cls}Ptr get{child_cls}(const {child_cls}SetIterConst &setIterator) const;") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(f" {header_group_cls}Ptr my{header_group_cls};") - lines.append(f" {child_cls}Set my{child_cls}Set;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _generate_score_outer_cpp(outer_xml_name, class_name, attrs_name, - header_group_cls, child_cls, inner_xml_name, - header_skip_names, flavor_cfg): - """Emit ScorePartwise.cpp or ScoreTimewise.cpp. - - header_skip_names: list of XML element names (from score-header group) that - the loop silently ``continue``s past because ScoreHeaderGroup already - consumed them. Schema-driven from model.groups['score-header']. - flavor_cfg: per-flavor knobs from SCORE_WRAPPER_FLAVOR_CONFIG. - """ - is_first_var = "isFirst" + pascal(inner_xml_name) + "Added" - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append(f'#include "mx/core/elements/{class_name}.h"') - for inc in flavor_cfg.get("outer_extra_includes", []): - lines.append(f'#include "{inc}"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append(f'#include "mx/core/elements/{child_cls}.h"') - lines.append(f'#include "mx/core/elements/{header_group_cls}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append(f"{class_name}::{class_name}()") - lines.append(f" : myAttributes(std::make_shared<{attrs_name}>()), my{header_group_cls}(make{header_group_cls}()),") - lines.append(f" my{child_cls}Set()") - lines.append("{") - lines.append(f" my{child_cls}Set.push_back(make{child_cls}());") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return myAttributes->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{outer_xml_name}";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" os << std::endl;") - lines.append(f" my{header_group_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(f" for (auto x : my{child_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"{header_group_cls}Ptr {class_name}::get{header_group_cls}() const") - lines.append("{") - lines.append(f" return my{header_group_cls};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{header_group_cls}(const {header_group_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{header_group_cls} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"const {child_cls}Set &{class_name}::get{child_cls}Set() const") - lines.append("{") - lines.append(f" return my{child_cls}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{child_cls}(const {child_cls}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{child_cls}Set.cend())") - lines.append(" {") - lines.append(f" if (my{child_cls}Set.size() > 1)") - lines.append(" {") - lines.append(f" my{child_cls}Set.erase(value);") - lines.append(" }") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{child_cls}(const {child_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{child_cls}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{child_cls}Set()") - lines.append("{") - lines.append(f" my{child_cls}Set.clear();") - lines.append(f" my{child_cls}Set.push_back(make{child_cls}());") - lines.append("}") - lines.append("") - lines.append(f"{child_cls}Ptr {class_name}::get{child_cls}(const {child_cls}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{child_cls}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {child_cls}Ptr();") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(f" bool {is_first_var} = false;") - lines.append("") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append(f" isSuccess &= my{header_group_cls}->fromXElement(message, xelement);") - lines.append("") - lines.append(" auto it = xelement.begin();") - lines.append(" auto end = xelement.end();") - lines.append("") - if flavor_cfg.get("outer_loop_uses_end_var", False): - lines.append(" for (; it != end; ++it)") - else: - lines.append(" for (; it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append("") - skip_check = " || ".join(f'elementName == "{n}"' for n in header_skip_names) - lines.append(f" if ({skip_check})") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(f' else if (elementName == "{inner_xml_name}")') - lines.append(" {") - lines.append(f" auto {camel(inner_xml_name)} = make{child_cls}();") - lines.append("") - lines.append(f" if (!{is_first_var} && my{child_cls}Set.size() == 1)") - lines.append(" {") - lines.append(f" *(my{child_cls}Set.begin()) = {camel(inner_xml_name)};") - lines.append(f" {is_first_var} = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{child_cls}Set.push_back({camel(inner_xml_name)});") - lines.append(f" {is_first_var} = true;") - lines.append(" }") - lines.append("") - lines.append(f" isSuccess &= {camel(inner_xml_name)}->fromXElement(message, *it);") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" -def _generate_partwise_part_h(class_name, attrs_name, child_cls): - """Header for PartwisePart: wraps an unbounded set of PartwiseMeasure.""" - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append("#pragma once") - lines.append("") - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append("") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - # Forward declares ordered: PartwiseMeasure < PartwisePart. - for n in sorted([child_cls, class_name]): - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({n})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {child_cls} minOccurs = 0, maxOccurs = unbounded _________ */") - lines.append(f" const {child_cls}Set &get{child_cls}Set() const;") - lines.append(f" void add{child_cls}(const {child_cls}Ptr &value);") - lines.append(f" void remove{child_cls}(const {child_cls}SetIterConst &value);") - lines.append(f" void clear{child_cls}Set();") - lines.append(f" {child_cls}Ptr get{child_cls}(const {child_cls}SetIterConst &setIterator) const;") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(f" {child_cls}Set my{child_cls}Set;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _generate_partwise_part_cpp(outer_xml_name, class_name, attrs_name, - child_cls, child_xml_name, flavor_cfg): - """Cpp for the set holder (PartwisePart / TimewiseMeasure). - - outer_xml_name is the XSD name (typically 'part' for partwise, - 'measure' for timewise) used in streamName. - flavor_cfg: per-flavor knobs from SCORE_WRAPPER_FLAVOR_CONFIG. - """ - is_first_var = flavor_cfg.get("set_holder_first_flag_name", "isFirstAdded") - if flavor_cfg.get("set_holder_child_var_source", "xml_name") == "class_name": - child_var = pascal_to_camel(child_cls) - else: - child_var = camel(child_xml_name) - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append(f'#include "mx/core/elements/{child_cls}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append(f"{class_name}::{class_name}() : myAttributes(std::make_shared<{attrs_name}>()), my{child_cls}Set()") - lines.append("{") - lines.append(f" my{child_cls}Set.push_back(make{child_cls}());") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return myAttributes->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{outer_xml_name}";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" if (hasContents())") - lines.append(" {") - lines.append(f" for (auto x : my{child_cls}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"const {child_cls}Set &{class_name}::get{child_cls}Set() const") - lines.append("{") - lines.append(f" return my{child_cls}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{child_cls}(const {child_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{child_cls}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{child_cls}(const {child_cls}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{child_cls}Set.cend())") - lines.append(" {") - if flavor_cfg.get("set_holder_remove_has_size_guard", True): - lines.append(f" if (my{child_cls}Set.size() > 1)") - lines.append(" {") - lines.append(f" my{child_cls}Set.erase(value);") - lines.append(" }") - else: - lines.append(f" my{child_cls}Set.erase(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{child_cls}Set()") - lines.append("{") - lines.append(f" my{child_cls}Set.clear();") - if flavor_cfg.get("set_holder_clear_repushes_default", True): - lines.append(f" my{child_cls}Set.push_back(make{child_cls}());") - lines.append("}") - lines.append("") - lines.append(f"{child_cls}Ptr {class_name}::get{child_cls}(const {child_cls}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{child_cls}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {child_cls}Ptr();") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - if flavor_cfg.get("set_holder_blank_after_first_decl", False): - lines.append("") - lines.append(f" bool {is_first_var} = false;") - lines.append("") - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - if flavor_cfg.get("set_holder_loop_uses_element_name_var", False): - lines.append(" const std::string elementName = it->getName();") - lines.append("") - name_expr = "elementName" - else: - name_expr = "it->getName()" - lines.append(f' if ({name_expr} != "{child_xml_name}")') - lines.append(" {") - unexpected_msg = flavor_cfg.get("set_holder_unexpected_msg", "encountered_quoted") - if unexpected_msg == "trailing_encountered": - msg_line = (f' message << "{class_name}: unexpected element \'"' - f' << {name_expr} << "\' encountered" << std::endl;') - else: # "encountered_quoted" - msg_line = (f' message << "{class_name}: encountered an unexpected element \'"' - f' << {name_expr} << "\'" << std::endl;') - if flavor_cfg.get("set_holder_unexpected_order", "message_first") == "issuccess_first": - lines.append(" isSuccess = false;") - lines.append(msg_line) - else: - lines.append(msg_line) - lines.append(" isSuccess = false;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" auto {child_var} = make{child_cls}();") - if flavor_cfg.get("set_holder_blank_inside_else", False): - lines.append(f" isSuccess &= {child_var}->fromXElement(message, *it);") - lines.append("") - else: - lines.append(f" isSuccess &= {child_var}->fromXElement(message, *it);") - lines.append(f" if (!{is_first_var} && my{child_cls}Set.size() == 1)") - lines.append(" {") - if flavor_cfg.get("set_holder_begin_deref_parens", False): - lines.append(f" *(my{child_cls}Set.begin()) = {child_var};") - else: - lines.append(f" *my{child_cls}Set.begin() = {child_var};") - lines.append(f" {is_first_var} = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{child_cls}Set.push_back({child_var});") - lines.append(f" {is_first_var} = true;") - lines.append(" }") - lines.append(" }") - lines.append(" }") - lines.append("") - if flavor_cfg.get("set_holder_post_loop_required", False): - lines.append(f" if (!{is_first_var})") - lines.append(" {") - lines.append(f' message << "{class_name}: no \'{child_xml_name}\' elements were found";') - lines.append(" isSuccess = false;") - lines.append(" }") - lines.append("") - if flavor_cfg.get("set_holder_use_return_macro", True): - lines.append(" MX_RETURN_IS_SUCCESS;") - else: - lines.append(" return isSuccess;") - lines.append("}") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" -def _generate_partwise_measure_h(class_name, attrs_name, music_data_group_cls, - flavor_cfg): - """Header for the music-data holder (PartwiseMeasure / TimewisePart). - - flavor_cfg.music_data_holder_attrs_jit controls whether the attrs member - is JIT-allocated under MX_MUTEX (partwise) or eagerly held (timewise). - """ - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append("#pragma once") - lines.append("") - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append("") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for n in sorted([music_data_group_cls, class_name]): - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({n})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - lines.append(f" /* _________ {music_data_group_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {music_data_group_cls}Ptr get{music_data_group_cls}() const;") - lines.append(f" void set{music_data_group_cls}(const {music_data_group_cls}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - if flavor_cfg.get("music_data_holder_attrs_jit", True): - lines.append(" MX_MUTEX") - lines.append(f" mutable {attrs_name}Ptr myAttributes;") - else: - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(f" {music_data_group_cls}Ptr my{music_data_group_cls};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _generate_partwise_measure_cpp(outer_xml_name, class_name, attrs_name, - music_data_group_cls, music_data_choices, - flavor_cfg): - """Cpp for the music-data holder (PartwiseMeasure / TimewisePart). - - music_data_choices: list of dicts each with: - xml_name - XML element name ('note', 'attributes', ...) - class_name - C++ class name ('Note', 'Properties', ...) - enum_name - Choice enum name ('note', 'properties', ...) - flavor_cfg: per-flavor knobs from SCORE_WRAPPER_FLAVOR_CONFIG. - """ - attrs_jit = flavor_cfg.get("music_data_holder_attrs_jit", True) - debug_throw = flavor_cfg.get("music_data_holder_debug_throw", True) - # JIT flavor (partwise) refers to attrs via getAttributes() everywhere - # to trigger the lazy allocation; eager flavor (timewise) accesses - # myAttributes directly because it is initialized in the ctor. - attrs_ref = "getAttributes()" if attrs_jit else "myAttributes" - # Includes: alphabetical list of music-data choice classes, plus - # MusicDataChoice and MusicDataGroup. - choice_class_includes = sorted({c["class_name"] for c in music_data_choices}) - extra = sorted({"MusicDataChoice", music_data_group_cls}) - - lines = [] - lines.append("// MusicXML Class Library") - lines.append("// Copyright (c) by Matthew James Briggs") - lines.append("// Distributed under the MIT License") - lines.append("") - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - # The committed PartwiseMeasure.cpp has #includes sorted alphabetically - # for each choice class then MusicDataChoice then MusicDataGroup then Note - # (because Note alphabetically falls between MusicDataGroup and Print). - # We compose the full sorted set: - all_includes = sorted(set(choice_class_includes) | set(extra)) - for n in all_includes: - lines.append(f'#include "mx/core/elements/{n}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - if attrs_jit: - ctor_init = f"myAttributes(nullptr)" - else: - ctor_init = f"myAttributes(std::make_shared<{attrs_name}>())" - lines.append(f"{class_name}::{class_name}() : {ctor_init}, my{music_data_group_cls}(make{music_data_group_cls}())") - lines.append("{") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(f" return {attrs_ref}->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(f" return {attrs_ref}->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(f' os << "{outer_xml_name}";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(f" return my{music_data_group_cls}->hasContents();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" if (hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" my{music_data_group_cls}->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" isOneLineOnly = true;") - lines.append(" }") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - if attrs_jit: - lines.append(" MX_LOCK") - lines.append(f" MX_JIT_ALLOCATE_ATTRIBUTES({attrs_name});") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"{music_data_group_cls}Ptr {class_name}::get{music_data_group_cls}() const") - lines.append("{") - lines.append(f" return my{music_data_group_cls};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{music_data_group_cls}(const {music_data_group_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{music_data_group_cls} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" bool isFirstMusicDataChoiceAdded = false;") - lines.append(f" isSuccess &= {attrs_ref}->fromXElement(message, xelement);") - lines.append("") - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append("") - lines.append(" auto choiceObject = makeMusicDataChoice();") - lines.append(" bool choiceObjectShouldBeAdded = true;") - lines.append("") - for i, c in enumerate(music_data_choices): - prefix = "if" if i == 0 else "else if" - lines.append(f' {prefix} (elementName == "{c["xml_name"]}")') - lines.append(" {") - lines.append(f" choiceObject->setChoice(MusicDataChoice::Choice::{c['enum_name']});") - lines.append(f" isSuccess &= choiceObject->get{c['class_name']}()->fromXElement(message, *it);") - if debug_throw: - lines.append(" MX_DEBUG_THROW_ON_PARSE_ISSUE;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" choiceObjectShouldBeAdded = false;") - lines.append(" }") - lines.append("") - lines.append(" if (choiceObjectShouldBeAdded)") - lines.append(" {") - lines.append(f" if (!isFirstMusicDataChoiceAdded && my{music_data_group_cls}->getMusicDataChoiceSet().size() == 1)") - lines.append(" {") - lines.append(f" my{music_data_group_cls}->addMusicDataChoice(choiceObject);") - lines.append(f" my{music_data_group_cls}->removeMusicDataChoice(my{music_data_group_cls}->getMusicDataChoiceSet().cbegin());") - lines.append(" isFirstMusicDataChoiceAdded = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{music_data_group_cls}->addMusicDataChoice(choiceObject);") - lines.append(" isFirstMusicDataChoiceAdded = true;") - lines.append(" }") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def _emit_score_wrapper_family(elem_name, elem, ct, model, generated_attrs, stats): - """Shared bespoke handler for the score-partwise / score-timewise family. - - Emits the three classes that make up the wrapper (outer, set holder, - music-data holder) plus their attribute structs from a single - schema-driven structural walk of the XSD. Per-flavor hand-written - variations are pulled from SCORE_WRAPPER_FLAVOR_CONFIG[elem_name]. - """ - flavor_cfg = SCORE_WRAPPER_FLAVOR_CONFIG.get(elem_name, {}) - s = _extract_score_wrapper_structure(model, elem_name) - - flavor = _score_flavor_prefix(elem_name) # "Partwise" / "Timewise" - outer_class = element_class_name(elem_name) # "ScorePartwise" / "ScoreTimewise" - inner_class = flavor + pascal(s["inner_name"]) # "PartwisePart" / "TimewiseMeasure" - deepest_class = flavor + pascal(s["deepest_name"]) # "PartwiseMeasure" / "TimewisePart" - header_group_cls = pascal(s["header_group"]) + "Group" # "ScoreHeaderGroup" - music_data_group_cls = pascal(s["deepest_content_group"]) + "Group" # "MusicDataGroup" - - # Attribute struct names follow the standard convention based on the - # underlying element XML name; resolve_attrs_name returns: - # "score-partwise" -> "ScorePartwiseAttributes" - # "part" -> "PartAttributes" - # "measure" -> "MeasureAttributes" - outer_attrs_name = resolve_attrs_name(elem_name, "", model) - inner_attrs_name = resolve_attrs_name(s["inner_name"], "", model) - deepest_attrs_name = resolve_attrs_name(s["deepest_name"], "", model) - - # 1. Outer attrs struct (document-attributes -> ScorePartwiseAttributes). - if ct.attributes: - if outer_attrs_name not in generated_attrs and outer_attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(outer_attrs_name, ct.attributes, model) - c = generate_attrs_cpp(outer_attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{outer_attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{outer_attrs_name}.cpp"), c) - generated_attrs.add(outer_attrs_name) - stats["attrs_written"] += 1 - - # 2. Inner attrs struct (part-attributes -> PartAttributes). - inner_attrs_list = model.attribute_groups.get(s["inner_attr_group"], []) - if inner_attrs_list: - if inner_attrs_name not in generated_attrs and inner_attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(inner_attrs_name, inner_attrs_list, model) - c = generate_attrs_cpp(inner_attrs_name, inner_attrs_list, model) - write_file(os.path.join(ELEM_DIR, f"{inner_attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{inner_attrs_name}.cpp"), c) - generated_attrs.add(inner_attrs_name) - stats["attrs_written"] += 1 - - # 3. Deepest attrs struct (measure-attributes -> MeasureAttributes). - deepest_attrs_list = model.attribute_groups.get(s["deepest_attr_group"], []) - if deepest_attrs_list: - if deepest_attrs_name not in generated_attrs and deepest_attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(deepest_attrs_name, deepest_attrs_list, model) - c = generate_attrs_cpp(deepest_attrs_name, deepest_attrs_list, model) - write_file(os.path.join(ELEM_DIR, f"{deepest_attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{deepest_attrs_name}.cpp"), c) - generated_attrs.add(deepest_attrs_name) - stats["attrs_written"] += 1 - - # 4. ScorePartwise.h / .cpp - # Forward declares in ScorePartwise.h come out as - # [PartwisePart, ScoreHeaderGroup, ScorePartwise] - # which is sorted lexicographically. - fwd_order = sorted([inner_class, header_group_cls, outer_class]) - # The score-header group's child element XML names are the children that - # the outer fromXElementImpl should silently 'continue' past (they were - # already consumed by ScoreHeaderGroup). Schema-driven from model.groups. - header_skip_names = [c.element_name for c in model.groups.get(s["header_group"], [])] - h = _generate_score_outer_h(outer_class, outer_attrs_name, - header_group_cls, inner_class, fwd_order) - c = _generate_score_outer_cpp(elem_name, outer_class, outer_attrs_name, - header_group_cls, inner_class, - s["inner_name"], header_skip_names, - flavor_cfg) - write_file(os.path.join(ELEM_DIR, f"{outer_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{outer_class}.cpp"), c) - - # 5. PartwisePart.h / .cpp - h = _generate_partwise_part_h(inner_class, inner_attrs_name, deepest_class) - c = _generate_partwise_part_cpp(s["inner_name"], inner_class, - inner_attrs_name, deepest_class, - s["deepest_name"], flavor_cfg) - write_file(os.path.join(ELEM_DIR, f"{inner_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{inner_class}.cpp"), c) - - # 6. PartwiseMeasure.h / .cpp - # Build the MusicDataChoice schema-driven from model.groups["music-data"]. - # The music-data group is an xs:choice; _parse_groups would store the - # choice children directly. Walk model.root to be safe. - md_group = s["deepest_content_group"] # "music-data" - music_data_choices = [] - for group_node in model.root.iter(f"{XS}group"): - if group_node.get("name") == md_group: - choice_node = None - seq_node = group_node.find(f"{XS}sequence") - if seq_node is not None: - choice_node = seq_node.find(f"{XS}choice") - if choice_node is None: - choice_node = group_node.find(f"{XS}choice") - if choice_node is None: - raise RuntimeError(f"group '{md_group}' has no xs:choice") - for el in choice_node.findall(f"{XS}element"): - xml_n = el.get("name") or el.get("ref") - cls = element_class_name(xml_n) - enum_n = pascal_to_camel(cls) - music_data_choices.append({ - "xml_name": xml_n, - "class_name": cls, - "enum_name": enum_n, - }) - break - - h = _generate_partwise_measure_h(deepest_class, deepest_attrs_name, - music_data_group_cls, flavor_cfg) - c = _generate_partwise_measure_cpp(s["deepest_name"], deepest_class, - deepest_attrs_name, - music_data_group_cls, - music_data_choices, - flavor_cfg) - write_file(os.path.join(ELEM_DIR, f"{deepest_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{deepest_class}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -# --------------------------------------------------------------------------- -# Note (bespoke, schema-driven) -# -# note's complex type is structurally unique. It has: -# -# sequence: -# choice: -> NoteChoice (synthetic) -# sequence(grace, full-note group, tie*) -> GraceNoteGroup (synthetic) -# sequence(cue, full-note group, duration) -> CueNoteGroup (synthetic) -# sequence(full-note group, duration, tie*)-> NormalNoteGroup(synthetic) -# <16 trailing children: instrument? editorial-voice group type? dot* ...> -# -# The full-note group is itself complex: -# -# sequence: -# chord? -# choice(pitch | unpitched | rest) -> FullNoteTypeChoice (synth) -# -# Note also has a 19-attribute NoteAttributes struct. -# -# The committed code uses MX_MUTEX / MX_LOCK / MX_JIT_ALLOCATE in Note (the -# only top-level element with lazy allocation) and in FullNoteGroup + -# FullNoteTypeChoice. The branch groups (Grace/Cue/Normal) eagerly construct -# all members. -# -# Note::fromXElementImpl is a hand-written state machine with three private -# helpers: parseNoteChoice, parseFullNoteGroup, parseEditorialVoiceGroup. All -# six synthetic helper classes use MX_FROM_XELEMENT_UNUSED -- Note owns the -# parse entirely. -# -# Schema-driven inputs (everything that should propagate when the XSD changes): -# - The 3 outer-choice branches and their member sequences (model.elements, -# content_tree). -# - The trailing children (instrument, type, dot, ..., play). -# - The full-note group's inner choice members (pitch/unpitched/rest) and -# pre-choice element (chord). -# - The editorial-voice group's members (footnote/level/voice). -# - The duration group's single element name. -# - The staff group's single element name. -# - The note attributes (model.complex_types['note'].attributes). -# -# Hand-naming decisions (not in the XSD): -# - The choice class name is 'NoteChoice'. -# - The third (no-leading-element) branch's group class is 'NormalNoteGroup' -# and its enum variant is 'normal'. Stored as NOTE_THIRD_BRANCH_*. -# - The full-note inner choice class is 'FullNoteTypeChoice'. - - -# Naming decision for the third outer-choice branch (the one without a -# leading singleton element ref). The XSD does not give this branch a name; -# the original codegen called it 'Normal'. -NOTE_THIRD_BRANCH_CLASS = "NormalNoteGroup" -NOTE_THIRD_BRANCH_ENUM = "normal" - - -def _extract_note_structure(ct, model): - """Return a structured dict describing note's content tree. - - Returns: - { - "branches": [BranchInfo, BranchInfo, BranchInfo], - "trailing": [(kind, name, min, max), ...], # outer-sequence trailing - "full_note": { - "pre": ["chord"], # element refs before the inner choice - "choice_branches": ["pitch", "unpitched", "rest"], - }, - "editorial_voice": ["footnote", "level", "voice"], - "duration": "duration", - "staff": "staff", - } - - BranchInfo is a dict: - { - "class_name": "GraceNoteGroup", - "enum_name": "grace", - "leading_element": "grace" or None, # the singleton element name - "members": [(kind, name, min, max), ...] # all members in order - "tie_max": int or 0, - "has_full_note": bool, - "has_duration": bool, - } - """ - seq = ct.content_tree - if not isinstance(seq, SequenceNode): - raise RuntimeError(f"note: expected outer SequenceNode, got {type(seq).__name__}") - if not seq.children or not isinstance(seq.children[0], ChoiceNode): - raise RuntimeError("note: first child of outer sequence must be a ChoiceNode") - - outer_choice = seq.children[0] - - branches = [] - for branch in outer_choice.branches: - if not isinstance(branch, SequenceNode): - raise RuntimeError( - f"note: outer choice branch must be SequenceNode, got {type(branch).__name__}") - members = [] - leading_element = None - tie_max = 0 - has_full_note = False - has_duration = False - for c in branch.children: - if isinstance(c, ElementRefNode): - members.append(("element", c.element_name, c.min_occurs, c.max_occurs)) - if leading_element is None and not members[:-1]: - leading_element = c.element_name - if c.element_name == "tie": - tie_max = c.max_occurs - elif isinstance(c, GroupRefNode): - members.append(("group", c.group_name, c.min_occurs, c.max_occurs)) - if c.group_name == "full-note": - has_full_note = True - elif c.group_name == "duration": - has_duration = True - - if leading_element == "grace": - cls = "GraceNoteGroup" - enum_n = "grace" - elif leading_element == "cue": - cls = "CueNoteGroup" - enum_n = "cue" - else: - cls = NOTE_THIRD_BRANCH_CLASS - enum_n = NOTE_THIRD_BRANCH_ENUM - branches.append({ - "class_name": cls, - "enum_name": enum_n, - "leading_element": leading_element, - "members": members, - "tie_max": tie_max, - "has_full_note": has_full_note, - "has_duration": has_duration, - }) - - trailing = [] - for c in seq.children[1:]: - if isinstance(c, ElementRefNode): - trailing.append(("element", c.element_name, c.min_occurs, c.max_occurs)) - elif isinstance(c, GroupRefNode): - trailing.append(("group", c.group_name, c.min_occurs, c.max_occurs)) - - # full-note group: walk model.root because _parse_groups drops the inner choice. - fn_pre = [] - fn_choice = [] - for g in model.root.iter(f"{XS}group"): - if g.get("name") != "full-note": - continue - sq = g.find(f"{XS}sequence") - if sq is None: - raise RuntimeError("full-note group missing xs:sequence") - for child in sq: - tag = child.tag.split("}")[-1] - if tag == "element": - fn_pre.append(child.get("name") or child.get("ref")) - elif tag == "choice": - for el in child.findall(f"{XS}element"): - fn_choice.append(el.get("name") or el.get("ref")) - break - - ev = [c.element_name for c in model.groups.get("editorial-voice", [])] - dur_elems = [c.element_name for c in model.groups.get("duration", [])] - staff_elems = [c.element_name for c in model.groups.get("staff", [])] - - return { - "branches": branches, - "trailing": trailing, - "full_note": {"pre": fn_pre, "choice_branches": fn_choice}, - "editorial_voice": ev, - "duration": dur_elems[0] if dur_elems else "duration", - "staff": staff_elems[0] if staff_elems else "staff", - } - - -# --------------------------------------------------------------------------- -# FullNoteTypeChoice (synthetic choice over pitch | unpitched | rest) -# --------------------------------------------------------------------------- - -def generate_full_note_type_choice_h(class_name, branch_names): - """branch_names: ordered list of XSD element names, e.g. ['pitch','unpitched','rest']""" - branch_classes = [pascal(b) for b in branch_names] - fwds = sorted(set(branch_classes)) + [class_name] - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, b in enumerate(branch_names): - comma = "," if i < len(branch_names) - 1 else "" - lines.append(f" {camel(b)} = {i + 1}{comma}") - lines.append(" };") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(" /* _________ Choice minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {class_name}::Choice getChoice() const;") - lines.append(f" void setChoice(const {class_name}::Choice value);") - for b, bc in zip(branch_names, branch_classes): - lines.append("") - lines.append(f" /* _________ {bc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {bc}Ptr get{bc}() const;") - lines.append(f" void set{bc}(const {bc}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" MX_MUTEX") - lines.append(" Choice myChoice;") - for bc in branch_classes: - lines.append(f" mutable {bc}Ptr my{bc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_full_note_type_choice_cpp(class_name, branch_names): - branch_classes = [pascal(b) for b in branch_names] - includes = sorted(set([f"mx/core/elements/{bc}.h" for bc in branch_classes])) - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in includes: - lines.append(f'#include "{inc}"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - # Constructor - inits = [f"myChoice(Choice::{camel(branch_names[0])})"] - for bc in branch_classes: - inits.append(f"my{bc}(make{bc}())") - lines.append(f"{class_name}::{class_name}()") - lines.append(f" : {', '.join(inits)}") - lines.append("{") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append("") - lines.append(" MX_UNUSED(indentLevel);") - lines.append(" MX_UNUSED(isOneLineOnly);") - lines.append("") - lines.append(" switch (myChoice)") - lines.append(" {") - for b, bc in zip(branch_names, branch_classes): - lines.append(f" case Choice::{camel(b)}:") - lines.append(f" get{bc}()->toStream(os, indentLevel);") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{class_name}::Choice {class_name}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setChoice(const {class_name}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}") - for bc in branch_classes: - lines.append("") - lines.append(f"{bc}Ptr {class_name}::get{bc}() const") - lines.append("{") - lines.append(" MX_LOCK") - lines.append(f" MX_JIT_ALLOCATE({bc});") - lines.append(f" return my{bc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{bc}(const {bc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{bc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# FullNoteGroup (chord? + FullNoteTypeChoice) -# --------------------------------------------------------------------------- - -def generate_full_note_group_h(class_name, pre_elems, inner_choice_cls): - """pre_elems: list of element names before the inner choice (e.g. ['chord'])""" - fwds = sorted(set([pascal(e) for e in pre_elems] + [inner_choice_cls])) + [class_name] - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - for e in pre_elems: - ec = pascal(e) - lines.append("") - lines.append(f" /* _________ {ec} minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(f" {ec}Ptr get{ec}() const;") - lines.append(f" void set{ec}(const {ec}Ptr &value);") - lines.append(f" bool getHas{ec}() const;") - lines.append(f" void setHas{ec}(const bool value);") - lines.append("") - lines.append(f" /* _________ {inner_choice_cls} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {inner_choice_cls}Ptr get{inner_choice_cls}() const;") - lines.append(f" void set{inner_choice_cls}(const {inner_choice_cls}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" MX_MUTEX") - for e in pre_elems: - ec = pascal(e) - lines.append(f" {ec}Ptr my{ec};") - lines.append(f" bool myHas{ec};") - lines.append(f" mutable {inner_choice_cls}Ptr my{inner_choice_cls};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_full_note_group_cpp(class_name, pre_elems, inner_choice_cls): - includes = sorted(set([f"mx/core/elements/{pascal(e)}.h" for e in pre_elems] + - [f"mx/core/elements/{inner_choice_cls}.h"])) - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in includes: - lines.append(f'#include "{inc}"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - inits = [] - for e in pre_elems: - ec = pascal(e) - inits.append(f"my{ec}(make{ec}())") - inits.append(f"myHas{ec}(false)") - inits.append(f"my{inner_choice_cls}(nullptr)") - lines.append(f"{class_name}::{class_name}() : {', '.join(inits)}") - lines.append("{") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - for e in pre_elems: - ec = pascal(e) - lines.append(f" if (myHas{ec})") - lines.append(" {") - lines.append(f" my{ec}->toStream(os, indentLevel);") - lines.append(" os << std::endl;") - lines.append(" }") - lines.append(f" get{inner_choice_cls}()->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - for e in pre_elems: - ec = pascal(e) - lines.append("") - lines.append(f"{ec}Ptr {class_name}::get{ec}() const") - lines.append("{") - lines.append(f" return my{ec};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{ec}(const {ec}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{ec} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{ec}() const") - lines.append("{") - lines.append(f" return myHas{ec};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{ec}(const bool value)") - lines.append("{") - lines.append(f" myHas{ec} = value;") - lines.append("}") - lines.append("") - lines.append(f"{inner_choice_cls}Ptr {class_name}::get{inner_choice_cls}() const") - lines.append("{") - lines.append(" MX_LOCK") - lines.append(f" MX_JIT_ALLOCATE({inner_choice_cls});") - lines.append(f" return my{inner_choice_cls};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{inner_choice_cls}(const {inner_choice_cls}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{inner_choice_cls} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Outer-choice branch group (Grace/Cue/Normal) -# Each branch is a sequence of element/group refs. The committed branch -# classes share a uniform shape: each member is either a singleton element -# (chord-like: get/set/has) or a set (Tie, max=2 with size cap). -# --------------------------------------------------------------------------- - -def generate_outer_branch_group_h(class_name, members, full_note_cls): - """members: list of (kind, name, min, max). - 'group' members with name 'full-note' refer to FullNoteGroup; - 'group' members with name 'duration' refer to a single Duration element.""" - fwds = set() - decls = [] # tuple-of-decl-lines for ordered member emission - privates = [] - for kind, name, mn, mx in members: - if kind == "group" and name == "full-note": - fwds.add(full_note_cls) - decls.append([ - "", - f" /* _________ {full_note_cls} minOccurs = 1, maxOccurs = 1 _________ */", - f" {full_note_cls}Ptr get{full_note_cls}() const;", - f" void set{full_note_cls}(const {full_note_cls}Ptr &value);", - ]) - privates.append(f" {full_note_cls}Ptr my{full_note_cls};") - elif kind == "group" and name == "duration": - cc = "Duration" - fwds.add(cc) - decls.append([ - "", - f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */", - f" {cc}Ptr get{cc}() const;", - f" void set{cc}(const {cc}Ptr &value);", - ]) - privates.append(f" {cc}Ptr my{cc};") - elif kind == "element": - cc = pascal(name) - fwds.add(cc) - if mx != 1: - # Set member (e.g. tie maxOccurs=2) - mx_str = "unbounded" if mx == -1 else str(mx) - decls.append([ - "", - f" /* _________ {cc} minOccurs = {mn}, maxOccurs = {mx_str} _________ */", - f" const {cc}Set &get{cc}Set() const;", - f" void add{cc}(const {cc}Ptr &value);", - f" void remove{cc}(const {cc}SetIterConst &value);", - f" void clear{cc}Set();", - f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;", - ]) - privates.append(f" {cc}Set my{cc}Set;") - elif mn == 0: - decls.append([ - "", - f" /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */", - f" {cc}Ptr get{cc}() const;", - f" void set{cc}(const {cc}Ptr &value);", - f" bool getHas{cc}() const;", - f" void setHas{cc}(const bool value);", - ]) - privates.append(f" {cc}Ptr my{cc};") - privates.append(f" bool myHas{cc};") - else: - decls.append([ - "", - f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */", - f" {cc}Ptr get{cc}() const;", - f" void set{cc}(const {cc}Ptr &value);", - ]) - privates.append(f" {cc}Ptr my{cc};") - - fwd_list = sorted(fwds) + [class_name] - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwd_list: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - for d in decls: - for ln in d: - lines.append(ln) - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - for p in privates: - lines.append(p) - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_outer_branch_group_cpp(class_name, members, full_note_cls): - """Stream contents: walk members in order. Singletons stream directly - (no os<") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - - # Constructor inits - inits = [] - for kind, name, mn, mx in members: - if kind == "group" and name == "full-note": - inits.append(f"my{full_note_cls}(make{full_note_cls}())") - elif kind == "group" and name == "duration": - inits.append("myDuration(makeDuration())") - elif kind == "element": - cc = pascal(name) - if mx != 1: - inits.append(f"my{cc}Set()") - elif mn == 0: - inits.append(f"my{cc}(make{cc}())") - inits.append(f"myHas{cc}(false)") - else: - inits.append(f"my{cc}(make{cc}())") - lines.append(f"{class_name}::{class_name}() : {', '.join(inits)}") - lines.append("{") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - # Walk members in order, producing the committed streamContents pattern. - has_set = False - for i, (kind, name, mn, mx) in enumerate(members): - is_first = (i == 0) - if kind == "group" and name == "full-note": - # FullNoteGroup -> streamContents (not toStream) because it's a synthetic group - if not is_first: - lines.append(" os << std::endl;") - lines.append(f" my{full_note_cls}->streamContents(os, indentLevel, isOneLineOnly);") - else: - lines.append(f" my{full_note_cls}->streamContents(os, indentLevel, isOneLineOnly);") - elif kind == "group" and name == "duration": - if not is_first: - lines.append(" os << std::endl;") - lines.append(" myDuration->toStream(os, indentLevel);") - elif kind == "element": - cc = pascal(name) - if mx != 1: - # Set - has_set = True - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel);") - lines.append(" }") - else: - if not is_first: - lines.append(" os << std::endl;") - lines.append(f" my{cc}->toStream(os, indentLevel);") - # The committed Grace/Cue cases emit os< 0 else None - lines.append("") - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - if cap is not None: - lines.append(f" if (my{cc}Set.size() < {cap})") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - else: - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}") - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}") - elif mn == 0: - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}") - else: - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - - lines.append("") - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# NoteChoice (3-branch choice over the synthetic outer-branch groups) -# Members are eagerly constructed (not JIT). Mirrors committed. -# --------------------------------------------------------------------------- - -def generate_note_choice_h(class_name, branches): - """branches: list of dicts (from _extract_note_structure) with class_name + enum_name.""" - branch_classes = [b["class_name"] for b in branches] - fwds = sorted(set(branch_classes)) + [class_name] - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - for f in fwds: - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(" enum class Choice") - lines.append(" {") - for i, b in enumerate(branches): - comma = "," if i < len(branches) - 1 else "" - lines.append(f" {b['enum_name']} = {i + 1}{comma}") - lines.append(" };") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append("") - lines.append(" /* _________ Choice minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {class_name}::Choice getChoice() const;") - lines.append(f" void setChoice(const {class_name}::Choice value);") - for b in branches: - bc = b["class_name"] - lines.append("") - lines.append(f" /* _________ {bc} minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(f" {bc}Ptr get{bc}() const;") - lines.append(f" void set{bc}(const {bc}Ptr &value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" Choice myChoice;") - for bc in branch_classes: - lines.append(f" {bc}Ptr my{bc};") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_note_choice_cpp(class_name, branches): - branch_classes = [b["class_name"] for b in branches] - incs = sorted(set(branch_classes)) - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in incs: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - # Default choice is the LAST branch (the no-leading-element branch). - # The committed code defaults to Choice::normal which is branch index 2. - default_branch = branches[-1] - inits = [f"myChoice(Choice::{default_branch['enum_name']})"] - for bc in branch_classes: - inits.append(f"my{bc}(make{bc}())") - lines.append(f"{class_name}::{class_name}()") - lines.append(f" : {', '.join(inits)}") - lines.append("{") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return false;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" switch (myChoice)") - lines.append(" {") - for b in branches: - lines.append(f" case Choice::{b['enum_name']}:") - lines.append(f" my{b['class_name']}->streamContents(os, indentLevel, isOneLineOnly);") - lines.append(" break;") - lines.append(" default:") - lines.append(" break;") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{class_name}::Choice {class_name}::getChoice() const") - lines.append("{") - lines.append(" return myChoice;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setChoice(const {class_name}::Choice value)") - lines.append("{") - lines.append(" myChoice = value;") - lines.append("}") - for bc in branch_classes: - lines.append("") - lines.append(f"{bc}Ptr {class_name}::get{bc}() const") - lines.append("{") - lines.append(f" return my{bc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{bc}(const {bc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{bc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"MX_FROM_XELEMENT_UNUSED({class_name});") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Note.h / Note.cpp -# -# Note is a TREE-style flat container, but with: -# - MX_MUTEX + JIT-allocated singleton members (only top-level element with this) -# - A custom fromXElementImpl that dispatches by element name into three -# private parse helpers: parseNoteChoice, parseFullNoteGroup, -# parseEditorialVoiceGroup. -# --------------------------------------------------------------------------- - -def _note_member_info(kind, name, mn, mx): - """Return (display_cls, is_set, is_optional, is_singleton_required, member_name). - - For 'group' members the class is the synthetic group class - (EditorialVoiceGroup, Staff has just 'staff' element so it uses the - Staff element class directly via group_ref mapping). Returns None for - members that should be skipped. - """ - if kind == "element": - cc = pascal(name) - is_set = (mx != 1) - is_optional = (mn == 0 and mx == 1) - return (cc, is_set, is_optional, False, name) - elif kind == "group": - if name == "editorial-voice": - return ("EditorialVoiceGroup", False, False, False, name) - elif name == "staff": - # staff group contains a single 'staff' element. Committed Note - # treats Staff as a singleton optional element directly. - return ("Staff", False, mn == 0, False, "staff") - return None - - -def generate_note_h(class_name, attrs_name, choice_class, trailing, model): - """Generate Note.h. attrs_name is e.g. 'NoteAttributes'. choice_class is - 'NoteChoice'. trailing is the list of (kind,name,mn,mx) from the outer - sequence after the outer choice.""" - fwds = set([attrs_name, choice_class, "FullNoteGroup"]) - member_decls = [] - member_privates = [] - - # First member is always the NoteChoice - member_decls.append([ - "", - f" /* _________ {choice_class} minOccurs = 1, maxOccurs = 1 _________ */", - f" {choice_class}Ptr get{choice_class}() const;", - f" void set{choice_class}(const {choice_class}Ptr &value);", - ]) - member_privates.append([f"mutable {choice_class}Ptr my{choice_class};"]) - - for kind, name, mn, mx in trailing: - info = _note_member_info(kind, name, mn, mx) - if info is None: - continue - cc, is_set, is_optional, _, _ = info - fwds.add(cc) - if is_set: - mx_str = "unbounded" if mx == -1 else str(mx) - member_decls.append([ - "", - f" /* _________ {cc} minOccurs = {mn}, maxOccurs = {mx_str} _________ */", - f" const {cc}Set &get{cc}Set() const;", - f" void add{cc}(const {cc}Ptr &value);", - f" void remove{cc}(const {cc}SetIterConst &value);", - f" void clear{cc}Set();", - f" {cc}Ptr get{cc}(const {cc}SetIterConst &setIterator) const;", - ]) - member_privates.append([f"{cc}Set my{cc}Set;"]) - elif kind == "group" and name == "editorial-voice": - # Required group (treated as min=1 max=1) - member_decls.append([ - "", - f" /* _________ {cc} minOccurs = 1, maxOccurs = 1 _________ */", - f" {cc}Ptr get{cc}() const;", - f" void set{cc}(const {cc}Ptr &value);", - ]) - member_privates.append([f"mutable {cc}Ptr my{cc};"]) - elif is_optional: - member_decls.append([ - "", - f" /* _________ {cc} minOccurs = 0, maxOccurs = 1 _________ */", - f" {cc}Ptr get{cc}() const;", - f" void set{cc}(const {cc}Ptr &value);", - f" bool getHas{cc}() const;", - f" void setHas{cc}(const bool value);", - ]) - member_privates.append([ - f"mutable {cc}Ptr my{cc};", - f"bool myHas{cc};", - ]) - - fwd_list = sorted(fwds) + [class_name] - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace ezxml") - lines.append("{") - lines.append("class XElementIterator;") - lines.append("}") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - # Attributes forward declare is a struct, not element - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - for f in fwd_list: - if f == attrs_name: - continue - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({f})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - for d in member_decls: - for ln in d: - lines.append(ln) - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(" MX_MUTEX") - lines.append(f" mutable {attrs_name}Ptr myAttributes;") - for grp in member_privates: - for ln in grp: - lines.append(f" {ln}") - lines.append("") - # The three private parse helpers - lines.append(f" bool parseNoteChoice(std::ostream &message, ::ezxml::XElement ¬eElement, ::ezxml::XElementIterator &iter);") - lines.append("") - lines.append(f" bool parseFullNoteGroup(std::ostream &message, ::ezxml::XElement ¬eElement, ::ezxml::XElementIterator &iter,") - lines.append(" FullNoteGroupPtr &outFullNoteGroup);") - lines.append("") - lines.append(f" bool parseEditorialVoiceGroup(std::ostream &message, ::ezxml::XElement ¬eElement,") - lines.append(" ::ezxml::XElementIterator &iter);") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -def generate_note_cpp(class_name, attrs_name, choice_class, trailing, - full_note_pre, full_note_choice, editorial_voice_members, - branches, duration_elem, model): - """Big one. Generates: - - Includes - - Constructor with init list - - hasAttributes/streamAttributes/streamName/hasContents - - streamContents (walk trailing members in order) - - All getters/setters/etc. - - fromXElementImpl: dispatch by element name (schema-driven) - - parseNoteChoice helper - - parseFullNoteGroup helper - - parseEditorialVoiceGroup helper - """ - - # ---- Compute member info table for trailing ---- - members = [] # ordered list of dicts - for kind, name, mn, mx in trailing: - info = _note_member_info(kind, name, mn, mx) - if info is None: - continue - cc, is_set, is_optional, _, xml_name = info - is_required_group = (kind == "group" and name == "editorial-voice") - members.append({ - "cls": cc, - "xml_name": xml_name, - "is_set": is_set, - "is_optional": is_optional, - "is_group": is_required_group, - "is_unbounded": (mx == -1), - "max_occurs": mx, - "kind": kind, - "group_name": name if kind == "group" else None, - }) - - # ---- Includes ---- - incs = set([attrs_name, choice_class, "FullNoteGroup", "FullNoteTypeChoice"]) - incs |= set([b["class_name"] for b in branches]) - # Include all member class headers - for m in members: - incs.add(m["cls"]) - # Note's fromXElementImpl references the inner-choice element classes - # via the FullNoteGroup parse. Include them so the .cpp can call e.g. - # outFullNoteGroup->getFullNoteTypeChoice()->getPitch()->fromXElement. - for fc in full_note_choice: - incs.add(pascal(fc)) - # FullNoteGroup's pre-elements (chord) also referenced for setHasChord. - for pe in full_note_pre: - incs.add(pascal(pe)) - # Tie element class for the parseNoteChoice helper. - incs.add("Tie") - # Cue / Grace element classes referenced by parseNoteChoice. - for b in branches: - if b["leading_element"]: - incs.add(pascal(b["leading_element"])) - # Duration class referenced in parseNoteChoice helper. - incs.add(pascal(duration_elem)) - # Editorial-voice group's element classes referenced in parseEditorialVoiceGroup. - for evn in editorial_voice_members: - incs.add(pascal(evn)) - - inc_list = sorted(incs) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - for inc in inc_list: - lines.append(f'#include "mx/core/elements/{inc}.h"') - lines.append("#include ") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{") - - # ---- Constructor ---- - # The committed Note ctor inits myAttributes(nullptr), myNoteChoice(nullptr), - # then for each trailing member: ptr(nullptr) for singletons (with myHasX(false) - # if optional), or default ctor for sets, or required-group ptr(nullptr). - inits = ["myAttributes(nullptr)", f"my{choice_class}(nullptr)"] - for m in members: - cc = m["cls"] - if m["is_set"]: - inits.append(f"my{cc}Set()") - elif m["is_group"]: - inits.append(f"my{cc}(nullptr)") - elif m["is_optional"]: - inits.append(f"my{cc}(nullptr)") - inits.append(f"myHas{cc}(false)") - - lines.append(f"{class_name}::{class_name}()") - # Wrap inits across multiple lines like committed (we'll let make fmt - # normalize the line breaking). - lines.append(f" : {', '.join(inits)}") - lines.append("{") - lines.append("}") - lines.append("") - - # hasAttributes / streamAttributes - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return getAttributes()->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return getAttributes()->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(' os << "note";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - - # streamContents -- the committed pattern: - # os << std::endl; (always at start) - # getNoteChoice()->streamContents(...); - # for each trailing member in order: - # - singleton optional: if (myHasX) { os<toStream(os, indentLevel+1); } - # - required group: if (getX()->hasContents()) { os<streamContents(os, indentLevel+1, isOneLineOnly); } - # - set: for (auto x : myXSet) { os<toStream(os, indentLevel+1); } - # isOneLineOnly = false; os << std::endl; return os; - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" os << std::endl;") - lines.append(f" get{choice_class}()->streamContents(os, indentLevel + 1, isOneLineOnly);") - for m in members: - cc = m["cls"] - if m["is_set"]: - lines.append(f" for (auto x : my{cc}Set)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - elif m["is_group"]: - lines.append(f" if (get{cc}()->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" get{cc}()->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - elif m["is_optional"]: - lines.append(f" if (myHas{cc})") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(f" get{cc}()->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" isOneLineOnly = false;") - lines.append(" os << std::endl;") - lines.append(" return os;") - lines.append("}") - lines.append("") - - # ---- Attribute getters/setters ---- - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" MX_LOCK;") - lines.append(f" MX_JIT_ALLOCATE_ATTRIBUTES({attrs_name});") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # ---- NoteChoice getter/setter ---- - lines.append(f"{choice_class}Ptr {class_name}::get{choice_class}() const") - lines.append("{") - lines.append(" MX_LOCK;") - lines.append(f" MX_JIT_ALLOCATE({choice_class});") - lines.append(f" return my{choice_class};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{choice_class}(const {choice_class}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{choice_class} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - - # ---- Trailing member getters/setters ---- - for m in members: - cc = m["cls"] - if m["is_set"]: - mx = m["max_occurs"] - lines.append(f"const {cc}Set &{class_name}::get{cc}Set() const") - lines.append("{") - lines.append(f" return my{cc}Set;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::remove{cc}(const {cc}SetIterConst &value)") - lines.append("{") - lines.append(f" if (value != my{cc}Set.cend())") - lines.append(" {") - lines.append(f" my{cc}Set.erase(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::add{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - if mx > 0: - lines.append(f" if (my{cc}Set.size() < {mx})") - lines.append(" {") - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - else: - lines.append(f" my{cc}Set.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clear{cc}Set()") - lines.append("{") - lines.append(f" my{cc}Set.clear();") - lines.append("}") - lines.append("") - lines.append(f"{cc}Ptr {class_name}::get{cc}(const {cc}SetIterConst &setIterator) const") - lines.append("{") - lines.append(f" if (setIterator != my{cc}Set.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(f" return {cc}Ptr();") - lines.append("}") - lines.append("") - elif m["is_group"]: - # JIT-allocated group - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(" MX_LOCK;") - lines.append(f" MX_JIT_ALLOCATE({cc});") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - elif m["is_optional"]: - lines.append(f"{cc}Ptr {class_name}::get{cc}() const") - lines.append("{") - lines.append(" MX_LOCK;") - lines.append(f" MX_JIT_ALLOCATE({cc});") - lines.append(f" return my{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cc}(const {cc}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" my{cc} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::getHas{cc}() const") - lines.append("{") - lines.append(f" return myHas{cc};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cc}(const bool value)") - lines.append("{") - lines.append(f" myHas{cc} = value;") - lines.append("}") - lines.append("") - - # ============================================================ - # fromXElementImpl - # ============================================================ - # Build the dispatch list -- schema-driven from: - # - the outer choice branches' "starter" element names (the elements - # that signal "this is a NoteChoice" iteration): the leading element - # of each branch (grace/cue) PLUS the elements that introduce the - # full-note group (chord + full-note-choice members) -- collectively - # the set of "note-choice elements". - # - editorial-voice group member names - # - trailing element names (instrument, type, dot, ...) - # - # The dispatch ordering matches the committed code's hand-written order. - # In committed: pitch | unpitched | rest | chord | grace | cue (note-choice) - # Then trailing in declared XSD order: - # instrument, editorial-voice, type, dot, accidental, time-modification, - # stem, notehead, notehead-text, staff, beam, notations, lyric, play. - - note_choice_elements = [] # element XML names that trigger parseNoteChoice - note_choice_elements.extend(full_note_choice) # pitch, unpitched, rest - note_choice_elements.extend(full_note_pre) # chord - for b in branches: - if b["leading_element"]: - note_choice_elements.append(b["leading_element"]) - - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" bool isNoteChoiceFound = false;") - # For each set member with maxOccurs != 1, we need a "first added" flag - # that handles the "default-constructed singleton already in the set" - # idiom that the committed code uses. The set is initialized empty - # (myDotSet()) so the size==1 branch actually never fires here. But - # the committed code does have that pattern -- emit it for parity. - first_added_flags = [] - for m in members: - if m["is_set"]: - flag = f"isFirst{m['cls']}Added" - first_added_flags.append((flag, m["cls"])) - lines.append(f" bool {flag} = false;") - lines.append("") - lines.append(" isSuccess &= getAttributes()->fromXElement(message, xelement);") - lines.append("") - lines.append(" for (auto it = xelement.begin(); it != xelement.end(); ++it)") - lines.append(" {") - lines.append(" const std::string elementName = it->getName();") - lines.append("") - - # First branch: note-choice elements (handled by parseNoteChoice with --it). - nc_cond = " || ".join([f'elementName == "{n}"' for n in note_choice_elements]) - lines.append(f" if ({nc_cond})") - lines.append(" {") - lines.append(" isNoteChoiceFound = true;") - lines.append(" isSuccess &= parseNoteChoice(message, xelement, it);") - lines.append(" --it;") - lines.append(" }") - - # Then walk trailing members in order, emitting if/else if branches. - for m in members: - cls = m["cls"] - if m["is_set"]: - xml_n = m["xml_name"] - lines.append(f' else if (elementName == "{xml_n}")') - lines.append(" {") - lines.append(f" auto {camel(xml_n)} = make{cls}();") - lines.append(f" isSuccess &= {camel(xml_n)}->fromXElement(message, *it);") - lines.append("") - lines.append(f" if (!isFirst{cls}Added && my{cls}Set.size() == 1)") - lines.append(" {") - lines.append(f" *(my{cls}Set.begin()) = {camel(xml_n)};") - lines.append(f" isFirst{cls}Added = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" my{cls}Set.push_back({camel(xml_n)});") - lines.append(f" isFirst{cls}Added = true;") - lines.append(" }") - lines.append(" }") - elif m["is_group"]: - # editorial-voice: dispatch to parseEditorialVoiceGroup - ev_cond = " || ".join([f'elementName == "{n}"' for n in editorial_voice_members]) - lines.append(f" else if ({ev_cond})") - lines.append(" {") - lines.append(" isSuccess &= parseEditorialVoiceGroup(message, xelement, it);") - lines.append(" }") - elif m["is_optional"]: - xml_n = m["xml_name"] - lines.append(f' else if (elementName == "{xml_n}")') - lines.append(" {") - lines.append(f" myHas{cls} = true;") - lines.append(f" isSuccess &= get{cls}()->fromXElement(message, *it);") - lines.append(" }") - - lines.append(" else") - lines.append(" {") - lines.append(" isSuccess = false;") - lines.append(f' message << "Note: unexpected element \'" << elementName << "\'" << std::endl;') - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" if (!isNoteChoiceFound)") - lines.append(" {") - lines.append(" isSuccess = false;") - lines.append(f' message << "Note: \'note-choice\' elements were required but not found" << std::endl;') - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - - # ============================================================ - # parseNoteChoice - # ============================================================ - # Dispatch on the first child element name: - # - "grace" -> setChoice(grace); parse Grace; fullNoteGroup = getGrace().getFullNoteGroup; ++iter - # - "cue" -> setChoice(cue); parse Cue; fullNoteGroup = getCue().getFullNoteGroup; ++iter - # - else -> setChoice(normal); fullNoteGroup = getNormal().getFullNoteGroup - # Then parseFullNoteGroup(message, noteElement, iter, fullNoteGroup). - # Then if NOT grace, expect 'duration'; assign via the right branch. - # Then if NOT cue, parse tie* (assigning via the right branch). - lines.append(f"bool {class_name}::parseNoteChoice(std::ostream &message, ::ezxml::XElement ¬eElement, ::ezxml::XElementIterator &iter)") - lines.append("{") - lines.append(" if (iter == noteElement.end())") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(" bool isSuccess = true;") - lines.append("") - lines.append(" const std::string elementName = iter->getName();") - lines.append(" FullNoteGroupPtr fullNoteGroup = nullptr;") - lines.append("") - - # Find normal branch (no leading element) and the named branches. - named_branches = [b for b in branches if b["leading_element"]] - normal_branch = next((b for b in branches if not b["leading_element"]), None) - if normal_branch is None: - raise RuntimeError("Note: no third (no-leading-element) branch found") - - for i, b in enumerate(named_branches): - le = b["leading_element"] - le_cls = pascal(le) - bg = b["class_name"] - prefix = "if" if i == 0 else "else if" - lines.append(f' {prefix} (elementName == "{le}")') - lines.append(" {") - lines.append(f" get{choice_class}()->setChoice({choice_class}::Choice::{b['enum_name']});") - lines.append(f" isSuccess &= get{choice_class}()->get{bg}()->get{le_cls}()->fromXElement(message, *iter);") - lines.append(f" fullNoteGroup = get{choice_class}()->get{bg}()->getFullNoteGroup();") - lines.append(" ++iter;") - lines.append(" }") - - nbg = normal_branch["class_name"] - lines.append(" else") - lines.append(" {") - lines.append(f" get{choice_class}()->setChoice({choice_class}::Choice::{normal_branch['enum_name']});") - lines.append(f" fullNoteGroup = get{choice_class}()->get{nbg}()->getFullNoteGroup();") - lines.append(" }") - lines.append("") - lines.append(" // we should now be pointing at the full note group") - lines.append(" isSuccess &= parseFullNoteGroup(message, noteElement, iter, fullNoteGroup);") - lines.append("") - - # Branches that require duration after the full-note group: any branch - # whose members include a duration group ref (i.e. branches "cue" and - # "normal" in MusicXML 3.0). - duration_branches = [b for b in branches if b["has_duration"]] - # Filter named branches that have duration -> "cue" - named_dur_branches = [b for b in duration_branches if b["leading_element"]] - # The committed code does: - # if (elementName != "grace") -> the only branch that has NO duration. - # We invert by listing the leading-element name that is duration-FREE. - no_dur_branches = [b for b in branches if not b["has_duration"]] - # Expecting exactly one of these (grace). - if len(no_dur_branches) == 1 and no_dur_branches[0]["leading_element"]: - no_dur_name = no_dur_branches[0]["leading_element"] - lines.append(f" // {pascal(no_dur_branches[0]['class_name'])}s do not have a duration element") - lines.append(f' if (elementName != "{no_dur_name}")') - else: - # fallback: enumerate - cond = " || ".join([f'elementName == "{b["leading_element"]}"' for b in duration_branches if b["leading_element"]]) - if not duration_branches[-1]["leading_element"]: - # Normal branch (no leading element) is reached when elementName - # is none of the named leading elements. - named_le = " && ".join([f'elementName != "{b["leading_element"]}"' for b in named_branches]) - cond = f"({cond}) || ({named_le})" - lines.append(f" if ({cond})") - lines.append(" {") - lines.append(f' if (iter == noteElement.end() || iter->getName() != "{duration_elem}")') - lines.append(" {") - lines.append(f' message << "Note: parseNoteChoice - a \'{duration_elem}\' element was required but not found" << std::endl;') - lines.append(" return false;") - lines.append(" }") - lines.append("") - # For each branch that has duration, emit: - # if (getNoteChoice()->getChoice() == NoteChoice::Choice::normal) { ... } - # else if (...cue) { ... } - for j, b in enumerate(duration_branches): - prefix = "if" if j == 0 else "else if" - bg = b["class_name"] - lines.append(f" {prefix} (get{choice_class}()->getChoice() == {choice_class}::Choice::{b['enum_name']})") - lines.append(" {") - lines.append(f" get{choice_class}()->get{bg}()->get{pascal(duration_elem)}()->fromXElement(message, *iter);") - lines.append(" }") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" // additional stuff is optional so we may be at the end iter") - lines.append(" if (iter == noteElement.end())") - lines.append(" {") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - lines.append("") - - # Tie parsing: only branches whose tie_max > 0 - tie_branches = [b for b in branches if b["tie_max"] > 0] - # Branches WITHOUT tie support -> early-return. - no_tie_branches = [b for b in branches if b["tie_max"] == 0] - - tie_cond = " || ".join( - [f"get{choice_class}()->getChoice() == {choice_class}::Choice::{b['enum_name']}" for b in tie_branches]) - lines.append(f" // now we may be pointing at tie elements, but only if the choice supports them") - lines.append(f" if ({tie_cond})") - lines.append(" {") - lines.append(" std::string possibleTieElementName = iter->getName();") - lines.append(' while (iter != noteElement.end() && iter->getName() == "tie")') - lines.append(" {") - lines.append(" auto tie = makeTie();") - lines.append(" isSuccess &= tie->fromXElement(message, *iter);") - for k, b in enumerate(tie_branches): - prefix = "if" if k == 0 else "else if" - bg = b["class_name"] - lines.append(f" {prefix} (get{choice_class}()->getChoice() == {choice_class}::Choice::{b['enum_name']})") - lines.append(" {") - lines.append(f" get{choice_class}()->get{bg}()->addTie(tie);") - lines.append(" }") - lines.append(" ++iter;") - lines.append(" }") - lines.append(" }") - for b in no_tie_branches: - lines.append(f" else if (get{choice_class}()->getChoice() == {choice_class}::Choice::{b['enum_name']})") - lines.append(" {") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - - # ============================================================ - # parseFullNoteGroup - # ============================================================ - lines.append(f"bool {class_name}::parseFullNoteGroup(std::ostream &message, ::ezxml::XElement ¬eElement, ::ezxml::XElementIterator &iter,") - lines.append(" FullNoteGroupPtr &outFullNoteGroup)") - lines.append("{") - lines.append(" if (iter == noteElement.end())") - lines.append(" {") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(" bool isSuccess = true;") - lines.append("") - # chord pre-element: if first iter name == "chord", set has chord and advance. - for pe in full_note_pre: - pe_cls = pascal(pe) - lines.append(f' if (iter->getName() == "{pe}")') - lines.append(" {") - lines.append(f" outFullNoteGroup->setHas{pe_cls}(true);") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" // we should now be pointing at the FullNoteTypeChoice") - lines.append(" if (iter == noteElement.end())") - lines.append(" {") - lines.append(' message << "Note: parseFullNoteGroup did not find the FullNoteChoice" << std::endl;') - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(" const std::string noteChoiceElementName = iter->getName();") - lines.append("") - # full_note_choice members: pitch, unpitched, rest - for k, fc in enumerate(full_note_choice): - prefix = "if" if k == 0 else "else if" - fc_cls = pascal(fc) - lines.append(f' {prefix} (noteChoiceElementName == "{fc}")') - lines.append(" {") - lines.append(f" outFullNoteGroup->getFullNoteTypeChoice()->setChoice(FullNoteTypeChoice::Choice::{camel(fc)});") - lines.append(f" isSuccess &= outFullNoteGroup->getFullNoteTypeChoice()->get{fc_cls}()->fromXElement(message, *iter);") - lines.append(" ++iter;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(' message << "Note: parseFullNoteGroup encountered unexpected element \'" << noteChoiceElementName << "\'"') - lines.append(" << std::endl;") - lines.append(" isSuccess = false;") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - - # ============================================================ - # parseEditorialVoiceGroup - # ============================================================ - lines.append(f"bool {class_name}::parseEditorialVoiceGroup(std::ostream &message, ::ezxml::XElement ¬eElement,") - lines.append(" ::ezxml::XElementIterator &iter)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" bool isIterIncremented = false;") - ev_cond = " || ".join([f'iter->getName() == "{n}"' for n in editorial_voice_members]) - lines.append(f" while (iter != noteElement.end() &&") - lines.append(f" ({ev_cond}))") - lines.append(" {") - for j, evn in enumerate(editorial_voice_members): - prefix = "if" if j == 0 else "else if" - ev_cls = pascal(evn) - lines.append(f' {prefix} (iter->getName() == "{evn}")') - lines.append(" {") - lines.append(f" getEditorialVoiceGroup()->setHas{ev_cls}(true);") - lines.append(f" isSuccess &= getEditorialVoiceGroup()->get{ev_cls}()->fromXElement(message, *iter);") - lines.append(" }") - lines.append("") - lines.append(" ++iter;") - lines.append(" isIterIncremented = true;") - lines.append(" }") - lines.append("") - lines.append(" if (isIterIncremented)") - lines.append(" {") - lines.append(" --iter;") - lines.append(" }") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("} // namespace core") - lines.append("} // namespace mx") - return "\n".join(lines) + "\n" - - -# --------------------------------------------------------------------------- -# Orchestrator -# --------------------------------------------------------------------------- - -def _emit_note_family(elem_name, elem, ct, model, generated_attrs, stats): - """Emit Note + NoteAttributes + NoteChoice + 5 helper classes. - - File set (8 .h + 8 .cpp): - NoteAttributes, FullNoteTypeChoice, FullNoteGroup, - GraceNoteGroup, CueNoteGroup, NormalNoteGroup, NoteChoice, Note. - """ - s = _extract_note_structure(ct, model) - - class_name = element_class_name(elem_name) # "Note" - choice_class = pascal(elem_name) + "Choice" # "NoteChoice" - full_note_cls = pascal("full-note") + "Group" # "FullNoteGroup" - full_note_type_choice_cls = pascal("full-note") + "TypeChoice" # "FullNoteTypeChoice" - - type_name = elem.type_name or "" - attrs_name = resolve_attrs_name(elem_name, type_name, model) # "NoteAttributes" - - # 1. NoteAttributes -- standard attrs path - if ct.attributes and attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - # 2. FullNoteTypeChoice (inner pitch/unpitched/rest choice) - h = generate_full_note_type_choice_h(full_note_type_choice_cls, s["full_note"]["choice_branches"]) - c = generate_full_note_type_choice_cpp(full_note_type_choice_cls, s["full_note"]["choice_branches"]) - write_file(os.path.join(ELEM_DIR, f"{full_note_type_choice_cls}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{full_note_type_choice_cls}.cpp"), c) - - # 3. FullNoteGroup - h = generate_full_note_group_h(full_note_cls, s["full_note"]["pre"], full_note_type_choice_cls) - c = generate_full_note_group_cpp(full_note_cls, s["full_note"]["pre"], full_note_type_choice_cls) - write_file(os.path.join(ELEM_DIR, f"{full_note_cls}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{full_note_cls}.cpp"), c) - - # 4. The three outer branch groups (Grace/Cue/Normal) - for b in s["branches"]: - h = generate_outer_branch_group_h(b["class_name"], b["members"], full_note_cls) - c = generate_outer_branch_group_cpp(b["class_name"], b["members"], full_note_cls) - write_file(os.path.join(ELEM_DIR, f"{b['class_name']}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{b['class_name']}.cpp"), c) - - # 5. NoteChoice - h = generate_note_choice_h(choice_class, s["branches"]) - c = generate_note_choice_cpp(choice_class, s["branches"]) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.cpp"), c) - - # 6. Note - h = generate_note_h(class_name, attrs_name, choice_class, s["trailing"], model) - c = generate_note_cpp(class_name, attrs_name, choice_class, s["trailing"], - s["full_note"]["pre"], s["full_note"]["choice_branches"], - s["editorial_voice"], s["branches"], s["duration"], model) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -# --------------------------------------------------------------------------- -# Direction (bespoke, schema-driven) -# -# Direction's child contains a choice whose branches are -# read from the parsed XSD (each branch is an ElementRefNode with -# max_occurs == -1 indicating a "multi" branch). Two anonymous-namespace -# arrays in Direction.cpp -- directions[] (every branch name) and -# multiDirections[] (only the unbounded ones) -- are derived directly from -# those branches so MusicXML 4.0 changes propagate automatically. -# -# The bulk of the Direction.cpp body is a hand-shaped parser that promotes -# the first parsed direction-type into the default-seeded slot and then -# appends. For each "multi" branch (rehearsal, segno, words, coda, -# dynamics, percussion) the parser also swaps the DirectionType's first -# default-seeded sub-element using addX + removeX. The shape of that -# parser cannot be expressed by the shared rule-based path, so it lives -# here. - - -def _emit_direction_family(elem_name, elem, ct, model, generated_attrs, stats): - assert elem_name == "direction" - - # Read the direction-type choice branches from the parsed model. These - # drive the two anonymous-namespace arrays in Direction.cpp and the - # per-branch dispatch in createDirectionType(). - dt_ct = model.complex_types["direction-type"] - dt_branches = list(dt_ct.content_tree.branches) - branch_names = [b.element_name for b in dt_branches] - multi_branch_names = [b.element_name for b in dt_branches if b.max_occurs == -1] - - class_name = element_class_name(elem_name) # "Direction" - type_name = elem.type_name or "" - - # 1. Attrs struct via the standard generator. - attrs_name = None - if ct.attributes: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h = generate_attrs_h(attrs_name, ct.attributes, model) - c = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), c) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - # 2. Direction.h - h_content = _generate_direction_h(class_name, attrs_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h_content) - - # 3. Direction.cpp - c_content = _generate_direction_cpp(class_name, attrs_name, branch_names, multi_branch_names) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), c_content) - - stats["elem_written"] += 1 - stats["bespoke_written"] = stats.get("bespoke_written", 0) + 1 - - -def _generate_direction_h(class_name, attrs_name): - lines = [LICENSE, "#pragma once\n"] - lines.append('#include "mx/core/ElementInterface.h"') - lines.append('#include "mx/core/ForwardDeclare.h"') - lines.append(f'#include "mx/core/elements/{attrs_name}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace ezxml\n{\nclass XElementIterator;\n}") - lines.append("") - lines.append("namespace mx\n{\nnamespace core\n{\n") - lines.append(f"MX_FORWARD_DECLARE_ATTRIBUTES({attrs_name})") - lines.append("MX_FORWARD_DECLARE_ELEMENT(DirectionType)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(EditorialVoiceDirectionGroup)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(Offset)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(Sound)") - lines.append("MX_FORWARD_DECLARE_ELEMENT(Staff)") - lines.append(f"MX_FORWARD_DECLARE_ELEMENT({class_name})") - lines.append("") - lines.append(f"inline {class_name}Ptr make{class_name}()") - lines.append("{") - lines.append(f" return std::make_shared<{class_name}>();") - lines.append("}") - lines.append("") - lines.append(f"class {class_name} : public ElementInterface") - lines.append("{") - lines.append(" public:") - lines.append(f" {class_name}();") - lines.append("") - lines.append(" virtual bool hasAttributes() const;") - lines.append(" virtual std::ostream &streamAttributes(std::ostream &os) const;") - lines.append(" virtual std::ostream &streamName(std::ostream &os) const;") - lines.append(" virtual bool hasContents() const;") - lines.append(" virtual std::ostream &streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const;") - lines.append(f" {attrs_name}Ptr getAttributes() const;") - lines.append(f" void setAttributes(const {attrs_name}Ptr &value);") - lines.append("") - lines.append(" /* _________ DirectionType minOccurs = 1, maxOccurs = unbounded _________ */") - lines.append(" const DirectionTypeSet &getDirectionTypeSet() const;") - lines.append(" void addDirectionType(const DirectionTypePtr &value);") - lines.append(" void removeDirectionType(const DirectionTypeSetIterConst &value);") - lines.append(" void clearDirectionTypeSet();") - lines.append(" DirectionTypePtr getDirectionType(const DirectionTypeSetIterConst &setIterator) const;") - lines.append("") - lines.append(" /* _________ Offset minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(" OffsetPtr getOffset() const;") - lines.append(" void setOffset(const OffsetPtr &value);") - lines.append(" bool getHasOffset() const;") - lines.append(" void setHasOffset(const bool value);") - lines.append("") - lines.append(" /* _________ EditorialVoiceDirectionGroup minOccurs = 1, maxOccurs = 1 _________ */") - lines.append(" EditorialVoiceDirectionGroupPtr getEditorialVoiceDirectionGroup() const;") - lines.append(" void setEditorialVoiceDirectionGroup(const EditorialVoiceDirectionGroupPtr &value);") - lines.append("") - lines.append(" /* _________ Staff minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(" StaffPtr getStaff() const;") - lines.append(" void setStaff(const StaffPtr &value);") - lines.append(" bool getHasStaff() const;") - lines.append(" void setHasStaff(const bool value);") - lines.append("") - lines.append(" /* _________ Sound minOccurs = 0, maxOccurs = 1 _________ */") - lines.append(" SoundPtr getSound() const;") - lines.append(" void setSound(const SoundPtr &value);") - lines.append(" bool getHasSound() const;") - lines.append(" void setHasSound(const bool value);") - lines.append("") - lines.append(" private:") - lines.append(" virtual bool fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement);") - lines.append("") - lines.append(" private:") - lines.append(f" {attrs_name}Ptr myAttributes;") - lines.append(" DirectionTypeSet myDirectionTypeSet;") - lines.append(" OffsetPtr myOffset;") - lines.append(" bool myHasOffset;") - lines.append(" EditorialVoiceDirectionGroupPtr myEditorialVoiceDirectionGroup;") - lines.append(" StaffPtr myStaff;") - lines.append(" bool myHasStaff;") - lines.append(" SoundPtr mySound;") - lines.append(" bool myHasSound;") - lines.append("") - lines.append(" bool importDirectionTypeSet(std::ostream &message, ::ezxml::XElementIterator &iter,") - lines.append(" ::ezxml::XElementIterator &endIter, bool &isSuccess, bool &isFound);") - lines.append(" DirectionTypePtr createDirectionType(std::ostream &message, ::ezxml::XElementIterator &iter,") - lines.append(" ::ezxml::XElementIterator &endIter, bool &isSuccess);") - lines.append("") - lines.append(" bool isDirectionType(const std::string &elementName) const;") - lines.append(" bool isMultiDirectionType(const std::string &elementName) const;") - lines.append("};") - lines.append("} // namespace core") - lines.append("} // namespace mx") - lines.append("") - return "\n".join(lines) - - -def _generate_direction_cpp(class_name, attrs_name, branch_names, multi_branch_names): - # Build the anonymous-namespace arrays from XSD-derived branch names. - directions_init = ", ".join(f'"{n}"' for n in branch_names) - multi_init = ", ".join(f'"{n}"' for n in multi_branch_names) - - lines = [LICENSE] - lines.append(f'#include "mx/core/elements/{class_name}.h"') - lines.append('#include "mx/core/FromXElement.h"') - lines.append('#include "mx/core/elements/DirectionType.h"') - lines.append('#include "mx/core/elements/EditorialVoiceDirectionGroup.h"') - lines.append('#include "mx/core/elements/Footnote.h"') - lines.append('#include "mx/core/elements/Level.h"') - lines.append('#include "mx/core/elements/Offset.h"') - lines.append('#include "mx/core/elements/Sound.h"') - lines.append('#include "mx/core/elements/Staff.h"') - lines.append('#include "mx/core/elements/Voice.h"') - lines.append("") - # Per-branch direction-type element includes, sorted to match HEAD - # (alphabetical by C++ class name). - sorted_branches = sorted(branch_names, key=lambda n: pascal(n)) - for n in sorted_branches: - lines.append(f'#include "mx/core/elements/{pascal(n)}.h"') - lines.append("") - lines.append("#include ") - lines.append("#include ") - lines.append("#include ") - lines.append("") - lines.append("namespace") - lines.append("{") - lines.append("") - lines.append(f"constexpr const size_t directionsSize = {len(branch_names)};") - lines.append(f"constexpr const char *const directions[] = {{{directions_init}}};") - lines.append("") - lines.append(f"constexpr const size_t multiDirectionsSize = {len(multi_branch_names)};") - lines.append(f"constexpr const char *const multiDirections[] = {{{multi_init}}};") - lines.append("} // namespace") - lines.append("") - lines.append("namespace mx") - lines.append("{") - lines.append("namespace core") - lines.append("{") - lines.append(f"{class_name}::{class_name}()") - lines.append(f" : myAttributes(std::make_shared<{attrs_name}>()), myDirectionTypeSet(), myOffset(makeOffset()),") - lines.append(" myHasOffset(false), myEditorialVoiceDirectionGroup(makeEditorialVoiceDirectionGroup()), myStaff(makeStaff()),") - lines.append(" myHasStaff(false), mySound(makeSound()), myHasSound(false)") - lines.append("{") - lines.append(" myDirectionTypeSet.push_back(makeDirectionType());") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasAttributes() const") - lines.append("{") - lines.append(" return myAttributes->hasValues();") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamAttributes(std::ostream &os) const") - lines.append("{") - lines.append(" return myAttributes->toStream(os);") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamName(std::ostream &os) const") - lines.append("{") - lines.append(' os << "direction";') - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"bool {class_name}::hasContents() const") - lines.append("{") - lines.append(" return true;") - lines.append("}") - lines.append("") - lines.append(f"std::ostream &{class_name}::streamContents(std::ostream &os, const int indentLevel, bool &isOneLineOnly) const") - lines.append("{") - lines.append(" for (auto x : myDirectionTypeSet)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" x->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" if (myHasOffset)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" myOffset->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" if (myEditorialVoiceDirectionGroup->hasContents())") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" myEditorialVoiceDirectionGroup->streamContents(os, indentLevel + 1, isOneLineOnly);") - lines.append(" }") - lines.append(" if (myHasStaff)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" myStaff->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" if (myHasSound)") - lines.append(" {") - lines.append(" os << std::endl;") - lines.append(" mySound->toStream(os, indentLevel + 1);") - lines.append(" }") - lines.append(" os << std::endl;") - lines.append(" isOneLineOnly = false;") - lines.append(" return os;") - lines.append("}") - lines.append("") - lines.append(f"{attrs_name}Ptr {class_name}::getAttributes() const") - lines.append("{") - lines.append(" return myAttributes;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setAttributes(const {attrs_name}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myAttributes = value;") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"const DirectionTypeSet &{class_name}::getDirectionTypeSet() const") - lines.append("{") - lines.append(" return myDirectionTypeSet;") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::addDirectionType(const DirectionTypePtr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(" myDirectionTypeSet.push_back(value);") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::removeDirectionType(const DirectionTypeSetIterConst &value)") - lines.append("{") - lines.append(" if (value != myDirectionTypeSet.cend())") - lines.append(" {") - lines.append(" if (myDirectionTypeSet.size() > 1)") - lines.append(" {") - lines.append(" myDirectionTypeSet.erase(value);") - lines.append(" }") - lines.append(" }") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::clearDirectionTypeSet()") - lines.append("{") - lines.append(" myDirectionTypeSet.clear();") - lines.append(" myDirectionTypeSet.push_back(makeDirectionType());") - lines.append("}") - lines.append("") - lines.append(f"DirectionTypePtr {class_name}::getDirectionType(const DirectionTypeSetIterConst &setIterator) const") - lines.append("{") - lines.append(" if (setIterator != myDirectionTypeSet.cend())") - lines.append(" {") - lines.append(" return *setIterator;") - lines.append(" }") - lines.append(" return DirectionTypePtr();") - lines.append("}") - lines.append("") - # Plain single-occurrence accessors: offset, editorial-voice-direction, staff, sound - for (cap_field, lower_field, with_has) in [ - ("Offset", "myOffset", True), - ("EditorialVoiceDirectionGroup", "myEditorialVoiceDirectionGroup", False), - ("Staff", "myStaff", True), - ("Sound", "mySound", True), - ]: - lines.append(f"{cap_field}Ptr {class_name}::get{cap_field}() const") - lines.append("{") - lines.append(f" return {lower_field};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::set{cap_field}(const {cap_field}Ptr &value)") - lines.append("{") - lines.append(" if (value)") - lines.append(" {") - lines.append(f" {lower_field} = value;") - lines.append(" }") - lines.append("}") - lines.append("") - if with_has: - lines.append(f"bool {class_name}::getHas{cap_field}() const") - lines.append("{") - lines.append(f" return myHas{cap_field};") - lines.append("}") - lines.append("") - lines.append(f"void {class_name}::setHas{cap_field}(const bool value)") - lines.append("{") - lines.append(f" myHas{cap_field} = value;") - lines.append("}") - lines.append("") - - # fromXElementImpl - lines.append(f"bool {class_name}::fromXElementImpl(std::ostream &message, ::ezxml::XElement &xelement)") - lines.append("{") - lines.append(" bool isSuccess = true;") - lines.append(" bool isDirectionTypeFound = false;") - lines.append(" isSuccess &= myAttributes->fromXElement(message, xelement);") - lines.append("") - lines.append(" auto endIter = xelement.end();") - lines.append(" for (auto it = xelement.begin(); it != endIter; ++it)") - lines.append(" {") - lines.append(" if (importDirectionTypeSet(message, it, endIter, isSuccess, isDirectionTypeFound))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" if (importElement(message, *it, isSuccess, *myOffset, myHasOffset))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" importGroup(message, it, endIter, isSuccess, myEditorialVoiceDirectionGroup);") - lines.append(" if (importElement(message, *it, isSuccess, *myStaff, myHasStaff))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" if (importElement(message, *it, isSuccess, *mySound, myHasSound))") - lines.append(" {") - lines.append(" continue;") - lines.append(" }") - lines.append(" }") - lines.append("") - lines.append(" MX_RETURN_IS_SUCCESS;") - lines.append("}") - lines.append("") - - # importDirectionTypeSet - lines.append(f"bool {class_name}::importDirectionTypeSet(std::ostream &message, ::ezxml::XElementIterator &iter,") - lines.append(" ::ezxml::XElementIterator &endIter, bool &isSuccess, bool &isFound)") - lines.append("{") - lines.append(" if (iter == endIter)") - lines.append(" {") - lines.append(" isFound = false;") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(' if (iter->getName() != "direction-type")') - lines.append(" {") - lines.append(" isFound = false;") - lines.append(" return false;") - lines.append(" }") - lines.append("") - lines.append(" isFound = true;") - lines.append(" bool isIterIncremented = false;") - lines.append(" bool isFirstDirectionTypeAdded = false;") - lines.append("") - lines.append(' while ((iter != endIter) && (iter->getName() == "direction-type"))') - lines.append(" {") - lines.append(" auto subiter = iter->begin();") - lines.append(" auto subiterEnd = iter->end();") - lines.append("") - lines.append(" auto directionType = createDirectionType(message, subiter, subiterEnd, isSuccess);") - lines.append("") - lines.append(" if (!isFirstDirectionTypeAdded && myDirectionTypeSet.size() == 1)") - lines.append(" {") - lines.append(" *myDirectionTypeSet.begin() = directionType;") - lines.append(" isFirstDirectionTypeAdded = true;") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(" myDirectionTypeSet.push_back(directionType);") - lines.append(" isFirstDirectionTypeAdded = true;") - lines.append(" }") - lines.append("") - lines.append(" isIterIncremented = true;") - lines.append(" ++iter;") - lines.append(" }") - lines.append("") - lines.append(" if (isIterIncremented)") - lines.append(" {") - lines.append(" --iter;") - lines.append(" }") - lines.append(" return isFirstDirectionTypeAdded;") - lines.append("}") - lines.append("") - - # isDirectionType - lines.append(f"bool {class_name}::isDirectionType(const std::string &elementName) const") - lines.append("{") - lines.append(" for (size_t i = 0; i < directionsSize; ++i)") - lines.append(" {") - lines.append(" if (strcmp(directions[i], elementName.c_str()) == 0)") - lines.append(" {") - lines.append(" return true;") - lines.append(" }") - lines.append(" }") - lines.append(" return false;") - lines.append("}") - lines.append("") - - # isMultiDirectionType - lines.append(f"bool {class_name}::isMultiDirectionType(const std::string &elementName) const") - lines.append("{") - lines.append(" for (size_t i = 0; i < multiDirectionsSize; ++i)") - lines.append(" {") - lines.append(" if (strcmp(multiDirections[i], elementName.c_str()) == 0)") - lines.append(" {") - lines.append(" return true;") - lines.append(" }") - lines.append(" }") - lines.append(" return false;") - lines.append("}") - lines.append("") - - # createDirectionType - lines.append(f"DirectionTypePtr {class_name}::createDirectionType(std::ostream &message, ::ezxml::XElementIterator &subIter,") - lines.append(" ::ezxml::XElementIterator &subIterEnd, bool &isSuccess)") - lines.append("{") - lines.append(" auto directionType = makeDirectionType();") - lines.append("") - lines.append(" if (subIter == subIterEnd)") - lines.append(" {") - lines.append(' message << "Direction: well thats weird - should not get here" << std::endl;') - lines.append(" isSuccess = false;") - lines.append(" return directionType;") - lines.append(" }") - lines.append("") - - # Single-occurrence branches (non-multi): emit if-block with setChoice + fromXElement. - # HEAD emits these BEFORE the multi branches. - multi_set = set(multi_branch_names) - for n in branch_names: - if n in multi_set: - continue - cls = pascal(n) # e.g. OctaveShift - getter = f"get{cls}" # e.g. getOctaveShift - choice_enum = camel(n) # e.g. octaveShift - lines.append(f' if (subIter->getName() == "{n}")') - lines.append(" {") - lines.append(f" directionType->setChoice(DirectionType::Choice::{choice_enum});") - lines.append(f" isSuccess &= directionType->{getter}()->fromXElement(message, *subIter);") - lines.append(" return directionType;") - lines.append(" }") - lines.append("") - - # Multi branches: HEAD orders them rehearsal, segno, words, coda, dynamics, - # percussion -- which is the XSD order filtered to multi. - for idx, n in enumerate(multi_branch_names): - cls = pascal(n) - choice_enum = camel(n) - if idx == 0: - lines.append(f' std::string name = "{n}";') - else: - lines.append(f' name = "{n}";') - lines.append(" if (subIter->getName() == name)") - lines.append(" {") - lines.append(f" directionType->setChoice(DirectionType::Choice::{choice_enum});") - lines.append(" bool isFirstSubItemAdded = false;") - lines.append("") - lines.append(" while (subIter != subIterEnd)") - lines.append(" {") - lines.append(" if (subIter->getName() != name)") - lines.append(" {") - lines.append(f' message << "Direction: createDirectionType encountered an unexpected element \'" << subIter->getName()') - lines.append(f' << "\' while parsing a collection of \'" << name << "\' elements" << std::endl;') - lines.append(" isSuccess = false;") - lines.append(" return directionType;") - lines.append(" }") - lines.append(f" auto itemToAdd = make{cls}();") - lines.append(" isSuccess &= itemToAdd->fromXElement(message, *subIter);") - lines.append(f" if (!isFirstSubItemAdded && directionType->get{cls}Set().size() == 1)") - lines.append(" {") - lines.append(f" directionType->add{cls}(itemToAdd);") - lines.append(f" directionType->remove{cls}(directionType->get{cls}Set().cbegin());") - lines.append(" }") - lines.append(" else") - lines.append(" {") - lines.append(f" directionType->add{cls}(itemToAdd);") - lines.append(" }") - lines.append(" isFirstSubItemAdded = true;") - lines.append(" ++subIter;") - lines.append(" } // end loop") - lines.append(" return directionType;") - lines.append(f" }} // end {n}") - lines.append("") - - lines.append(" return directionType;") - lines.append("}") - lines.append("") - lines.append("} // namespace core") - lines.append("} // namespace mx") - lines.append("") - return "\n".join(lines) - - -BESPOKE_ELEMENTS = { - "credit": _emit_credit_family, - "lyric": _emit_lyric_family, - "note": _emit_note_family, - "part-list": _emit_part_list_family, - "harmony": _emit_harmony_family, - "score-partwise": _emit_score_wrapper_family, - "score-timewise": _emit_score_wrapper_family, - "direction": _emit_direction_family, -} - - -def _parse_config() -> ParseConfig: - """Bundle the C++-aware structural-config globals for injection into the parser. - - The four set fields are passed by reference: the parser records synthetic groups - into them during parsing, and the emission code below reads the same objects. - """ - return ParseConfig( - generate_groups=GENERATE_GROUPS, - synthetic_optional_groups=SYNTHETIC_OPTIONAL_GROUPS, - synthetic_unbounded_groups=SYNTHETIC_UNBOUNDED_GROUPS, - suppress_group_suffix=SUPPRESS_GROUP_SUFFIX, - extension_optional_group_rename=EXTENSION_OPTIONAL_GROUP_RENAME, - nested_optional_sequence_as_group=NESTED_OPTIONAL_SEQUENCE_AS_GROUP, - unbounded_sequence_as_group=UNBOUNDED_SEQUENCE_AS_GROUP, - ) - - -def main(): - model = XsdModel(XSD_PATH, _parse_config()) - - stats = {"attrs_written": 0, "elem_written": 0, "elem_skipped": 0} - categories = {} - - generated_attrs = set() - - for elem_name, elem in model.elements.items(): - if elem_name in SKIP_ELEMENTS or elem_name in DYNAMICS_MARKS: - continue - # Elements claimed by a family-handler (e.g. score-partwise emits - # PartwisePart and PartwiseMeasure for "part" and "measure") must be - # silently skipped here so the default path doesn't double-write or - # produce broken files. - if elem_name in BESPOKE_FAMILY_OWNED: - continue - - cat = classify_element(elem, model) - categories.setdefault(cat, []).append(elem_name) - - ct = model.complex_types.get(elem.type_name) if elem.type_name else None - if ct is None and elem.anonymous_type is not None: - ct = elem.anonymous_type - - # Bespoke per-element generators (last-resort path for elements that - # don't fit any reusable mechanism). Each handler is schema-driven - # (reads names from the parsed XSD). - bespoke = BESPOKE_ELEMENTS.get(elem_name) - if bespoke and ct: - bespoke(elem_name, elem, ct, model, generated_attrs, stats) - continue - - # Tree-based generation for elements with nested choice/sequence - if elem_name in TREE_ELEMENTS and ct and ct.content_tree: - if True: - plan = analyze_tree(elem_name, ct.content_tree, model) - if plan: - class_name = element_class_name(elem_name) - type_name = elem.type_name or "" - has_attrs = bool(ct.attributes) - attrs_name = None - - if has_attrs: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h_content = generate_attrs_h(attrs_name, ct.attributes, model) - cpp_content = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), cpp_content) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - for group_class, group_children in plan.groups_to_generate: - gh = generate_tree_group_h(group_class, group_children, model) - gc = generate_tree_group_cpp(group_class, group_children, model) - write_file(os.path.join(ELEM_DIR, f"{group_class}.h"), gh) - write_file(os.path.join(ELEM_DIR, f"{group_class}.cpp"), gc) - - for og_class, og_children in plan.optional_groups_to_generate: - gh = generate_tree_group_h(og_class, og_children, model) - gc = generate_tree_group_cpp(og_class, og_children, model) - write_file(os.path.join(ELEM_DIR, f"{og_class}.h"), gh) - write_file(os.path.join(ELEM_DIR, f"{og_class}.cpp"), gc) - - for nc_class, nc_branches, nc_parent in plan.nested_choices_to_generate: - nch = generate_tree_choice_h(nc_class, nc_branches, nc_parent) - ncc = generate_tree_choice_cpp(nc_class, nc_branches, nc_parent) - write_file(os.path.join(ELEM_DIR, f"{nc_class}.h"), nch) - write_file(os.path.join(ELEM_DIR, f"{nc_class}.cpp"), ncc) - - for container in plan.containers_to_generate: - cth = generate_container_h(container) - ctc = generate_container_cpp(container) - write_file(os.path.join(ELEM_DIR, f"{container.class_name}.h"), cth) - write_file(os.path.join(ELEM_DIR, f"{container.class_name}.cpp"), ctc) - - tree_config = _get_tree_config(elem_name) - if tree_config.get("inlined_choice"): - inline_cfg = { - "branches": [ - { - "enum_name": b.enum_name, - "class_name": b.class_name, - "is_group": b.is_group, - "element_name": b.xml_name or b.class_name, - } - for b in plan.choice_branches - ], - "enum_start": 1, - } - ph = generate_inline_choice_h(elem_name, class_name, inline_cfg, - has_attrs, attrs_name) - pc = generate_inline_choice_cpp(elem_name, class_name, inline_cfg, - has_attrs, attrs_name, elem_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), ph) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), pc) - stats["elem_written"] += 1 - stats["tree_written"] = stats.get("tree_written", 0) + 1 - continue - - if len(plan.inline_choices) > 1: - for ic in plan.inline_choices: - ch = generate_tree_choice_h(ic.choice_class, ic.branches, elem_name) - cc = generate_tree_choice_cpp(ic.choice_class, ic.branches, elem_name) - write_file(os.path.join(ELEM_DIR, f"{ic.choice_class}.h"), ch) - write_file(os.path.join(ELEM_DIR, f"{ic.choice_class}.cpp"), cc) - else: - ch = generate_tree_choice_h(plan.choice_class, plan.choice_branches, elem_name) - cc = generate_tree_choice_cpp(plan.choice_class, plan.choice_branches, elem_name) - write_file(os.path.join(ELEM_DIR, f"{plan.choice_class}.h"), ch) - write_file(os.path.join(ELEM_DIR, f"{plan.choice_class}.cpp"), cc) - - ph = generate_tree_parent_h(elem_name, class_name, plan.choice_class, - plan.trailing_children, has_attrs, attrs_name, model, - choice_is_set=plan.choice_is_set, - leading_groups=plan.leading_groups, - leading_children=plan.leading_children, - choice_is_optional=plan.choice_is_optional, - inline_choices=plan.inline_choices, - choice_branches=plan.choice_branches) - pc = generate_tree_parent_cpp(elem_name, class_name, plan.choice_class, - plan.trailing_children, has_attrs, attrs_name, - plan, model) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), ph) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), pc) - stats["elem_written"] += 1 - stats["tree_written"] = stats.get("tree_written", 0) + 1 - continue - - if cat == "choice" and elem_name not in CHOICE_SKIP: - enum_val_cfg = ENUM_VALUE_CHOICE_CONFIG.get(elem_name) - inline_cfg = INLINE_CHOICE_CONFIG.get(elem_name) - if enum_val_cfg and ct: - class_name = element_class_name(elem_name) - has_attrs = bool(ct.attributes) - attrs_name = None - type_name = elem.type_name or "" - if has_attrs: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h_content = generate_attrs_h(attrs_name, ct.attributes, model) - cpp_content = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), cpp_content) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - h_content = generate_enum_value_choice_h( - elem_name, class_name, enum_val_cfg["value_type"], - has_attrs, attrs_name) - cpp_content = generate_enum_value_choice_cpp( - elem_name, class_name, enum_val_cfg["value_type"], - enum_val_cfg["enum_type"], enum_val_cfg["other_variant"], - enum_val_cfg["other_xml_name"], has_attrs, attrs_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), cpp_content) - stats["elem_written"] += 1 - elif inline_cfg and ct: - class_name = element_class_name(elem_name) - stream_name = elem_name - type_name = elem.type_name or "" - has_attrs = bool(ct.attributes) - attrs_name = None - - if has_attrs: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h_content = generate_attrs_h(attrs_name, ct.attributes, model) - cpp_content = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), cpp_content) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - for branch in inline_cfg["branches"]: - if branch.get("is_group"): - group_children = [ - XsdChildRef( - element_name=c["name"], - min_occurs=c["min"], - max_occurs=c["max"], - ) - for c in branch["children"] - ] - group_class = branch["class_name"] - group_base = group_class.replace("Group", "").lower() - gh = generate_group_h(group_base, group_children, model) - gc = generate_group_cpp(group_base, group_children, model) - write_file(os.path.join(ELEM_DIR, f"{group_class}.h"), gh) - write_file(os.path.join(ELEM_DIR, f"{group_class}.cpp"), gc) - - ph = generate_inline_choice_h(elem_name, class_name, inline_cfg, - has_attrs, attrs_name) - pc = generate_inline_choice_cpp(elem_name, class_name, inline_cfg, - has_attrs, attrs_name, stream_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), ph) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), pc) - stats["elem_written"] += 1 - stats["inline_choice_written"] = stats.get("inline_choice_written", 0) + 1 - - else: - config = CHOICE_ELEMENT_CONFIG.get(elem_name) - if config and ct and ct.choice_children: - choice_class = config["choice_class"] - is_set = config["is_set"] - enum_start = config["enum_start"] - class_name = element_class_name(elem_name) - stream_name = elem_name - type_name = elem.type_name or "" - has_attrs = bool(ct.attributes) - attrs_name = None - - if has_attrs: - attrs_name = resolve_attrs_name(elem_name, type_name, model) - if attrs_name not in generated_attrs and attrs_name not in CORE_ROOT_ATTRS: - h_content = generate_attrs_h(attrs_name, ct.attributes, model) - cpp_content = generate_attrs_cpp(attrs_name, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{attrs_name}.cpp"), cpp_content) - generated_attrs.add(attrs_name) - stats["attrs_written"] += 1 - - choice_children = list(ct.choice_children) - extra = config.get("extra_children", []) - if extra: - after_name = config.get("extra_children_after") - if after_name: - insert_idx = next((i for i, c in enumerate(choice_children) - if c.element_name == after_name), len(choice_children) - 1) + 1 - else: - insert_idx = len(choice_children) - 1 - for ec in extra: - choice_children.insert(insert_idx, type('obj', (), { - 'element_name': ec, 'min_occurs': 1, 'max_occurs': 1})()) - insert_idx += 1 - - if config.get("bespoke_choice"): - ch = generate_time_choice_h() - cc = generate_time_choice_cpp() - write_file(os.path.join(ELEM_DIR, f"{choice_class}.h"), ch) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.cpp"), cc) - gh = generate_group_h("time-signature", - TIME_SIGNATURE_GROUP_CHILDREN, model) - gc = generate_group_cpp("time-signature", - TIME_SIGNATURE_GROUP_CHILDREN, model) - write_file(os.path.join(ELEM_DIR, "TimeSignatureGroup.h"), gh) - write_file(os.path.join(ELEM_DIR, "TimeSignatureGroup.cpp"), gc) - else: - choice_is_set = config.get("choice_is_set", is_set) - ch = generate_choice_class_h(choice_class, choice_children, - choice_is_set, enum_start, elem_name, model) - cc = generate_choice_class_cpp(choice_class, choice_children, - choice_is_set, enum_start, elem_name, - model, config) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.h"), ch) - write_file(os.path.join(ELEM_DIR, f"{choice_class}.cpp"), cc) - - if not config.get("skip_parent"): - ph = generate_choice_parent_h(elem_name, class_name, choice_class, - is_set, has_attrs, attrs_name, model, - config) - pc = generate_choice_parent_cpp(elem_name, class_name, choice_class, - is_set, has_attrs, attrs_name, - stream_name, model, config, - choice_children) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), ph) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), pc) - stats["elem_written"] += 1 - stats["choice_written"] = stats.get("choice_written", 0) + 1 - else: - stats["elem_skipped"] += 1 - - elif cat in ("empty-with-attrs", "text-with-attrs", "complex-with-attrs", - "complex", "text-value", "empty", "simple-value"): - - class_name = element_class_name(elem_name) - stream_name = elem_name - type_name = elem.type_name or "" - - if ct and ct.attributes: - sname = resolve_attrs_name(elem_name, type_name, model) - if sname not in generated_attrs and sname not in CORE_ROOT_ATTRS: - h_content = generate_attrs_h(sname, ct.attributes, model) - cpp_content = generate_attrs_cpp(sname, ct.attributes, model) - write_file(os.path.join(ELEM_DIR, f"{sname}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{sname}.cpp"), cpp_content) - generated_attrs.add(sname) - stats["attrs_written"] += 1 - - if cat == "simple-value": - value_type = resolve_cpp_type(elem.type_name, model) - fake_ct = XsdComplexType(name=elem.type_name) - fake_ct.has_simple_content = True - fake_ct.simple_content_base = elem.type_name - h_content = generate_element_h(elem_name, class_name, stream_name, cat, fake_ct, model, type_name) - cpp_content = generate_element_cpp(elem_name, class_name, stream_name, cat, fake_ct, model, type_name) - else: - h_content = generate_element_h(elem_name, class_name, stream_name, cat, ct, model, type_name) - cpp_content = generate_element_cpp(elem_name, class_name, stream_name, cat, ct, model, type_name) - - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), cpp_content) - stats["elem_written"] += 1 - else: - stats["elem_skipped"] += 1 - - # Generate Group wrapper classes - groups_written = 0 - for group_name, group_children in model.groups.items(): - if group_name not in GENERATE_GROUPS: - continue - h_content = generate_group_h(group_name, group_children, model) - cpp_content = generate_group_cpp(group_name, group_children, model) - class_name = group_class_name(group_name) - write_file(os.path.join(ELEM_DIR, f"{class_name}.h"), h_content) - write_file(os.path.join(ELEM_DIR, f"{class_name}.cpp"), cpp_content) - groups_written += 1 - - print("=== Generation Stats ===") - print(f"Attributes structs written: {stats['attrs_written']}") - print(f"Element classes written: {stats['elem_written']}") - print(f"Choice classes written: {stats.get('choice_written', 0)}") - print(f"Inline choice written: {stats.get('inline_choice_written', 0)}") - print(f"Tree-based written: {stats.get('tree_written', 0)}") - print(f"Group classes written: {groups_written}") - print(f"Element classes skipped: {stats['elem_skipped']}") - print() - print("=== Element Categories ===") - for cat, names in sorted(categories.items()): - print(f" {cat}: {len(names)}") - print() - print("=== Skipped Elements ===") - for cat in ["choice", "sequence-with-choice", "anonymous", "unknown"]: - if cat in categories: - for n in categories[cat][:10]: - print(f" [{cat}] {n}") - - -def write_file(path, content): - with open(path, "w") as f: - f.write(content) - - -if __name__ == "__main__": - main() diff --git a/gen/ids.py b/gen/ids.py deleted file mode 100644 index 45915ee0f..000000000 --- a/gen/ids.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -"""NodeId: a typed, canonically-stringified identifier for nodes in the parsed XSD model. - -Assigned in ``parse.py``. Currently additive and unconsumed: every parsed node carries a -``NodeId``, but no generation logic reads it yet. Later milestones key structural configuration -and cross-unit lookups off these IDs. - -Canonical string form ---------------------- -A ``NodeId`` is a ``/``-joined path of segments. The first segment names a top-level XSD -construct by kind and name; each subsequent segment names a child relative to its parent. - - named construct: ``:`` e.g. ``el:note``, ``cx:note-type``, - ``st:above-below``, ``gr:editorial``, - ``ag:bend-sound``, ``cx:note-type/at:type`` - anonymous child: ``#`` e.g. ``cx:note-type/seq#0``, - ``cx:note-type/seq#0/choice#1`` - -Kinds: ``el`` (element), ``cx`` (complexType), ``st`` (simpleType), ``gr`` (group), -``ag`` (attributeGroup), ``at`` (attribute), ``seq`` (sequence), ``choice`` (choice). - -Kind is embedded in every segment so that a local element and a local attribute sharing a name -under one owner cannot collide. Same-kind anonymous siblings are disambiguated by ordinal. - -Only the leading named-construct segment is version-stable; nested/anonymous segments are -positional and run-local by design. -""" -from dataclasses import dataclass - - -@dataclass(frozen=True) -class NodeId: - """An immutable identifier with a canonical ``str`` form (``value``).""" - - value: str - - def __str__(self) -> str: - return self.value - - @classmethod - def root(cls, kind: str, name: str) -> "NodeId": - """Identifier for a top-level named construct, e.g. ``el:note``.""" - return cls(f"{kind}:{name}") - - def named_child(self, kind: str, name: str) -> "NodeId": - """Identifier for a named child, e.g. ``cx:note-type/el:pitch``.""" - return NodeId(f"{self.value}/{kind}:{name}") - - def anon_child(self, kind: str, ordinal: int) -> "NodeId": - """Identifier for an anonymous child, e.g. ``cx:note-type/seq#0``.""" - return NodeId(f"{self.value}/{kind}#{ordinal}") diff --git a/gen/ir/__init__.py b/gen/ir/__init__.py new file mode 100644 index 000000000..bf6796e55 --- /dev/null +++ b/gen/ir/__init__.py @@ -0,0 +1,7 @@ +"""Intermediate representation for the mx generator.""" + +from gen.ir.build import build_ir +from gen.ir.model import Ir +from gen.ir.resolve import Resolver + +__all__ = ["Ir", "Resolver", "build_ir"] diff --git a/gen/ir/build.py b/gen/ir/build.py new file mode 100644 index 000000000..ed2d4a159 --- /dev/null +++ b/gen/ir/build.py @@ -0,0 +1,390 @@ +"""Lower a parsed XSD schema (gen.xsd.model) into the IR (gen.ir.model).""" + +from __future__ import annotations + +from collections import Counter + +from gen.xsd import model as xsd +from gen.xsd.analyze import content_particle, reachable_types +from gen.ir import model as ir +from gen.ir.resolve import Resolver + +# Map XSD builtin types to canonical IR primitive names. +_XS_PRIMITIVE = { + "xs:string": "string", + "xs:token": "token", + "xs:NMTOKEN": "nmtoken", + "xs:decimal": "decimal", + "xs:integer": "integer", + "xs:int": "integer", + "xs:positiveInteger": "positive_integer", + "xs:nonNegativeInteger": "non_negative_integer", + "xs:date": "date", + "xs:anyURI": "string", + "xs:language": "token", + # 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 + # neutral schema fact -- so a target that wants validity-by-construction + # can repair them and a non-validating target maps them straight back to + # its token spelling (zero output change). NMTOKEN is already distinct. + "xs:ID": "id", + "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. +_EXTERNAL_ATTR = { + "xml:lang": "token", + "xml:space": "token", + "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"} + +# The closed set of canonical IR primitives (the values of the builtins map): +# everything a Ref with category "primitive" can name, and the keys a target +# type map may override. +PRIMITIVES = frozenset(_XS_PRIMITIVE.values()) | frozenset(_EXTERNAL_ATTR.values()) + + +def _primitive(name: str) -> str: + if name in _XS_PRIMITIVE: + return _XS_PRIMITIVE[name] + if name in _EXTERNAL_ATTR: + return _EXTERNAL_ATTR[name] + return name.split(":")[-1] + + +def _occ(value: int) -> int | str: + return ir.UNBOUNDED if value == xsd.UNBOUNDED else value + + +def _cardinality(min_occurs: int, max_occurs: int) -> str: + if max_occurs == xsd.UNBOUNDED or max_occurs > 1: + return "vector" + return "optional" if min_occurs == 0 else "required" + + +def build_ir(schema: xsd.Schema, source: str) -> ir.Ir: + return _Builder(schema, source).build() + + +class _Builder: + def __init__(self, schema: xsd.Schema, source: str): + self.schema = schema + self.source = source + self.anon_names: dict[int, str] = {} # id(ComplexType) -> synthesized name + self.synth: list[tuple[str, xsd.ComplexType]] = [] + + # ----- top level ------------------------------------------------------- # + + def build(self) -> ir.Ir: + self._hoist_anonymous() + reachable = reachable_types(self.schema) + + value_types = [ + self._value_type(st) + 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() + ] + attribute_groups = [ + ir.AttributeGroup( + name, + [self._attr(a) for a in ag.attributes], + [r.ref for r in ag.group_refs], + ag.doc, + ) + for name, ag in self.schema.attribute_groups.items() + ] + + complex_types = [ + self._complex_type(name, ct) + for name, ct in self.schema.complex_types.items() + if name in reachable + ] + complex_types += [self._complex_type(name, ct) for name, ct in self.synth] + + resolver = Resolver(groups, attribute_groups, complex_types) + for ct in complex_types: + ct.deps = sorted(resolver.deps(ct)) + complex_types = self._topo_sort(complex_types) + + all_named = set(self.schema.simple_types) | set(self.schema.complex_types) + dropped = sorted(all_named - reachable) + + return ir.Ir( + source=self.source, + builtins={**_XS_PRIMITIVE, **_EXTERNAL_ATTR}, + value_types=value_types, + groups=groups, + attribute_groups=attribute_groups, + complex_types=complex_types, + roots=[ir.Root(top.name, top.name) for top in self.schema.elements], + dropped_dead=dropped, + stats=self._stats(value_types, complex_types, dropped), + ) + + # ----- anonymous type hoisting ----------------------------------------- # + + def _hoist_anonymous(self) -> None: + used = set(self.schema.complex_types) | set(self.schema.simple_types) + # Document roots: the root element name is free; descendants are + # qualified by the partwise/timewise hierarchy to keep part/measure + # (which differ between the two) distinct. + for top in self.schema.elements: + if top.inline_type: + qualifier = top.name.replace("score-", "") + self._hoist(top.inline_type, top.name, qualifier, used) + # Anonymous types nested inside named types (e.g. directive). + for ct in self.schema.complex_types.values(): + self._scan(content_particle(ct), "", used) + for g in self.schema.groups.values(): + self._scan(g.particle, "", used) + + def _hoist(self, ct: xsd.ComplexType, name: str, qualifier: str, used: set) -> None: + self.anon_names[id(ct)] = name + self.synth.append((name, ct)) + used.add(name) + self._scan(content_particle(ct), qualifier, used) + + def _scan(self, particle, qualifier: str, used: set) -> None: + for ep in _iter_elements(particle): + if isinstance(ep.inline_type, xsd.ComplexType): + candidate = f"{qualifier}-{ep.name}" if qualifier else ep.name + if candidate in used: + candidate = f"{qualifier}-{ep.name}" if qualifier else f"{ep.name}-type" + self._hoist(ep.inline_type, candidate, qualifier, used) + + # ----- value types ----------------------------------------------------- # + + def _value_type(self, st: xsd.SimpleType) -> ir.ValueType: + if isinstance(st.content, xsd.Union): + return self._union(st) + 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) + if facets.enumerations: + return ir.EnumType( + st.name, primitive, [e.value for e in facets.enumerations], st.doc + ) + if primitive in _NUMERIC: + return ir.NumberType( + st.name, + primitive, + facets.min_inclusive, + facets.max_inclusive, + facets.min_exclusive, + facets.max_exclusive, + st.doc, + ) + return ir.StringType( + st.name, + primitive, + list(facets.patterns), + facets.min_length, + facets.max_length, + facets.length, + st.doc, + ) + + def _resolve_restriction(self, 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) + 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 + + def _union(self, st: xsd.SimpleType) -> ir.UnionType: + members: list[ir.UnionMember] = [] + for m in st.content.member_types: + if m in self.schema.simple_types: + members.append(ir.UnionMember(ir.Ref(m, "value"))) + else: + members.append(ir.UnionMember(ir.Ref(_primitive(m), "primitive"))) + for inline in st.content.inline_members: + if isinstance(inline.content, xsd.Restriction) and inline.content.facets.enumerations: + members.append( + ir.UnionMember( + literals=[e.value for e in inline.content.facets.enumerations] + ) + ) + return ir.UnionType(st.name, members, st.doc) + + # ----- attributes ------------------------------------------------------ # + + def _attr(self, a: xsd.Attribute) -> ir.Attr: + if a.ref: + ref = ir.Ref(_primitive(a.ref), "primitive") + 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) + + # ----- complex types --------------------------------------------------- # + + def _complex_type(self, name: str, ct: xsd.ComplexType) -> ir.ComplexType: + c = ct.content + attrs = [self._attr(a) for a in c.attributes] + agrefs = [r.ref for r in c.attribute_group_refs] + + if isinstance(c, xsd.SimpleContent): + return ir.ComplexType( + name, "value", attrs, agrefs, value_type=self._type_ref(c.base), doc=ct.doc + ) + if isinstance(c, xsd.ComplexContent): + content = self._particle(c.particle) if c.particle else None + return ir.ComplexType( + name, "derived", attrs, agrefs, base=c.base, content=content, doc=ct.doc + ) + # ImplicitContent + if c.particle is not None: + return ir.ComplexType( + name, "composite", attrs, agrefs, content=self._particle(c.particle), doc=ct.doc + ) + presence = not attrs and not agrefs + return ir.ComplexType(name, "empty", attrs, agrefs, presence_only=presence, doc=ct.doc) + + # ----- particles ------------------------------------------------------- # + + def _particle(self, p) -> ir.Particle: + if isinstance(p, xsd.Sequence): + return ir.Sequence([self._particle(i) for i in p.items], p.min_occurs, _occ(p.max_occurs)) + if isinstance(p, xsd.Choice): + return ir.Choice([self._particle(i) for i in p.items], p.min_occurs, _occ(p.max_occurs)) + if isinstance(p, xsd.GroupRef): + return ir.GroupRef(p.ref, p.min_occurs, _occ(p.max_occurs)) + if isinstance(p, xsd.ElementParticle): + return ir.Element( + p.name, + self._element_ref(p), + _cardinality(p.min_occurs, p.max_occurs), + p.min_occurs, + _occ(p.max_occurs), + p.doc, + ) + raise ValueError(f"unexpected particle: {type(p).__name__}") + + def _element_ref(self, ep: xsd.ElementParticle) -> ir.Ref: + if isinstance(ep.inline_type, xsd.ComplexType): + return ir.Ref(self.anon_names[id(ep.inline_type)], "complex") + if ep.type: + return self._type_ref(ep.type) + return ir.Ref("string", "primitive") + + def _type_ref(self, type_name: str) -> ir.Ref: + if type_name in self.schema.complex_types: + return ir.Ref(type_name, "complex") + if type_name in self.schema.simple_types: + return ir.Ref(type_name, "value") + if type_name.startswith(("xs:", "xml:", "xlink:")): + return ir.Ref(_primitive(type_name), "primitive") + return ir.Ref(type_name, "complex") + + # ----- dependency ordering --------------------------------------------- # + + def _topo_sort(self, types: list[ir.ComplexType]) -> list[ir.ComplexType]: + by_name = {t.name: t for t in types} + ordered: list[ir.ComplexType] = [] + state: dict[str, int] = {} # 0 visiting, 1 done + + def visit(name: str) -> None: + if state.get(name) == 1 or name not in by_name: + return + state[name] = 0 + for dep in by_name[name].deps: + visit(dep) + state[name] = 1 + ordered.append(by_name[name]) + + for name in sorted(by_name): + visit(name) + return ordered + + def _topo_sort_values(self, values: list[ir.ValueType]) -> list[ir.ValueType]: + """Order value types deps-first. Only unions reference other value + types (their members); every other kind resolves to a primitive.""" + by_name = {v.name: v for v in values} + ordered: list[ir.ValueType] = [] + state: dict[str, int] = {} + + def deps(v: ir.ValueType) -> list[str]: + if isinstance(v, ir.UnionType): + return [ + m.ref.name + for m in v.members + if m.ref and m.ref.category == "value" and m.ref.name in by_name + ] + return [] + + def visit(name: str) -> None: + if state.get(name) == 1 or name not in by_name: + return + state[name] = 1 + for dep in sorted(deps(by_name[name])): + visit(dep) + ordered.append(by_name[name]) + + for name in sorted(by_name): + visit(name) + return ordered + + # ----- stats ----------------------------------------------------------- # + + def _stats(self, value_types, complex_types, dropped) -> dict: + return { + "value_types": len(value_types), + "value_kinds": dict(Counter(v.kind for v in value_types)), + "complex_types": len(complex_types), + "complex_kinds": dict(Counter(c.kind for c in complex_types)), + "groups": len(self.schema.groups), + "attribute_groups": len(self.schema.attribute_groups), + "synthesized_types": len(self.synth), + "dropped_dead_types": len(dropped), + } + + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # + + +def _iter_elements(particle): + """Yield element particles directly contained in a particle (not inside + group refs, which are scanned at the group definition).""" + if isinstance(particle, (xsd.Sequence, xsd.Choice)): + for item in particle.items: + yield from _iter_elements(item) + elif isinstance(particle, xsd.ElementParticle): + yield particle + + +def _merge_facets(into: xsd.Facets, src: xsd.Facets) -> None: + if src.enumerations: + into.enumerations = src.enumerations + into.patterns = into.patterns + src.patterns + for f in ("min_inclusive", "max_inclusive", "min_exclusive", "max_exclusive", + "min_length", "max_length", "length"): + v = getattr(src, f) + if v is not None: + setattr(into, f, v) diff --git a/gen/ir/dump.py b/gen/ir/dump.py new file mode 100644 index 000000000..d16c1c40b --- /dev/null +++ b/gen/ir/dump.py @@ -0,0 +1,56 @@ +"""Serialize the IR to JSON for inspection.""" + +from __future__ import annotations + +import json +from dataclasses import fields, is_dataclass + +# Discriminator fields are emitted first so each object announces what it is. +_FIRST = ("kind", "node", "name", "element") + + +def to_jsonable(obj): + """Convert IR dataclasses to plain JSON-able data, dropping None and empty + collections to keep the output readable.""" + if is_dataclass(obj): + names = [f.name for f in fields(obj)] + order = [n for n in _FIRST if n in names] + [n for n in names if n not in _FIRST] + result = {} + for name in order: + value = getattr(obj, name) + if value is None or (isinstance(value, (list, dict)) and not value): + continue + result[name] = to_jsonable(value) + return result + if isinstance(obj, list): + return [to_jsonable(x) for x in obj] + if isinstance(obj, dict): + return {k: to_jsonable(v) for k, v in obj.items()} + return obj + + +def to_json(obj) -> str: + return json.dumps(to_jsonable(obj), indent=2) + + +def resolved_view(resolver, ct) -> dict: + """A complex type as an emitter consumes it: attribute groups flattened into + one list, model-group refs spliced into the content. The collapsed form the + Resolver computes, shaped for inspection via `ir --resolve`.""" + view: dict = {"kind": ct.kind, "name": ct.name} + attrs = resolver.attributes(ct) + if attrs: + view["attributes"] = attrs + if ct.kind == "derived": + view["base"] = ct.base + view["all_attributes"] = resolver.all_attributes(ct) + if ct.value_type: + view["value_type"] = ct.value_type + content = resolver.content(ct) + if content is not None: + view["content"] = content + if ct.presence_only: + view["presence_only"] = True + if ct.doc: + view["doc"] = ct.doc + return view diff --git a/gen/ir/model.py b/gen/ir/model.py new file mode 100644 index 000000000..f57e39eb4 --- /dev/null +++ b/gen/ir/model.py @@ -0,0 +1,212 @@ +"""The intermediate representation: a resolved, language-agnostic model. + +The raw XSD model (gen.xsd.model) mirrors the schema 1:1 and still speaks in +XSD terms (restriction chains, attribute-group refs, anonymous inline types). +The IR is what the language emitters consume instead. It is a pure function of +the XSD with every cross-reference resolved: + + - all types are named (anonymous types are hoisted, with context-qualified + names for the partwise/timewise scaffolding); + - simple-type restriction chains are collapsed to one primitive plus merged + facets; + - element occurrence is normalized to a cardinality (required/optional/vector); + - dead types are dropped and complex types are emitted in dependency order. + +The IR deliberately *preserves* named structure (aliases keep their names, the +five inheritance edges stay as derivations, model groups and attribute groups +remain addressable) so emitters can choose how much to collapse. See the +analyze module's recommendations for the reasoning. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +# Canonical maxOccurs="unbounded" marker in normalized particles. Kept as a +# string so it is self-describing in the JSON dump. +UNBOUNDED = "unbounded" + + +# --------------------------------------------------------------------------- # +# Type references +# --------------------------------------------------------------------------- # + + +@dataclass +class Ref: + """A reference to another type by name, tagged with where to resolve it.""" + + name: str + category: str # "complex" | "value" | "primitive" + + +# --------------------------------------------------------------------------- # +# Value types (lowered from simpleType and simpleContent bases) +# --------------------------------------------------------------------------- # + + +@dataclass +class EnumType: + name: str + base: str # primitive the tokens are drawn from (token/string) + values: list[str] + doc: str | None = None + kind: str = "enum" + + +@dataclass +class NumberType: + name: str + base: str # decimal/integer/positive_integer/non_negative_integer + min_inclusive: str | None = None + max_inclusive: str | None = None + min_exclusive: str | None = None + max_exclusive: str | None = None + doc: str | None = None + kind: str = "number" + + +@dataclass +class StringType: + name: str + base: str # string/token/nmtoken/date; a plain alias has no constraints + patterns: list[str] = field(default_factory=list) + min_length: str | None = None + max_length: str | None = None + length: str | None = None + doc: str | None = None + kind: str = "string" + + +@dataclass +class UnionMember: + # Exactly one is set: a Ref to another type, or inline enumeration literals. + ref: Ref | None = None + literals: list[str] | None = None + + +@dataclass +class UnionType: + name: str + members: list[UnionMember] + doc: str | None = None + kind: str = "union" + + +ValueType = EnumType | NumberType | StringType | UnionType + + +# --------------------------------------------------------------------------- # +# Attributes +# --------------------------------------------------------------------------- # + + +@dataclass +class Attr: + name: str + type: Ref + required: bool = False + default: str | None = None + fixed: str | None = None + doc: str | None = None + + +@dataclass +class AttributeGroup: + name: str + attributes: list[Attr] = field(default_factory=list) + attribute_groups: list[str] = field(default_factory=list) # nested refs + doc: str | None = None + + +# --------------------------------------------------------------------------- # +# Content model (normalized particles) +# --------------------------------------------------------------------------- # + + +@dataclass +class Element: + name: str + type: Ref + card: str # "required" | "optional" | "vector" + min: int = 1 + max: int | str = 1 # int or UNBOUNDED + doc: str | None = None + node: str = "element" + + +@dataclass +class GroupRef: + name: str + min: int = 1 + max: int | str = 1 + node: str = "group" + + +@dataclass +class Sequence: + items: list + min: int = 1 + max: int | str = 1 + node: str = "sequence" + + +@dataclass +class Choice: + items: list + min: int = 1 + max: int | str = 1 + node: str = "choice" + + +Particle = Element | GroupRef | Sequence | Choice + + +@dataclass +class Group: + name: str + content: Particle + doc: str | None = None + + +# --------------------------------------------------------------------------- # +# Complex types +# --------------------------------------------------------------------------- # + + +@dataclass +class ComplexType: + name: str + kind: str # "value" | "composite" | "derived" | "empty" + attributes: list[Attr] = field(default_factory=list) + attribute_groups: list[str] = field(default_factory=list) + value_type: Ref | None = None # kind == "value" (text content type) + base: str | None = None # kind == "derived" (parent complex type) + content: Particle | None = None # composite/derived particle + presence_only: bool = False # empty element used as a boolean flag + deps: list[str] = field(default_factory=list) # complex types referenced + doc: str | None = None + + +# --------------------------------------------------------------------------- # +# Schema root +# --------------------------------------------------------------------------- # + + +@dataclass +class Root: + element: str + type: str + + +@dataclass +class Ir: + source: str + builtins: dict[str, str] + value_types: list[ValueType] + groups: list[Group] + attribute_groups: list[AttributeGroup] + complex_types: list[ComplexType] # dependency-ordered + roots: list[Root] + dropped_dead: list[str] + stats: dict diff --git a/gen/ir/resolve.py b/gen/ir/resolve.py new file mode 100644 index 000000000..7e7477162 --- /dev/null +++ b/gen/ir/resolve.py @@ -0,0 +1,401 @@ +"""Collapsed views over the named structure the IR preserves. + +The IR keeps the schema's reusable structure addressable: a complex type lists +its attribute groups by name and leaves group references in its content tree, so +an emitter that wants mixins or shared structs can mirror them. Most emitters +instead want the collapsed view -- the full ordered attribute list, the content +with groups spliced in. Producing it means expanding attribute-group and +model-group references, deduping, and guarding cycles. That is schema reasoning, +so it lives here, once, rather than re-derived in every target's templates. + +Resolver is a pure read over the IR; it never mutates it. It depends only on the +three reusable tables (groups, attribute groups, complex types), not the whole +Ir, so build can use it mid-construction to compute dependencies. +""" + +from __future__ import annotations + +from gen.ir import model as ir + + +def _card(min_occurs: int, max_occurs: int | str) -> str: + """IR cardinality of an occurrence (mirrors gen.ir.build._cardinality, + which works in pre-lowering XSD space).""" + if max_occurs == ir.UNBOUNDED or max_occurs > 1: + return "vector" + return "optional" if min_occurs == 0 else "required" + + +class Resolver: + """Collapsed views over an IR's preserved named structure.""" + + def __init__( + self, + groups: list[ir.Group], + attribute_groups: list[ir.AttributeGroup], + complex_types: list[ir.ComplexType], + ): + self._groups = {g.name: g for g in groups} + self._agroups = {a.name: a for a in attribute_groups} + self._complex = {c.name: c for c in complex_types} + + @classmethod + def from_ir(cls, m: ir.Ir) -> "Resolver": + return cls(m.groups, m.attribute_groups, m.complex_types) + + # ----- attributes ------------------------------------------------------ # + + def attributes(self, ct: ir.ComplexType) -> list[ir.Attr]: + """ct's own attributes with its attribute groups expanded inline, in + declaration order, deduped by name (first wins). Excludes the base.""" + out: list[ir.Attr] = [] + self._add_attrs(ct.attributes, ct.attribute_groups, out, set(), set()) + return out + + def all_attributes(self, ct: ir.ComplexType) -> list[ir.Attr]: + """attributes() plus the base chain's attributes (base-most first), for + the flattened set an emitter needs when the target has no inheritance.""" + out: list[ir.Attr] = [] + seen: set[str] = set() + for c in self.base_chain(ct): + self._add_attrs(c.attributes, c.attribute_groups, out, seen, set()) + return out + + def _add_attrs(self, attrs, group_names, out, seen, seen_groups) -> None: + for a in attrs: + if a.name not in seen: + seen.add(a.name) + out.append(a) + for name in group_names: + ag = self._agroups.get(name) + if ag is not None and name not in seen_groups: + seen_groups.add(name) + self._add_attrs(ag.attributes, ag.attribute_groups, out, seen, seen_groups) + + # ----- content --------------------------------------------------------- # + + def content(self, ct: ir.ComplexType) -> ir.Particle | None: + """ct.content with every group reference spliced in: a self-contained + tree of elements/sequences/choices with no GroupRef nodes. Nesting and + all min/max bounds are preserved. None for types with no content.""" + return None if ct.content is None else self._inline(ct.content, ()) + + def _inline(self, p: ir.Particle, path: tuple[str, ...]) -> ir.Particle: + if isinstance(p, ir.Sequence): + return ir.Sequence([self._inline(i, path) for i in p.items], p.min, p.max) + if isinstance(p, ir.Choice): + return ir.Choice([self._inline(i, path) for i in p.items], p.min, p.max) + if isinstance(p, ir.GroupRef): + g = self._groups.get(p.name) + if g is None or p.name in path: # unknown or cyclic: leave the leaf + return p + body = self._inline(g.content, path + (p.name,)) + # The ref's occurrence wraps the group body's own. Drop the wrapper + # when the ref is exactly-one and so contributes nothing. + if p.min == 1 and p.max == 1: + return body + return ir.Sequence([body], p.min, p.max) + return p # Element: a leaf with an already-resolved Ref + + # ----- elements -------------------------------------------------------- # + + def elements(self, ct: ir.ComplexType) -> list[ir.Element]: + """Every element occurrence in ct's resolved content, in document order, + flattened across sequences/choices/groups. Drops the choice/sequence + grouping and keeps each occurrence's LOCAL cardinality; use content() + when the structure matters and flat_elements() for the effective, + deduplicated field view an emitter wants.""" + out: list[ir.Element] = [] + self._collect_elements(self.content(ct), out) + return out + + def _collect_elements(self, p, out) -> None: + if isinstance(p, (ir.Sequence, ir.Choice)): + for i in p.items: + self._collect_elements(i, out) + elif isinstance(p, ir.Element): + out.append(p) + + def flat_elements(self, ct: ir.ComplexType) -> list[tuple[ir.Element, str]]: + """Each distinct element name in ct's resolved content, in document + order of first occurrence, with its EFFECTIVE cardinality for a flat + one-field-per-name view: + + - an element under any repeated particle (max != 1) is a vector; + - an element under a choice, or under an optional wrapper, is at + most optional; + - only an element required along a spine of exactly-once sequences + stays required. + + Occurrences of the same name merge by co-occurrence analysis: if two + occurrences sit in different branches of one choice they are mutually + exclusive (at most one per instance: optional), but otherwise both + can appear in a single instance and the merged field must be a vector + (e.g. metronome's beat-unit, which appears on a branch's spine and + again inside that same branch's inner choice).""" + merged: dict[str, int] = {} # name -> index into out + paths: dict[str, list[tuple]] = {} # name -> choice paths seen + out: list[tuple[ir.Element, str]] = [] + rank = {"required": 0, "optional": 1, "vector": 2} + + def exclusive(a: tuple, b: tuple) -> bool: + """True when the two occurrence paths diverge at two different + branches of one choice node, so they can never co-occur.""" + i = 0 + while i < len(a) and i < len(b) and a[i] == b[i]: + i += 1 + return ( + i < len(a) + and i < len(b) + and a[i][0] == b[i][0] # same choice node + and a[i][1] != b[i][1] # different branches + ) + + def walk(node, forced: bool, repeated: bool, path: tuple) -> None: + if node is None: + return + if isinstance(node, ir.Element): + if repeated or node.card == "vector": + card = "vector" + elif forced and node.card == "required": + card = "required" + else: + card = "optional" + if node.name not in merged: + merged[node.name] = len(out) + paths[node.name] = [path] + out.append((node, card)) + return + i = merged[node.name] + prev_el, prev_card = out[i] + if all(exclusive(path, seen) for seen in paths[node.name]): + # Alternative branches: at most one occurs, but none is + # statically guaranteed. + card = max(card, prev_card, key=lambda c: rank[c]) + if card == "required": + card = "optional" + else: + # The occurrences can co-occur in one instance. + card = "vector" + paths[node.name].append(path) + out[i] = (prev_el, card) + return + if node.max == 0: + return # a never-occurring particle contributes nothing + once = node.min >= 1 and node.max == 1 + again = repeated or node.max != 1 + if isinstance(node, ir.Sequence): + for item in node.items: + walk(item, forced and once, again, path) + elif isinstance(node, ir.Choice): + for branch, item in enumerate(node.items): + walk(item, False, again, path + ((id(node), branch),)) + # GroupRef leaves cannot appear: content() spliced them. + + walk(self.content(ct), True, False, ()) + return out + + def all_flat_elements(self, ct: ir.ComplexType) -> list[tuple[ir.Element, str]]: + """flat_elements() merged across the base chain (base-most first, + first occurrence of a name wins), mirroring all_attributes, for the + flattened view a target without inheritance emits.""" + out: list[tuple[ir.Element, str]] = [] + seen: set[str] = set() + for c in self.base_chain(ct): + for element, card in self.flat_elements(c): + if element.name not in seen: + seen.add(element.name) + out.append((element, card)) + return out + + # ----- the field view (content projected onto ordered fields) ----------- # + # + # The grammar-preserving counterpart of flat_elements: the content tree + # normalized so that every top-level item is one *field* a structural + # emitter renders -- an element, a structural group reference, a choice, + # 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 + # reasoning -- nothing here knows what any target does with the view. + + @staticmethod + def occurs_product( + a_min: int, a_max: int | str, b_min: int, b_max: int | str + ) -> tuple[int, int | str]: + """The effective occurrence of `b` nested under a singleton wrapper + occurring `a` times.""" + if a_max == ir.UNBOUNDED or b_max == ir.UNBOUNDED: + max_: int | str = ir.UNBOUNDED + else: + max_ = a_max * b_max + return a_min * b_min, max_ + + def trivial_group(self, name: str) -> ir.Element | None: + """The single element occurrence a *trivial* group contributes + (a group whose whole normalized content is one element), or None + when the group is structural and keeps its identity.""" + g = self._groups.get(name) + if g is None: + return None + norm = self.normalized(g.content) + return norm if isinstance(norm, ir.Element) else None + + def group_shape(self, name: str) -> tuple[str, ir.Particle]: + """How a structural group presents as a type: ("choice", node) when + its normalized content collapses to a single choice (the type IS the + choice; reference sites absorb the choice's own occurrence), else + ("group", node) with the normalized content.""" + g = self._groups[name] + norm = self.normalized(g.content) + if isinstance(norm, ir.Choice): + return "choice", norm + return "group", norm + + def normalized(self, p: ir.Particle | None) -> ir.Particle | None: + """The field-view normalization: trivial groups spliced inline, + exactly-once singleton wrappers collapsed (with occurrence bounds + multiplied through), nested exactly-once sequences inlined into + their parent item list. Structural group references are preserved.""" + if p is None: + return None + return self._norm(p) + + def _norm(self, p: ir.Particle) -> ir.Particle: + if isinstance(p, ir.Element): + return p + if isinstance(p, ir.GroupRef): + e = self.trivial_group(p.name) + if e is None: + return p + mn, mx = self.occurs_product(p.min, p.max, e.min, e.max) + return ir.Element(e.name, e.type, _card(mn, mx), mn, mx, e.doc) + items = [self._norm(i) for i in p.items] + if isinstance(p, ir.Sequence): + flat: list = [] + for i in items: + # An exactly-once sequence nested in a sequence is the same + # grammar with the parentheses removed. + if isinstance(i, ir.Sequence) and i.min == 1 and i.max == 1: + flat.extend(i.items) + else: + flat.append(i) + items = flat + if len(items) == 1: + return self._collapse_singleton(items[0], p.min, p.max) + return ir.Sequence(items, p.min, p.max) + # Choice + if len(items) == 1: + return self._collapse_singleton(items[0], p.min, p.max) + return ir.Choice(items, p.min, p.max) + + def _collapse_singleton( + self, item: ir.Particle, w_min: int, w_max: int | str + ) -> ir.Particle: + """A wrapper holding exactly one item is that item with the wrapper's + occurrence multiplied through.""" + mn, mx = self.occurs_product(w_min, w_max, item.min, item.max) + if isinstance(item, ir.Element): + return ir.Element(item.name, item.type, _card(mn, mx), mn, mx, item.doc) + if isinstance(item, ir.GroupRef): + return ir.GroupRef(item.name, mn, mx) + if isinstance(item, ir.Sequence): + return ir.Sequence(item.items, mn, mx) + return ir.Choice(item.items, mn, mx) + + def field_nodes(self, content: ir.Particle | None) -> list[ir.Particle]: + """The ordered top-level field nodes of a content tree: the items of + the normalized exactly-once top sequence, or the single normalized + node itself. Each is an Element, a structural GroupRef, a Choice, or + a repeated/optional Sequence.""" + top = self.normalized(content) + if top is None: + return [] + if isinstance(top, ir.Sequence) and top.min == 1 and top.max == 1: + return list(top.items) + return [top] + + def nullable(self, p: ir.Particle) -> bool: + """True when the particle can match the empty element sequence.""" + if p.min == 0: + return True + if isinstance(p, ir.Element): + return False + if isinstance(p, ir.GroupRef): + g = self._groups.get(p.name) + return g is not None and self.nullable(g.content) + if isinstance(p, ir.Sequence): + return all(self.nullable(i) for i in p.items) + return any(self.nullable(i) for i in p.items) # Choice + + def first_names(self, p: ir.Particle) -> list[str]: + """The element names that can begin an instance of the particle, in + document order, deduped -- the dispatch set a strict in-order parser + tests an incoming element name against.""" + out: list[str] = [] + self._first(p, out) + return out + + def _first(self, p: ir.Particle, out: list[str]) -> None: + def add(name: str) -> None: + if name not in out: + out.append(name) + + if isinstance(p, ir.Element): + add(p.name) + elif isinstance(p, ir.GroupRef): + g = self._groups.get(p.name) + if g is not None: + self._first(g.content, out) + elif isinstance(p, ir.Choice): + for i in p.items: + self._first(i, out) + else: # Sequence: items up to and including the first non-nullable + for i in p.items: + self._first(i, out) + if not self.nullable(i): + break + + def particle_element_names(self, p: ir.Particle | None) -> list[str]: + """Every element name anywhere inside the particle (groups resolved), + in document order, deduped -- the 'known names' set an emitter uses + to tell a misplaced element from an unknown one.""" + out: list[str] = [] + self._names(p, out, ()) + return out + + def _names(self, p, out: list[str], path: tuple[str, ...]) -> None: + if p is None: + return + if isinstance(p, ir.Element): + if p.name not in out: + out.append(p.name) + elif isinstance(p, ir.GroupRef): + g = self._groups.get(p.name) + if g is not None and p.name not in path: + self._names(g.content, out, path + (p.name,)) + else: + for i in p.items: + self._names(i, out, path) + + # ----- derivation ------------------------------------------------------ # + + def base_chain(self, ct: ir.ComplexType) -> list[ir.ComplexType]: + """ct's derivation chain, base-most first, ending with ct itself.""" + chain: list[ir.ComplexType] = [] + cur: ir.ComplexType | None = ct + while cur is not None: + chain.append(cur) + cur = self._complex.get(cur.base) if cur.base else None + chain.reverse() + return chain + + # ----- dependencies ---------------------------------------------------- # + + def deps(self, ct: ir.ComplexType) -> set[str]: + """Complex types ct structurally depends on: its child element types + (groups resolved) plus its base. Drives the topological emit order.""" + d = {e.type.name for e in self.elements(ct) if e.type.category == "complex"} + if ct.base: + d.add(ct.base) + return d diff --git a/gen/ir/sounds.py b/gen/ir/sounds.py new file mode 100644 index 000000000..8b3a32bb4 --- /dev/null +++ b/gen/ir/sounds.py @@ -0,0 +1,101 @@ +"""Companion patch: fold sounds.xml into the IR as an open sound enum. + +The MusicXML XSD types the instrument-sound element as a bare xs:string. The +standard timbre identifiers it expects ("brass.alphorn", ...) live only in the +separately versioned sounds.xml companion file, not the schema. This patch +reads that file and rewrites the IR so instrument-sound resolves to a sound-id +enumeration unioned with an open string: the standard identifiers become typed +values, while any other string stays valid exactly as the schema allows. + +This is the one place the IR depends on an input beyond the XSD, and it runs +only when a target's config names a sounds file (see gen.config). The result +introduces no new IR shape -- it is an ordinary enum plus an ordinary union, +the same shape as font-size (the css-font-size enum unioned with decimal). +""" + +from __future__ import annotations + +import xml.etree.ElementTree as ET +from pathlib import Path + +from gen.ir import model as ir + +# The element keeps its name; its new type takes the element's name (the +# MusicXML convention, e.g. element note has type note), and the enumeration of +# identifiers gets a sub-name -- mirroring font-size over css-font-size. +ELEMENT = "instrument-sound" +UNION = "instrument-sound" +ENUM = "sound-id" + + +def read_sound_ids(path) -> list[str]: + """The id of every in a sounds.xml companion file, in document + order. The file's DOCTYPE points at an external DTD; ElementTree ignores it, + so this stays offline.""" + root = ET.parse(Path(path)).getroot() + return [s.get("id") for s in root.findall("sound") if s.get("id")] + + +def patch_sounds(m: ir.Ir, sound_ids: list[str]) -> int: + """Fold sound_ids into m in place: add the sound-id enum and instrument-sound + union, then retype every instrument-sound element from string to that union. + Returns the number of element occurrences retyped, which must be >= 1.""" + enum = ir.EnumType( + name=ENUM, + base="token", + values=list(sound_ids), + doc=( + "Standard MusicXML instrument sound identifiers. The XSD types " + "instrument-sound as xs:string and lists these values only in the " + "sounds.xml companion file; the generator injects them here." + ), + ) + union = ir.UnionType( + name=UNION, + members=[ + ir.UnionMember(ref=ir.Ref(ENUM, "value")), + ir.UnionMember(ref=ir.Ref("string", "primitive")), + ], + doc=( + "The instrument-sound value: one of the standard sound-id " + "identifiers, or any other string. The schema leaves the content " + "open (xs:string), so the string member is intrinsic, not a fallback." + ), + ) + # Deps-first invariant: the enum (no value deps) precedes the union that + # references it, and nothing already in the list references either, so + # appending the pair keeps value_types topologically ordered. + m.value_types.append(enum) + m.value_types.append(union) + + # instrument-sound is declared inside the virtual-instrument-data group, not + # a complex type's content, so retype across groups as well as complex types. + new_type = ir.Ref(UNION, "value") + retyped = sum( + _retype(ct.content, new_type) for ct in m.complex_types if ct.content is not None + ) + retyped += sum(_retype(g.content, new_type) for g in m.groups) + if retyped == 0: + raise ValueError(f"no {ELEMENT!r} element found to patch; schema changed?") + + _bump_stats(m.stats, len(sound_ids)) + return retyped + + +def _retype(particle: ir.Particle, new_type: ir.Ref) -> int: + """Retype every ELEMENT occurrence reachable in particle. GroupRef leaves are + left alone -- their target group is retyped where it is defined.""" + if isinstance(particle, (ir.Sequence, ir.Choice)): + return sum(_retype(i, new_type) for i in particle.items) + if isinstance(particle, ir.Element) and particle.name == ELEMENT: + particle.type = new_type + return 1 + return 0 + + +def _bump_stats(stats: dict, n_ids: int) -> None: + stats["value_types"] = stats.get("value_types", 0) + 2 + kinds = stats.setdefault("value_kinds", {}) + kinds["enum"] = kinds.get("enum", 0) + 1 + kinds["union"] = kinds.get("union", 0) + 1 + stats["companion_sound_ids"] = n_ids diff --git a/gen/names.py b/gen/names.py new file mode 100644 index 000000000..40b716c80 --- /dev/null +++ b/gen/names.py @@ -0,0 +1,182 @@ +"""Name expansion: tokenize wire names, recase per convention, sanitize. + +A fundamental (wire) name is split into an ordered word vector of lowercase +words, then recased by each registered convention. The wire form is preserved +untouched alongside the casings -- tokenization feeds only the cased +identifiers, never serialization (design R3). + +Conventions live in a registry keyed by name, so adding one later is +registering one function; every Name simply grows a key (design R1). + +This module is deliberately a leaf: it is shared vocabulary for the config +loader (which validates convention names and rename-entry keys) and for the +Plates projection, so it sits below both and imports neither. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +# Word separators, split on and consumed. Hyphen covers ordinary kebab names; +# dot covers sound ids like `brass.alphorn`; whitespace covers space-separated +# enum values like `up down`; colon covers external refs like `xml:lang`. +_SEPARATORS = set("-._: \t\n\r\v\f") + +# Words uppercased whole by capitalizing conventions (Pascal, non-leading +# camel). Config-extensible via [naming] acronyms. +DEFAULT_ACRONYMS = ("midi", "id", "xml", "css", "smufl", "uri", "url") + +# Fallback word vector for wire names that tokenize to nothing (the empty enum +# value of positive-integer-or-empty and a few *-value enums). The wire form +# stays ""; only the identifier gets a name. A target wanting a different word +# for a particular enum renames it: [rename.enum-value.] "" = "none". +EMPTY_WORD = "empty" + + +@dataclass +class Name: + """The neutral/bound name bundle. `wire` is the immutable on-the-wire + string (never a code identifier); `words` is the tokenized vector the + casings expand from; `cased` maps convention name -> identifier, filled + by iterating the convention registry.""" + + wire: str + words: tuple[str, ...] + cased: dict[str, str] + + @property + def pascal(self) -> str: + return self.cased["pascal"] + + @property + def camel(self) -> str: + return self.cased["camel"] + + @property + def snake(self) -> str: + return self.cased["snake"] + + @property + def kebab(self) -> str: + return self.cased["kebab"] + + @property + def screaming(self) -> str: + return self.cased["screaming"] + + +def tokenize(wire: str, empty_word: str = EMPTY_WORD) -> tuple[str, ...]: + """Split a wire name into its canonical lowercase word vector.""" + tokens: list[str] = [] + current: list[str] = [] + for ch in wire: + if ch in _SEPARATORS: + if current: + tokens.append("".join(current)) + current = [] + else: + current.append(ch) + if current: + tokens.append("".join(current)) + + words: list[str] = [] + for token in tokens: + words.extend(w.lower() for w in _split_case_transitions(token)) + return tuple(words) if words else (empty_word,) + + +def _split_case_transitions(token: str) -> list[str]: + """Split an already-mixed-case token at a lower-to-upper boundary + (`fooBar` -> foo, Bar) and at an acronym boundary, where an uppercase run + is followed by uppercase+lowercase (`MIDIChannel` -> MIDI, Channel): the + last capital of the run begins the next word. Letter-digit boundaries do + not split; digits ride with their adjacent letters.""" + if not token: + return [] + starts = [0] + for i in range(1, len(token)): + prev, cur = token[i - 1], token[i] + if cur.isupper() and prev.islower(): + starts.append(i) + elif ( + cur.isupper() + and prev.isupper() + and i + 1 < len(token) + and token[i + 1].islower() + ): + starts.append(i) + return [token[a:b] for a, b in zip(starts, starts[1:] + [len(token)])] + + +def _capitalize(word: str, acronyms: frozenset[str]) -> str: + if word in acronyms: + return word.upper() + if word and word[0].isalpha(): + return word[0].upper() + word[1:] + return word # digit-led words like `1024th` stay lowercase + + +# Each convention maps (word vector, acronym set) -> identifier string. The +# camelCase leading word is always fully lowercased, so a leading acronym +# yields `midiChannel`, never `MIDIChannel`. snake/kebab/screaming are +# case-uniform and ignore the acronym set. +CONVENTIONS = { + "pascal": lambda ws, ac: "".join(_capitalize(w, ac) for w in ws), + "camel": lambda ws, ac: ws[0] + "".join(_capitalize(w, ac) for w in ws[1:]), + "snake": lambda ws, ac: "_".join(ws), + "kebab": lambda ws, ac: "-".join(ws), + "screaming": lambda ws, ac: "_".join(w.upper() for w in ws), +} + +# How a convention joins two already-cased parts when an identifier is +# composed from a scope plus a member (a type name plus a variant name, for +# targets whose enum constants share one namespace). Concatenating +# conventions join with nothing; delimited conventions reuse their delimiter. +JOINERS = { + "pascal": "", + "camel": "", + "snake": "_", + "kebab": "-", + "screaming": "_", +} + + +def sanitize_identifier(ident: str, reserved: frozenset[str], invalid_prefix: str = "_") -> str: + """Make a recased identifier legal for a code target: non-identifier + characters become underscores, a leading digit or empty result gets the + configured prefix, and reserved words get a trailing underscore. The + pre-sanitized casing stays available on the Name; collision detection + runs on the sanitized result.""" + out = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in ident) + if not out or out[0].isdigit(): + out = invalid_prefix + out + if out in reserved: + out += "_" + return out + + +class NameFactory: + """Builds Name bundles: tokenize once, expand every registered convention, + honoring a fundamental rename (re-expands all casings from the new root) + and per-convention overrides (pin one flavor, leave the rest expanded).""" + + def __init__(self, acronyms=DEFAULT_ACRONYMS): + # The acronym set matches against already-lowercased words, so it is + # normalized here: acronyms = ["MIDI"] must behave like ["midi"]. + self.acronyms = frozenset(a.lower() for a in acronyms) + + def make( + self, + wire: str, + fundamental: str | None = None, + overrides: dict[str, str] | None = None, + pluralize: bool = False, + ) -> Name: + words = tokenize(fundamental if fundamental is not None else wire) + if pluralize: + words = words[:-1] + (words[-1] + "s",) + cased = {conv: fn(words, self.acronyms) for conv, fn in CONVENTIONS.items()} + if overrides: + for conv, value in overrides.items(): + cased[conv] = value + return Name(wire=wire, words=words, cased=cased) diff --git a/gen/naming.base.toml b/gen/naming.base.toml new file mode 100644 index 000000000..3c5d27a3b --- /dev/null +++ b/gen/naming.base.toml @@ -0,0 +1,19 @@ +# Shared naming base for every target ([naming] extends in each config.toml). +# Holds renames that are forced by the schema itself rather than by any one +# language, so they stay identical across targets. A target's own entries win +# over these on any conflict. + +# `barline` carries both child ELEMENTS segno/coda (the visible symbols) and +# ATTRIBUTES segno/coda (sound/playback jump markers). Any field casing +# collapses each pair to one identifier, so every code target collides. The +# elements keep their wire-true names; the attributes are qualified. +[rename.attribute.barline] +segno = "segno-sound" +coda = "coda-sound" + +# `score-part` carries both a child ELEMENT literally named "group" and an +# anonymous midi-device/midi-instrument pair sequence that the content +# projection hoists as `score-part-group` -- whose field name would also be +# "group". Schema-forced, so every structural target collides identically. +[rename.type] +"score-part-group" = "score-part-midi-group" diff --git a/gen/parse.py b/gen/parse.py deleted file mode 100644 index 7faa68dd1..000000000 --- a/gen/parse.py +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/env python3 -"""Parse ``docs/musicxml.xsd`` into a target-neutral, self-contained XSD model. - -This module owns every step that touches the XML: the dataclasses describing XSD constructs, -the ``XsdModel`` parser, and ``NodeId`` assignment. It produces pure XSD facts -- no C++ names, -type maps, or output formatting -- so that a future Rust / docs / JSON-schema backend can consume -the same model. - -Structural configuration that the original hand-generation baked in (which anonymous sequences -become synthetic group classes, which inherited groups are renamed, which groups are emitted as -their own class) is C++-aware and lives in ``generate.py``. It is injected via ``ParseConfig`` so -that this module stays free of hardcoded config. - -Self-containment: after parsing, ``model.tree`` is not retained. ``model.root`` is still read by -the four bespoke family handlers in ``generate.py`` (harmony-chord, score-wrapper, music-data, -full-note); severing it fully waits on those handlers migrating to parsed data. -""" -import re -import xml.etree.ElementTree as ET -from collections import OrderedDict -from dataclasses import dataclass, field -from typing import Optional - -from ids import NodeId - -XS = "{http://www.w3.org/2001/XMLSchema}" - - -def pascal(name: str) -> str: - return "".join(p[:1].upper() + p[1:] for p in re.split(r"[-_]", name)) - - -@dataclass -class ParseConfig: - """C++-aware structural config injected into the parser by ``generate.py``. - - The four set fields are mutated in place during parsing (the parser discovers and records - synthetic groups); ``generate.py`` reads the same set objects after parse. The dict fields - are read-only inputs. - """ - generate_groups: set - synthetic_optional_groups: set - synthetic_unbounded_groups: set - suppress_group_suffix: set - extension_optional_group_rename: dict - nested_optional_sequence_as_group: dict - unbounded_sequence_as_group: dict - - -@dataclass -class XsdAttribute: - name: str - type_name: str - use: str = "optional" - xml_name: str = "" - node_id: Optional[NodeId] = field(default=None, compare=False) - - def get_xml_name(self) -> str: - return self.xml_name or self.name - - -@dataclass -class XsdEnumType: - name: str - values: list - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class XsdChildRef: - """A reference to a child element within a sequence or choice.""" - element_name: str - min_occurs: int = 1 - max_occurs: int = 1 # -1 = unbounded - is_group: bool = False - node_id: Optional[NodeId] = field(default=None, compare=False) - - -# --------------------------------------------------------------------------- -# Content Tree Nodes (preserves nested choice/sequence structure) -# --------------------------------------------------------------------------- - - -@dataclass -class ElementRefNode: - element_name: str - min_occurs: int = 1 - max_occurs: int = 1 # -1 = unbounded - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class GroupRefNode: - group_name: str - min_occurs: int = 1 - max_occurs: int = 1 - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class SequenceNode: - children: list = field(default_factory=list) - min_occurs: int = 1 - max_occurs: int = 1 - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class ChoiceNode: - branches: list = field(default_factory=list) - min_occurs: int = 1 - max_occurs: int = 1 - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class XsdComplexType: - name: str - attributes: list = field(default_factory=list) - children: list = field(default_factory=list) - has_simple_content: bool = False - simple_content_base: str = "" - has_choice: bool = False - choice_children: list = field(default_factory=list) - mixed: bool = False - extension_base: str = "" - content_tree: Optional[object] = None - node_id: Optional[NodeId] = field(default=None, compare=False) - - -@dataclass -class XsdElement: - name: str - type_name: str = "" - anonymous_type: Optional[XsdComplexType] = None - node_id: Optional[NodeId] = field(default=None, compare=False) - - -class XsdModel: - def __init__(self, xsd_path, cfg: ParseConfig): - self.cfg = cfg - # The ElementTree is a parse-time detail and is not retained on the model - # (model.tree is severed). model.root is kept only because four bespoke - # family handlers in generate.py still read it directly. - tree = ET.parse(xsd_path) - self.root = tree.getroot() - self.enum_types: dict[str, XsdEnumType] = OrderedDict() - # enum_docs: enum simpleType name -> its annotation/documentation text (or ""). - # Extracted here so generation never re-walks the XML for enum docs. - self.enum_docs: dict[str, str] = OrderedDict() - # Named complexTypes whose direct content is xs:complexContent or an xs:group - # ref. Recorded so the "pattern B" predicate does not re-walk the XML. - self.complex_content_or_group_cts: set = set() - self.complex_types: dict[str, XsdComplexType] = OrderedDict() - self.simple_types: dict[str, dict] = OrderedDict() - self.attribute_groups: dict[str, list] = OrderedDict() - self.groups: dict[str, list] = OrderedDict() - self.elements: dict[str, XsdElement] = OrderedDict() - self.class_names: set = set() - self._parse() - - def _parse(self): - self._parse_attribute_groups() - self._parse_simple_types() - self._parse_complex_types() - self._parse_groups() - self._inline_non_generated_groups() - self._parse_elements() - self._build_class_names() - self._assign_ids() - - def _parse_attribute_groups(self): - for ag in self.root.iter(f"{XS}attributeGroup"): - name = ag.get("name") - if name: - self.attribute_groups[name] = self._collect_attrs(ag) - - def _collect_attrs(self, node, seen=None): - if seen is None: - seen = set() - out = [] - for child in node: - tag = child.tag - if tag == f"{XS}attribute" and child.get("name"): - out.append(XsdAttribute( - name=child.get("name"), - type_name=child.get("type", "xs:string"), - use=child.get("use", "optional"), - )) - elif tag == f"{XS}attribute" and child.get("ref"): - ref = child.get("ref") - local_name = ref.split(":")[-1] if ":" in ref else ref - out.append(XsdAttribute( - name=local_name, - type_name=ref, - use=child.get("use", "optional"), - xml_name=ref, - )) - elif tag == f"{XS}attributeGroup" and child.get("ref"): - ref = child.get("ref") - if ref not in seen: - seen.add(ref) - if ref in self.attribute_groups: - out.extend(self.attribute_groups[ref]) - else: - deferred = self._collect_attrs_from_ref(ref) - out.extend(deferred) - elif tag in (f"{XS}simpleContent", f"{XS}complexContent", - f"{XS}extension", f"{XS}restriction"): - out.extend(self._collect_attrs(child, seen)) - return out - - def _collect_attrs_from_ref(self, ref): - for ag in self.root.iter(f"{XS}attributeGroup"): - if ag.get("name") == ref: - return self._collect_attrs(ag) - return [] - - @staticmethod - def _extract_documentation(node) -> str: - ann = node.find(f"{XS}annotation") - if ann is not None: - doc_el = ann.find(f"{XS}documentation") - if doc_el is not None and doc_el.text: - return doc_el.text.strip() - return "" - - def _parse_simple_types(self): - union_members = set() - for st in self.root.iter(f"{XS}simpleType"): - union = st.find(f"{XS}union") - if union is not None and union.get("memberTypes"): - for m in union.get("memberTypes").split(): - union_members.add(m) - - for st in self.root.iter(f"{XS}simpleType"): - name = st.get("name") - if not name: - continue - restr = st.find(f"{XS}restriction") - if restr is not None: - vals = [e.get("value") for e in restr.findall(f"{XS}enumeration")] - if vals: - self.enum_types[name] = XsdEnumType(name=name, values=vals) - self.enum_docs[name] = self._extract_documentation(st) - self.simple_types[name] = { - "kind": "enum", - "base": restr.get("base", ""), - "values": vals, - "is_union_member": name in union_members, - } - else: - self.simple_types[name] = { - "kind": "restriction", - "base": restr.get("base", ""), - } - else: - union = st.find(f"{XS}union") - if union is not None: - self.simple_types[name] = { - "kind": "union", - "member_types": union.get("memberTypes", "").split(), - } - else: - self.simple_types[name] = {"kind": "other"} - - def _parse_complex_types(self): - for ct in self.root.iter(f"{XS}complexType"): - name = ct.get("name") - if not name: - continue - ctype = XsdComplexType(name=name, mixed=ct.get("mixed") == "true") - ctype.attributes = self._collect_attrs(ct) - - if (ct.find(f"{XS}complexContent") is not None - or ct.find(f"{XS}group") is not None): - self.complex_content_or_group_cts.add(name) - - sc = ct.find(f"{XS}simpleContent") - if sc is not None: - ctype.has_simple_content = True - ext = sc.find(f"{XS}extension") - if ext is not None: - ctype.simple_content_base = ext.get("base", "") - ctype.attributes = self._collect_attrs(ext) - - cc = ct.find(f"{XS}complexContent") - if cc is not None: - ext = cc.find(f"{XS}extension") - if ext is not None: - ctype.extension_base = ext.get("base", "") - ext_seq = ext.find(f"{XS}sequence") - if ext_seq is not None: - ctype.children = self._parse_children(ext_seq, name) - - if not ctype.children: - seq = ct.find(f"{XS}sequence") - if seq is not None: - ctype.children = self._parse_children(seq, name) - - if not ctype.children: - grp = ct.find(f"{XS}group") - if grp is not None and grp.get("ref"): - ctype.children = self._parse_children(ct, name) - - choice = ct.find(f"{XS}choice") - if choice is not None: - ctype.has_choice = True - ctype.choice_children = self._parse_children(choice, name) - - seq_for_tree = ct.find(f"{XS}sequence") - choice_for_tree = ct.find(f"{XS}choice") - if seq_for_tree is not None: - min_o = int(seq_for_tree.get("minOccurs", "1")) - max_o = seq_for_tree.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - tree_children = self._parse_content_tree(seq_for_tree) - ctype.content_tree = SequenceNode( - children=tree_children, min_occurs=min_o, max_occurs=max_o) - elif choice_for_tree is not None: - min_o = int(choice_for_tree.get("minOccurs", "1")) - max_o = choice_for_tree.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - tree_branches = self._parse_content_tree(choice_for_tree) - ctype.content_tree = ChoiceNode( - branches=tree_branches, min_occurs=min_o, max_occurs=max_o) - - self.complex_types[name] = ctype - self._resolve_complex_content_extensions() - - def _resolve_complex_content_extensions(self): - for ctype in self.complex_types.values(): - if not ctype.extension_base: - continue - base = self.complex_types.get(ctype.extension_base) - if not base: - continue - ctype.attributes = base.attributes + ctype.attributes - if not ctype.children and base.children: - # The original codegen usually flattened inherited children: - # a type extending ``time-modification`` would expose the - # inner normal-type / normal-dot directly. For specific - # extending types (see EXTENSION_OPTIONAL_GROUP_RENAME) the - # original kept the inherited synthetic optional group as a - # distinctly-named wrapper sub-element on the extending - # type. Honor that override here; otherwise flatten as - # before. - rename_map = self.cfg.extension_optional_group_rename.get( - ctype.name, {}) - inherited = [] - for c in base.children: - if (c.is_group - and c.element_name in self.cfg.synthetic_optional_groups): - if c.element_name in rename_map: - new_name = rename_map[c.element_name] - if new_name not in self.groups: - self.groups[new_name] = list( - self.groups.get(c.element_name, [])) - self.cfg.generate_groups.add(new_name) - self.cfg.synthetic_optional_groups.add(new_name) - self.cfg.suppress_group_suffix.add(new_name) - inherited.append(XsdChildRef( - element_name=new_name, - min_occurs=0, - max_occurs=1, - is_group=True, - )) - continue - for gm in self.groups.get(c.element_name, []): - inherited.append(XsdChildRef( - element_name=gm.element_name, - min_occurs=gm.min_occurs, - max_occurs=gm.max_occurs, - is_group=gm.is_group, - )) - else: - inherited.append(c) - ctype.children = inherited + ctype.children - - def _parse_children(self, container, parent_type_name: Optional[str] = None): - children = [] - for child in container: - if child.tag == f"{XS}element": - elem_name = child.get("ref") or child.get("name") - if elem_name: - min_o = int(child.get("minOccurs", "1")) - max_o = child.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - children.append(XsdChildRef( - element_name=elem_name, min_occurs=min_o, max_occurs=max_o - )) - elif child.tag == f"{XS}group": - ref = child.get("ref") - if ref: - min_o = int(child.get("minOccurs", "1")) - max_o = child.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - children.append(XsdChildRef( - element_name=ref, min_occurs=min_o, max_occurs=max_o, - is_group=True, - )) - elif child.tag == f"{XS}sequence": - seq_min = int(child.get("minOccurs", "1")) - seq_max = child.get("maxOccurs", "1") - seq_max = -1 if seq_max == "unbounded" else int(seq_max) - # An anonymous nested optional sequence (minOccurs=0, - # maxOccurs=1) inside a parent sequence is the XSD shape that - # the original code generation sometimes promoted to a - # synthetic group class (e.g. NormalTypeNormalDotGroup inside - # time-modification). The naming and the decision to promote - # were human choices and are not consistent across the - # schema -- page-layout, for example, flattens the same - # shape rather than promoting it. Opt-in via - # NESTED_OPTIONAL_SEQUENCE_AS_GROUP keyed on the enclosing - # complex-type name. - if (seq_min == 0 and seq_max == 1 - and parent_type_name in self.cfg.nested_optional_sequence_as_group): - nested_children = self._parse_children(child) - if nested_children and all( - not c.is_group for c in nested_children - ): - group_name = self.cfg.nested_optional_sequence_as_group[ - parent_type_name] - group_ref = self._synthesize_optional_group( - group_name, nested_children) - children.append(group_ref) - continue - if (seq_min == 0 and seq_max == -1 - and parent_type_name in self.cfg.unbounded_sequence_as_group): - nested_children = self._parse_children(child) - if nested_children and all( - not c.is_group for c in nested_children - ): - group_name = self.cfg.unbounded_sequence_as_group[ - parent_type_name] - group_ref = self._synthesize_unbounded_group( - group_name, nested_children) - children.append(group_ref) - continue - children.extend(self._parse_children(child, parent_type_name)) - elif child.tag == f"{XS}choice": - pass - return children - - def _synthesize_optional_group(self, group_name: str, - nested_children: list) -> "XsdChildRef": - # ``group_name`` is the hyphenated-lowercase ref name (e.g. - # "normal-type-normal-dot") so that pascal(name) + "Group" via - # child_class_name + GENERATE_GROUPS yields the expected class name - # (e.g. NormalTypeNormalDotGroup). - if group_name not in self.groups: - self.groups[group_name] = list(nested_children) - self.cfg.generate_groups.add(group_name) - self.cfg.synthetic_optional_groups.add(group_name) - return XsdChildRef( - element_name=group_name, - min_occurs=0, - max_occurs=1, - is_group=True, - ) - - def _synthesize_unbounded_group(self, group_name: str, - nested_children: list) -> "XsdChildRef": - # Members of an unbounded synthetic group are forced to min_occurs=0, - # max_occurs=1 because the unbounded multiplicity belongs to the - # wrapping sequence at the parent. The original codegen produced - # group classes (e.g. MidiDeviceInstrumentGroup) with all-optional - # members in this exact shape. - if group_name not in self.groups: - members = [] - for c in nested_children: - members.append(XsdChildRef( - element_name=c.element_name, - min_occurs=0, - max_occurs=1, - is_group=c.is_group, - )) - self.groups[group_name] = members - self.cfg.generate_groups.add(group_name) - self.cfg.synthetic_unbounded_groups.add(group_name) - return XsdChildRef( - element_name=group_name, - min_occurs=0, - max_occurs=-1, - is_group=True, - ) - - def _parse_content_tree(self, container): - nodes = [] - for child in container: - min_o = int(child.get("minOccurs", "1")) - max_o = child.get("maxOccurs", "1") - max_o = -1 if max_o == "unbounded" else int(max_o) - if child.tag == f"{XS}element": - name = child.get("ref") or child.get("name") - if name: - nodes.append(ElementRefNode( - element_name=name, min_occurs=min_o, max_occurs=max_o)) - elif child.tag == f"{XS}group": - ref = child.get("ref") - if ref: - nodes.append(GroupRefNode( - group_name=ref, min_occurs=min_o, max_occurs=max_o)) - elif child.tag == f"{XS}sequence": - sub = self._parse_content_tree(child) - nodes.append(SequenceNode( - children=sub, min_occurs=min_o, max_occurs=max_o)) - elif child.tag == f"{XS}choice": - branches = self._parse_content_tree(child) - nodes.append(ChoiceNode( - branches=branches, min_occurs=min_o, max_occurs=max_o)) - return nodes - - def _parse_groups(self): - for grp in self.root.iter(f"{XS}group"): - name = grp.get("name") - if name: - seq = grp.find(f"{XS}sequence") - if seq is not None: - self.groups[name] = self._parse_children(seq) - else: - choice = grp.find(f"{XS}choice") - if choice is not None: - self.groups[name] = self._parse_children(choice) - - def _inline_group_children(self, children: list) -> list: - result = [] - for child in children: - if child.is_group and child.element_name not in self.cfg.generate_groups: - group_children = self.groups.get(child.element_name, []) - inlined = self._inline_group_children(group_children) - for gc in inlined: - min_o = gc.min_occurs - if child.min_occurs == 0: - min_o = 0 - gc_copy = XsdChildRef( - element_name=gc.element_name, - min_occurs=min_o, - max_occurs=gc.max_occurs, - is_group=gc.is_group, - ) - result.append(gc_copy) - else: - result.append(child) - return result - - def _inline_non_generated_groups(self): - for ct in self.complex_types.values(): - if ct.children: - ct.children = self._inline_group_children(ct.children) - - def _parse_elements(self): - for el in self.root.iter(f"{XS}element"): - name = el.get("name") - if not name: - continue - if name in self.elements: - continue - typ = el.get("type", "") - anon = None - ct = el.find(f"{XS}complexType") - if ct is not None: - anon = XsdComplexType( - name=f"_anon_{name}", mixed=ct.get("mixed") == "true") - anon.attributes = self._collect_attrs(ct) - sc = ct.find(f"{XS}simpleContent") - if sc is not None: - anon.has_simple_content = True - ext = sc.find(f"{XS}extension") - if ext is not None: - anon.simple_content_base = ext.get("base", "") - seq = ct.find(f"{XS}sequence") - if seq is not None: - anon.children = self._parse_children(seq) - self.elements[name] = XsdElement(name=name, type_name=typ, anonymous_type=anon) - - def _build_class_names(self): - for name in self.elements: - self.class_names.add(pascal(name)) - for name in self.complex_types: - self.class_names.add(pascal(name)) - self._build_type_usage_counts() - - def _build_type_usage_counts(self): - self.type_usage_count = {} - for elem in self.elements.values(): - if elem.type_name: - self.type_usage_count[elem.type_name] = \ - self.type_usage_count.get(elem.type_name, 0) + 1 - - # -- NodeId assignment --------------------------------------------------- - # Total coverage: every dataclass-backed node reachable from a top-level - # construct receives a NodeId. Additive and unconsumed -- no generation - # logic reads these yet. (simple_types, groups, and attribute_groups are - # stored as plain dicts/lists rather than node objects, so the construct - # itself has no field to carry an ID; its member nodes are still keyed - # under the construct's root ID.) - - def _assign_ids(self): - for name, enum in self.enum_types.items(): - enum.node_id = NodeId.root("st", name) - for name, attrs in self.attribute_groups.items(): - self._assign_attr_ids(NodeId.root("ag", name), attrs) - for name, children in self.groups.items(): - self._assign_childref_ids(NodeId.root("gr", name), children) - for name, ctype in self.complex_types.items(): - ctype.node_id = NodeId.root("cx", name) - self._assign_complex_type_ids(ctype.node_id, ctype) - for name, elem in self.elements.items(): - elem.node_id = NodeId.root("el", name) - if elem.anonymous_type is not None: - anon_id = elem.node_id.anon_child("cx", 0) - elem.anonymous_type.node_id = anon_id - self._assign_complex_type_ids(anon_id, elem.anonymous_type) - - def _assign_complex_type_ids(self, base: NodeId, ctype: XsdComplexType): - self._assign_attr_ids(base, ctype.attributes) - self._assign_childref_ids(base, ctype.children) - self._assign_childref_ids(base, ctype.choice_children) - if ctype.content_tree is not None: - self._assign_tree_ids(base, [ctype.content_tree], {}) - - @staticmethod - def _assign_attr_ids(base: NodeId, attrs: list): - for attr in attrs: - attr.node_id = base.named_child("at", attr.name) - - @staticmethod - def _assign_childref_ids(base: NodeId, children: list): - for child in children: - kind = "gr" if child.is_group else "el" - child.node_id = base.named_child(kind, child.element_name) - - def _assign_tree_ids(self, base: NodeId, nodes: list, counters: dict): - for node in nodes: - if isinstance(node, ElementRefNode): - node.node_id = base.named_child("el", node.element_name) - elif isinstance(node, GroupRefNode): - node.node_id = base.named_child("gr", node.group_name) - elif isinstance(node, SequenceNode): - ordinal = counters.get("seq", 0) - counters["seq"] = ordinal + 1 - node.node_id = base.anon_child("seq", ordinal) - self._assign_tree_ids(node.node_id, node.children, {}) - elif isinstance(node, ChoiceNode): - ordinal = counters.get("choice", 0) - counters["choice"] = ordinal + 1 - node.node_id = base.anon_child("choice", ordinal) - self._assign_tree_ids(node.node_id, node.branches, {}) diff --git a/gen/plates/__init__.py b/gen/plates/__init__.py new file mode 100644 index 000000000..a3fd8d2ae --- /dev/null +++ b/gen/plates/__init__.py @@ -0,0 +1,29 @@ +"""The Plates: the template-facing, per-target projection of the IR. + +See gen.plates.model for the data shape, gen.plates.build for the projection, +and docs/ai/design/plates.md for the design. +""" + +from gen.plates.build import PlatesError, build_plates +from gen.plates.model import Plates + +__all__ = ["Plates", "PlatesError", "build_plates", "build_for_config"] + + +def build_for_config(config_path): + """The whole pipeline for one target, shared by the CLI and tests: load + the config, lower its pinned XSD to the IR, apply companion patches, and + project. Returns (plates, config).""" + from gen.config import load as load_config + from gen.ir.build import build_ir + from gen.xsd.parser import parse + + cfg = load_config(config_path) + if cfg.xsd is None: + raise FileNotFoundError(f"config has no [input] xsd: {cfg.path}") + m = build_ir(parse(cfg.xsd), source=cfg.xsd.stem) + if cfg.sounds_xml is not None: + from gen.ir.sounds import patch_sounds, read_sound_ids + + patch_sounds(m, read_sound_ids(cfg.sounds_xml)) + return build_plates(m, cfg), cfg diff --git a/gen/plates/build.py b/gen/plates/build.py new file mode 100644 index 000000000..d32b5f236 --- /dev/null +++ b/gen/plates/build.py @@ -0,0 +1,1044 @@ +"""Project the IR onto one target: build the Plates. + +The build consumes the IR and its Resolver (it never re-derives a schema +fact: splicing, base-chain merging, and effective cardinality all come from +gen.ir.resolve) plus a Config, and produces the materialized Plates tree. +Three phases, each failing loud: + + 1. Config-against-IR validation: every [rename.*] key must name something + the IR actually contains, and every [types] key a real primitive (a + stale or misspelled key is a build error). + 2. Projection: names are tokenized and recased, renames and overrides + applied, identifiers composed per the target's scoping and sanitized, + types mapped, strategies and files assigned. + 3. Collision detection (gen.plates.check): distinct wire names that + collapsed to one identifier under the projection are reported together. +""" + +from __future__ import annotations + +import re + +from gen.config import Config +from gen.ir import model as ir +from gen.ir.build import PRIMITIVES +from gen.ir.resolve import Resolver +from gen.names import DEFAULT_ACRONYMS, JOINERS, NameFactory, sanitize_identifier +from gen.plates.check import run_checks +from gen.plates.model import ( + Alternative, + ChoicePlate, + ClampStep, + ComplexPlate, + ContentField, + EnumPlate, + GroupPlate, + Member, + Name, + NumberBounds, + NumberPlate, + PlateRef, + Plates, + StringPlate, + TargetInfo, + UnionPlate, + UnionPlateMember, + Variant, +) + + +# Primitive-implied lower bounds the schema leaves unstated; part of the +# uniform clamp policy (see model.ClampStep and data/README.md). +_IMPLIED_MIN = {"positive_integer": 1, "non_negative_integer": 0} + +# The epsilon an exclusive DECIMAL bound clamps past (an exclusive integer +# bound clamps to the next integer). Matches the corpus duration fixup. +_EPSILON = 1e-6 + +# The numeric IR primitives ((see gen.ir.build.PRIMITIVES for the full set). +_PRIM_NUMERIC = {"decimal", "integer", "positive_integer", "non_negative_integer"} + +# The name-token IR primitives: NCName/NMTOKEN-derived lexical types that are +# narrower than a free string (no whitespace, no empty value, and ID/IDREF +# disallow a leading digit). A neutral schema fact; a validating target +# repairs them, a non-validating one ignores the flag. +_PRIM_NAME_TOKEN = {"id", "idref", "idrefs", "nmtoken", "nmtokens"} + + +def wrap_doc(doc: str | None, width: int) -> list[str]: + """Greedy word-wrap of raw doc text at `width` (the wrapped TEXT width; + templates add their own comment syntax). The break points reproduce the + house comment style: a 3-character prefix plus width 97 is column 100.""" + if not doc: + return [] + words = doc.split() + lines: list[str] = [] + current = "" + for word in words: + if current and len(current) + 1 + len(word) > width: + lines.append(current) + current = word + else: + current = f"{current} {word}" if current else word + if current: + lines.append(current) + return lines + + +# ASCII subsets of the XML name-character classes XSD's \i and \c denote. +# The full classes add non-ASCII ranges whose spelling is engine-specific +# (\x{...} vs \uXXXX); every identifier vocabulary a MusicXML pattern +# describes (SMuFL canonical glyph names) is ASCII, and the strict parse is +# the only consumer, so the approximation can only under-accept. +_XSD_NAME_START = "[:A-Z_a-z]" +_XSD_NAME_CHAR = "[-.0-9:A-Z_a-z]" + + +def _translate_pattern(pattern: str) -> str: + """One XSD pattern, re-spelled in the portable dialect. Constructs with + no portable spelling (class subtraction, \\C/\\I complements, \\p + properties) fail loud: a new schema construct is a decision, not a + silent pass-through.""" + out: list[str] = [] + in_class = False + i, n = 0, len(pattern) + while i < n: + ch = pattern[i] + if ch == "\\": + if i + 1 >= n: + raise ValueError(f"trailing backslash in pattern {pattern!r}") + esc = pattern[i + 1] + if esc in "ci": + if in_class: + raise ValueError( + f"\\{esc} inside a character class has no portable " + f"expansion: {pattern!r}" + ) + out.append(_XSD_NAME_CHAR if esc == "c" else _XSD_NAME_START) + elif esc in "CIpP": + raise ValueError( + f"\\{esc} has no portable spelling: {pattern!r}" + ) + else: + out.append(ch + esc) + i += 2 + continue + if in_class: + if ch == "[": + raise ValueError( + f"character class subtraction is not portable: {pattern!r}" + ) + if ch == "]": + in_class = False + elif ch == "[": + in_class = True + elif ch in "^$": + # XSD has no anchors; ^ and $ are ordinary characters there and + # must be escaped to stay ordinary in the portable form. + out.append("\\") + out.append(ch) + i += 1 + return "".join(out) + + +def prefix_facets(patterns: list[str]) -> tuple[list[str], bool] | None: + """The structured view of a literal-prefixed name-token pattern: for a + single facet of the shape `lit\\c*`, `lit\\c+`, or `(a|b|...)\\c+`, + return (prefix literals, suffix required). None for any other shape. + Neutral pattern analysis: the schema's SMuFL glyph-name vocabularies all + have this form, and a structural target stores only the suffix.""" + if len(patterns) != 1: + return None + m = re.fullmatch( + r"\(?([A-Za-z]+(?:\|[A-Za-z]+)*)\)?\(?\\c([*+])\)?", patterns[0] + ) + if m is None: + return None + return m.group(1).split("|"), m.group(2) == "+" + + +def portable_pattern(patterns: list[str]) -> str | None: + """The type's pattern facets as one anchored portable regex, or None. + + XSD patterns match the whole value (implicit anchoring), so the portable + form is explicitly anchored. Multiple pattern facets on one restriction + step are alternatives (XSD OR semantics); MusicXML never re-restricts an + already-patterned type, so the IR's accumulated list is always a single + step and the facets OR-join.""" + if not patterns: + return None + translated = [f"(?:{_translate_pattern(p)})" for p in patterns] + if len(translated) == 1: + return f"^{translated[0]}$" + return "^(?:" + "|".join(translated) + ")$" + + +def _dep_refs(refs) -> list: + """The unique non-primitive references a plate's emitted code depends on, + sorted by wire name -- the data templates compose include/import lines + from. Primitive refs are excluded by CATEGORY (a primitive's name can + coincide with a type's wire name).""" + unique = {} + for ref in refs: + if ref.category != "primitive": + unique.setdefault(ref.wire, ref) + return [unique[wire] for wire in sorted(unique)] + + +def _number_family(base: str) -> str: + return "decimal" if base == "decimal" else "integer" + + +def _spell(value: float, family: str) -> str: + """A numeric literal valid in every current target language.""" + if family == "integer": + return str(int(value)) + return repr(float(value)) + + +def clamp_steps(base: str, bounds: NumberBounds) -> list[ClampStep]: + """Resolve facets plus primitive-implied bounds into the ordered clamp + rules a wrapper applies after parsing. The tightest lower bound wins (an + exclusive bound at v is tighter than an inclusive one at the same v).""" + family = _number_family(base) + steps: list[ClampStep] = [] + + lows: list[tuple[float, bool]] = [] # (value, exclusive) + if bounds.min_inclusive is not None: + lows.append((float(bounds.min_inclusive), False)) + if bounds.min_exclusive is not None: + lows.append((float(bounds.min_exclusive), True)) + if base in _IMPLIED_MIN: + lows.append((float(_IMPLIED_MIN[base]), False)) + if lows: + value, exclusive = max(lows) + if exclusive: + past = value + (1 if family == "integer" else _EPSILON) + steps.append(ClampStep("<=", _spell(value, family), _spell(past, family))) + else: + bound = _spell(value, family) + steps.append(ClampStep("<", bound, bound)) + + highs: list[tuple[float, bool]] = [] + if bounds.max_inclusive is not None: + highs.append((float(bounds.max_inclusive), False)) + if bounds.max_exclusive is not None: + highs.append((float(bounds.max_exclusive), True)) + if highs: + value, exclusive = min((v, not e) for v, e in highs) + exclusive = not exclusive + if exclusive: + past = value - (1 if family == "integer" else _EPSILON) + steps.append(ClampStep(">=", _spell(value, family), _spell(past, family))) + else: + bound = _spell(value, family) + steps.append(ClampStep(">", bound, bound)) + return steps + + +class PlatesError(Exception): + """One or more projection failures, collected so a run reports every + problem at once rather than the first.""" + + def __init__(self, errors: list[str]): + self.errors = errors + super().__init__("\n".join(errors)) + + +def build_plates(m: ir.Ir, config: Config) -> Plates: + plates = _Builder(m, config).build() + errors = run_checks(plates) + if errors: + raise PlatesError(errors) + return plates + + +class _Builder: + def __init__(self, m: ir.Ir, config: Config): + self.m = m + self.cfg = config + self.resolver = Resolver.from_ir(m) + self.values_by_name: dict[str, ir.ValueType] = {v.name: v for v in m.value_types} + self.complex_by_name = {c.name: c for c in m.complex_types} + self.groups_by_name = {g.name: g for g in m.groups} + self._base_wires = {c.base for c in m.complex_types if c.base} + + naming = config.naming + self.factory = NameFactory( + naming.acronyms if naming.acronyms is not None else DEFAULT_ACRONYMS + ) + # All of this is config data: the generator has no per-language + # defaults (the cardinal rule -- see generator-agnosticism.md). + self.reserved = frozenset(config.reserved.words) + self.invalid_prefix = config.reserved.invalid_prefix + self.type_map = dict(config.types) + self.variant_scope = config.target.variant_scope + + # The grammar-preserving content projection (plan §2.9): normalize + # each content tree once (the discovery and the build must see the + # SAME particle objects), walk it to find every structural group the + # schema actually references and every anonymous choice/sequence + # that needs hoisting into a named shared type. + self.field_nodes: dict[str, list] = {} # owner wire -> field nodes + self.synth_content: dict[str, ir.Particle] = {} # synth wire -> node + self.synth_kind: dict[str, str] = {} # synth wire -> "choice"|"group" + self.synth_by_id: dict[int, str] = {} # id(particle) -> synth wire + # Referenced structural groups, in first-reference order. A group's + # PLATE wire is `{name}-group` / `{name}-choice`: schema group names + # can collide with complex type names (clef, slash, transpose), so + # the suffix keeps the type namespace collision-free by construction + # (and reads as the legacy convention: FullNoteGroup, MusicDataChoice). + self.struct_groups: list[str] = [] + self.group_wire: dict[str, str] = {} # group name -> plate wire + self.group_shape_of: dict[str, str] = {} # plate wire -> "group"|"choice" + self.group_choice_node: dict[str, ir.Choice] = {} # plate wire -> node + self._synth_counts: dict[tuple[str, str], int] = {} + self._discover() + + # Every type's Name and final identifier, computed up front so any + # reference can be resolved to its target spelling in one lookup. + # Structural groups and synthesized content types share the type + # namespace (and therefore the collision gate). + self.type_names: dict[str, Name] = {} + self.type_idents: dict[str, str] = {} + all_type_wires = ( + list(self.values_by_name) + + list(self.complex_by_name) + + [self.group_wire[g] for g in self.struct_groups] + + list(self.synth_content) + ) + for type_wire in all_type_wires: + name = self._type_name(type_wire) + self.type_names[type_wire] = name + self.type_idents[type_wire] = self._sanitize( + config.target.symbol_prefix + name.cased[naming.type_convention] + ) + + # ----- content discovery (plan §2.9) -------------------------------------- # + # + # One deterministic preorder walk over every composite's normalized + # content, recursing through structural group references (each group + # walked once, at first reference). Anonymous choices get the wire name + # `{owner}-choice` (`-2`, `-3`, ... on repeats within one owner); + # anonymous sequences get `{owner}-group`. Nested hoists are owned by + # the hoisted type, so the names compose (`note-choice-group`). All of + # these wires are addressable by [rename.type]. + + def _discover(self) -> None: + for ct in self.m.complex_types: + if ct.content is not None: + self._walk_owner(ct.name, ct.content) + + def _walk_owner(self, owner: str, content: ir.Particle) -> None: + nodes = self.resolver.field_nodes(content) + self.field_nodes[owner] = nodes + for node in nodes: + self._visit(owner, node) + + def _visit(self, owner: str, node: ir.Particle) -> None: + if isinstance(node, ir.Element): + return + if isinstance(node, ir.GroupRef): + self._see_group(node.name) + return + if isinstance(node, ir.Choice): + wire = self._hoist(owner, "choice", node) + self._visit_choice_items(wire, node) + return + # An optional/repeated anonymous Sequence: hoist as a group whose + # content is the exactly-once body. + body = ir.Sequence(node.items, 1, 1) + wire = self._hoist(owner, "group", body) + self.synth_by_id[id(node)] = wire + self._walk_owner(wire, body) + + def _visit_choice_items(self, choice_wire: str, choice: ir.Choice) -> None: + for alt in choice.items: + if isinstance(alt, ir.Element): + continue + if isinstance(alt, ir.GroupRef): + self._see_group(alt.name) + elif isinstance(alt, ir.Choice): + wire = self._hoist(choice_wire, "choice", alt) + self.synth_by_id[id(alt)] = wire + self._visit_choice_items(wire, alt) + else: # Sequence alternative + body = ir.Sequence(alt.items, 1, 1) + wire = self._hoist(choice_wire, "group", body) + self.synth_by_id[id(alt)] = wire + self._walk_owner(wire, body) + + def _hoist(self, owner: str, kind: str, content: ir.Particle) -> str: + n = self._synth_counts.get((owner, kind), 0) + 1 + self._synth_counts[(owner, kind)] = n + wire = f"{owner}-{kind}" if n == 1 else f"{owner}-{kind}-{n}" + self.synth_content[wire] = content + self.synth_kind[wire] = kind + self.synth_by_id[id(content)] = wire + return wire + + def _see_group(self, name: str) -> None: + if name in self.group_wire: + return + group = self.groups_by_name.get(name) + if group is None: + return + shape, node = self.resolver.group_shape(name) + wire = f"{name}-{shape}" + self.group_wire[name] = wire + self.group_shape_of[wire] = shape + self.struct_groups.append(name) + if shape == "choice": + # The group IS the choice (reference sites absorb the choice's + # occurrence); its alternatives may still need hoisting. + self.group_choice_node[wire] = node + self._visit_choice_items(wire, node) + else: + self._walk_owner(wire, group.content) + + # ----- entry ------------------------------------------------------------ # + + def build(self) -> Plates: + errors = self._validate_config_against_ir() + if errors: + raise PlatesError(errors) + + version = re.search(r"musicxml-(\d+\.\d+)", self.m.source) + plates = Plates( + source=self.m.source, + schema_version=version.group(1) if version else "", + target=self._target_info(), + value_types=[self._value_plate(v) for v in self.m.value_types], + complex_types=[self._complex_plate(c) for c in self.m.complex_types], + groups=self._group_plates(), + roots=[self._plate_ref(ir.Ref(r.type, "complex")) for r in self.m.roots], + ) + return plates + + def _target_info(self) -> TargetInfo: + t, n = self.cfg.target, self.cfg.naming + return TargetInfo( + symbol_prefix=t.symbol_prefix, + type_convention=n.type_convention, + field_convention=n.field_convention, + variant_convention=n.variant_convention, + inheritance=t.inheritance, + variant_scope=self.variant_scope, + doc_wrap=self.cfg.docs.wrap, + reserved=sorted(self.reserved), + reserved_members=sorted(self.cfg.reserved.members), + reserved_type_suffixes=sorted(self.cfg.reserved.type_suffixes), + vars=dict(self.cfg.vars), + ) + + # ----- names and references ---------------------------------------------- # + + def _sanitize(self, raw: str) -> str: + return sanitize_identifier(raw, self.reserved, self.invalid_prefix) + + def _type_name(self, wire: str) -> Name: + entry = self.cfg.renames.types.get(wire) + return self.factory.make( + wire, + fundamental=entry.fundamental if entry else None, + overrides=entry.cased if entry else None, + ) + + def _element_name(self, wire: str, pluralize: bool) -> Name: + entry = self.cfg.renames.elements.get(wire) + return self.factory.make( + wire, + fundamental=entry.fundamental if entry else None, + overrides=entry.cased if entry else None, + pluralize=pluralize, + ) + + def _attribute_name(self, owner: str, wire: str) -> Name: + # A scoped key (this attribute on this owner) wins over a global one. + entry = self.cfg.renames.scoped_attributes.get((owner, wire)) + if entry is None: + entry = self.cfg.renames.attributes.get(wire) + return self.factory.make( + wire, + fundamental=entry.fundamental if entry else None, + overrides=entry.cased if entry else None, + ) + + def _variant(self, scope_wire: str, value_wire: str) -> Variant: + """Project one enum value (or union literal). The final constant + identifier follows the target's variant scope: `bare` sanitizes the + variant casing alone; `composed` joins the owning type's casing (and + symbol prefix) in the variant convention's join style, because the + constant will live in a flat namespace.""" + entry = self.cfg.renames.enum_values.get((scope_wire, value_wire)) + name = self.factory.make( + value_wire, + fundamental=entry.fundamental if entry else None, + overrides=entry.cased if entry else None, + ) + conv = self.cfg.naming.variant_convention + if self.variant_scope == "composed": + joiner = JOINERS.get(conv, "_") + if joiner: + parts = [] + if self.cfg.target.symbol_prefix: + prefix_name = self.factory.make(self.cfg.target.symbol_prefix) + parts.append(prefix_name.cased[conv]) + parts.append(self.type_names[scope_wire].cased[conv]) + parts.append(name.cased[conv]) + raw = joiner.join(parts) + else: + # Concatenating conventions: the type identifier (which + # already carries the prefix) plus the variant casing. + raw = self.type_idents[scope_wire] + name.cased[conv] + else: + raw = name.cased[conv] + return Variant(wire=value_wire, name=name, ident=self._sanitize(raw)) + + def _field_ident(self, name: Name) -> str: + raw = self.cfg.naming.field_prefix + name.cased[self.cfg.naming.field_convention] + return self._sanitize(raw) + + def _plate_ref(self, ref: ir.Ref) -> PlateRef: + """Resolve a reference with the referenced type's name bundle and kind + denormalized onto it, so templates never perform lookups.""" + if ref.category == "primitive": + return PlateRef( + wire=ref.name, + category="primitive", + ident=self.type_map.get(ref.name, ref.name), + name=self.factory.make(ref.name), + kind="primitive-" + _number_family(ref.name) + if ref.name in _PRIM_NUMERIC + else "primitive-string", + name_token=ref.name in _PRIM_NAME_TOKEN, + ) + if ref.category == "value": + kind = self.values_by_name[ref.name].kind + else: + kind = "complex" + return PlateRef( + wire=ref.name, + category=ref.category, + ident=self.type_idents[ref.name], + name=self.type_names[ref.name], + kind=kind, + ) + + # ----- value plates -------------------------------------------------------- # + + def _doc_lines(self, doc: str | None) -> list[str]: + return wrap_doc(doc, self.cfg.docs.wrap) + + def _value_plate(self, v: ir.ValueType): + name = self.type_names[v.name] + ident = self.type_idents[v.name] + if isinstance(v, ir.EnumType): + return EnumPlate( + name=name, + ident=ident, + base=v.base, + variants=[self._variant(v.name, value) for value in v.values], + doc=v.doc, + doc_lines=self._doc_lines(v.doc), + ) + if isinstance(v, ir.NumberType): + bounds = NumberBounds( + v.min_inclusive, v.max_inclusive, v.min_exclusive, v.max_exclusive + ) + return NumberPlate( + name=name, + ident=ident, + base=v.base, + bounds=bounds, + family=_number_family(v.base), + clamp=clamp_steps(v.base, bounds), + target_type=self.type_map.get(v.base, v.base), + doc=v.doc, + doc_lines=self._doc_lines(v.doc), + ) + if isinstance(v, ir.StringType): + facets = prefix_facets(list(v.patterns)) + prefixes, suffix_required = facets if facets else ([], False) + return StringPlate( + name=name, + ident=ident, + base=v.base, + patterns=list(v.patterns), + pattern=portable_pattern(list(v.patterns)), + prefixes=[self._variant(v.name, p) for p in prefixes], + multi_prefix=len(prefixes) > 1, + suffix_required=suffix_required, + min_length=v.min_length, + max_length=v.max_length, + length=v.length, + target_type=self.type_map.get(v.base, v.base), + doc=v.doc, + doc_lines=self._doc_lines(v.doc), + ) + members = [] + for m in v.members: + if m.ref is not None: + member_name = self.type_names.get(m.ref.name) or self.factory.make(m.ref.name) + clamp = [] + if m.ref.category == "primitive" and m.ref.name in _IMPLIED_MIN: + # The primitive's implied bounds apply inside a union just + # as they would on a named number type. + clamp = clamp_steps(m.ref.name, NumberBounds()) + members.append( + UnionPlateMember( + ref=self._plate_ref(m.ref), + name=member_name, + # The member's discriminator constant: scoped, renamed, + # and collision-gated exactly like an enum variant. + tag=self._variant(v.name, m.ref.name), + clamp=clamp, + ) + ) + else: + # An inline literal set projects like a tiny anonymous enum; + # its variants are addressable for renames under the union's + # own type name and double as the discriminator constants. + members.append( + UnionPlateMember( + literals=[self._variant(v.name, lit) for lit in m.literals or []] + ) + ) + plate = UnionPlate( + name=name, + ident=ident, + members=members, + open_ended=any( + m.ref is not None and m.ref.kind in ("primitive-string", "string") + for m in members + ), + doc=v.doc, + doc_lines=self._doc_lines(v.doc), + ) + plate.deps = _dep_refs( + m.ref for m in plate.members if m.ref is not None + ) + return plate + + # ----- complex plates ------------------------------------------------------ # + + def _complex_plate(self, ct: ir.ComplexType) -> ComplexPlate: + strategy = { + "value": "value-class", + "composite": "composite-class", + "empty": "flag" if ct.presence_only else "attrs-class", + "derived": "inherit" if self.cfg.target.inheritance else "flatten", + }[ct.kind] + + members = self._members(ct, flatten=False) + all_members = None + if ct.kind == "derived": + # Built under either strategy, so the collision gate covers the + # merged chain even for inheriting targets. + all_members = self._members(ct, flatten=True) + + fields: list[ContentField] = [] + element_names: list[str] = [] + if ct.content is not None: + fields = [self._field(ct.name, n) for n in self.field_nodes[ct.name]] + element_names = self.resolver.particle_element_names(ct.content) + + plate = ComplexPlate( + name=self.type_names[ct.name], + ident=self.type_idents[ct.name], + shape=ct.kind, + strategy=strategy, + members=members, + content=self.resolver.content(ct), + fields=fields, + element_names=element_names, + base=self._plate_ref(ir.Ref(ct.base, "complex")) if ct.base else None, + all_members=all_members, + is_base=ct.name in self._base_wires, + presence_only=ct.presence_only, + doc=ct.doc, + doc_lines=self._doc_lines(ct.doc), + ) + refs = [m.type_ref for m in plate.members] + refs += [m.type_ref for m in (plate.all_members or [])] + if plate.base is not None: + refs.append(plate.base) + plate.deps = _dep_refs(refs) + # What a grammar-preserving target depends on instead: attribute + # (and value-body) members plus the content fields, not the flat + # element view. + srefs = [m.type_ref for m in plate.members if m.kind != "element"] + srefs += [f.type_ref for f in plate.fields] + if plate.base is not None: + srefs.append(plate.base) + plate.structural_deps = _dep_refs(srefs) + return plate + + # ----- group, choice, and synthesized content plates (plan §2.9) --------- # + + def _group_plates(self) -> list: + out: list = [] + for name in self.struct_groups: + wire = self.group_wire[name] + g = self.groups_by_name[name] + if self.group_shape_of[wire] == "choice": + out.append( + self._choice_plate( + wire, self.group_choice_node[wire], synthesized=False, doc=g.doc + ) + ) + else: + out.append( + GroupPlate( + name=self.type_names[wire], + ident=self.type_idents[wire], + fields=[self._field(wire, n) for n in self.field_nodes[wire]], + element_names=self.resolver.particle_element_names(g.content), + synthesized=False, + doc=g.doc, + doc_lines=self._doc_lines(g.doc), + ) + ) + for wire, content in self.synth_content.items(): + if self.synth_kind[wire] == "choice": + out.append(self._choice_plate(wire, content, synthesized=True, doc=None)) + else: + out.append( + GroupPlate( + name=self.type_names[wire], + ident=self.type_idents[wire], + fields=[self._field(wire, n) for n in self.field_nodes[wire]], + element_names=self.resolver.particle_element_names(content), + synthesized=True, + ) + ) + for plate in out: + refs = [f.type_ref for f in getattr(plate, "fields", [])] + refs += [a.type_ref for a in getattr(plate, "alternatives", [])] + plate.deps = _dep_refs(refs) + return out + + def _choice_plate(self, wire, choice, synthesized: bool, doc) -> ChoicePlate: + alternatives = [self._alternative(wire, alt) for alt in choice.items] + # The natural zero of a nullable choice is its first nullable + # alternative (empty); otherwise the first alternative. + nullable_alts = [ + i + for i, alt in enumerate(choice.items) + if self.resolver.nullable(alt) + ] + alternatives[nullable_alts[0] if nullable_alts else 0].default_alt = True + return ChoicePlate( + name=self.type_names[wire], + ident=self.type_idents[wire], + alternatives=alternatives, + element_names=self.resolver.particle_element_names(choice), + nullable=bool(nullable_alts), + synthesized=synthesized, + doc=doc, + doc_lines=self._doc_lines(doc), + ) + + def _group_like_ref(self, plate_wire: str, kind: str) -> PlateRef: + return PlateRef( + wire=plate_wire, + category=kind, + ident=self.type_idents[plate_wire], + name=self.type_names[plate_wire], + kind=kind, + ) + + def _local_name(self, type_wire: str, owner_wire: str) -> Name: + """The field/alternative name for a hoisted type: the type's words + with the owning type's leading words stripped (a synthesized + `note-choice` is field `choice` on `note`); the full name when the + type's (possibly renamed) name is not prefixed by the owner's.""" + t = self.type_names[type_wire] + o = self.type_names.get(owner_wire) or self.factory.make(owner_wire) + tw, ow = tuple(t.words), tuple(o.words) + if len(tw) > len(ow) and tw[: len(ow)] == ow: + return self.factory.make("-".join(tw[len(ow):])) + return self.factory.make("-".join(tw)) + + def _occurrence(self, node: ir.Particle) -> tuple[int, int | str]: + """A field node's effective occurrence. A reference to a + choice-shaped group absorbs the choice's own occurrence (the type is + ONE alternative instance; the repetition moves to the field).""" + if isinstance(node, ir.GroupRef): + wire = self.group_wire.get(node.name) + if wire is not None and self.group_shape_of[wire] == "choice": + inner = self.group_choice_node[wire] + return self.resolver.occurs_product( + node.min, node.max, inner.min, inner.max + ) + return node.min, node.max + + def _presence_only_ref(self, ref: PlateRef) -> bool: + if ref.category != "complex": + return False + ct = self.complex_by_name.get(ref.wire) + return ct is not None and ct.presence_only + + def _field_facts(self, owner_wire: str, node: ir.Particle): + """The shared element/group/choice classification behind fields and + alternatives: (name, kind, type_ref, tag, occurrence, first, doc).""" + if isinstance(node, ir.Element): + mn, mx = node.min, node.max + pluralize = ( + self.cfg.naming.pluralize_vectors + and (mx == ir.UNBOUNDED or mx > 1) + ) + name = self._element_name(node.name, pluralize) + return ( + name, "element", self._plate_ref(node.type), node.name, + (mn, mx), [node.name], node.doc, + ) + if isinstance(node, ir.GroupRef): + plate_wire = self.group_wire[node.name] + kind = self.group_shape_of[plate_wire] + return ( + self.factory.make(node.name), kind, + self._group_like_ref(plate_wire, kind), None, + self._occurrence(node), self.resolver.first_names(node), None, + ) + # A hoisted anonymous choice or sequence. + wire = self.synth_by_id[id(node)] + kind = self.synth_kind[wire] + return ( + self._local_name(wire, owner_wire), kind, + self._group_like_ref(wire, kind), None, + (node.min, node.max), self.resolver.first_names(node), None, + ) + + def _field(self, owner_wire: str, node: ir.Particle) -> ContentField: + name, kind, ref, tag, (mn, mx), first, doc = self._field_facts(owner_wire, node) + if mx == ir.UNBOUNDED or mx > 1: + cardinality = "vector" + elif mn == 0: + cardinality = "optional" + else: + cardinality = "required" + return ContentField( + name=name, + ident=self._field_ident(name), + kind=kind, + type_ref=ref, + cardinality=cardinality, + tag=tag, + presence=self._presence_only_ref(ref), + min1=cardinality == "vector" and mn >= 1, + max=str(mx) if isinstance(mx, int) and mx > 1 else None, + nullable=self._instance_nullable(node), + first=first, + doc=doc, + ) + + def _instance_nullable(self, node: ir.Particle) -> bool: + """Whether ONE instance of the field's type can match the empty + element sequence -- about the content, not the field's occurrence + bounds (those are the cardinality).""" + if isinstance(node, ir.Element): + return False + if isinstance(node, ir.GroupRef): + wire = self.group_wire.get(node.name) + if wire is not None and self.group_shape_of[wire] == "choice": + inner = self.group_choice_node[wire] + return any(self.resolver.nullable(a) for a in inner.items) + group = self.groups_by_name.get(node.name) + return group is not None and self.resolver.nullable(group.content) + if isinstance(node, ir.Choice): + return any(self.resolver.nullable(a) for a in node.items) + return all(self.resolver.nullable(i) for i in node.items) # Sequence + + def _alternative(self, choice_wire: str, node: ir.Particle) -> Alternative: + name, kind, ref, tag, (mn, mx), first, doc = self._field_facts(choice_wire, node) + if mx == ir.UNBOUNDED or (isinstance(mx, int) and mx > 1): + cardinality = "vector" + else: + if mn == 0: + # A single optional alternative would make the choice itself + # optional in a way no current schema uses; fail loud so a + # future schema construct is a decision, not a silent guess. + raise PlatesError( + [f"{choice_wire}: choice alternative with occurrence (0, 1) " + f"has no projection"] + ) + cardinality = "required" + return Alternative( + name=name, + ident=self._field_ident(name), + kind=kind, + type_ref=ref, + cardinality=cardinality, + tag=tag, + presence=self._presence_only_ref(ref), + min1=cardinality == "vector" and mn >= 1, + max=str(mx) if isinstance(mx, int) and mx > 1 else None, + first=first, + doc=doc, + ) + + def _members(self, ct: ir.ComplexType, flatten: bool) -> list[Member]: + """The flat field list: attributes first, then the text value body, + then child elements in document order. The flattened variant merges + the base chain (base-most first) via the Resolver's chain views.""" + if flatten: + attrs = self.resolver.all_attributes(ct) + elements = self.resolver.all_flat_elements(ct) + chain = self.resolver.base_chain(ct) + else: + attrs = self.resolver.attributes(ct) + elements = self.resolver.flat_elements(ct) + chain = [ct] + + members = [self._attr_member(ct.name, a) for a in attrs] + for c in chain: + if c.value_type is not None: + members.append(self._value_member(c.value_type)) + members += [self._element_member(e, card) for e, card in elements] + return members + + def _attr_member(self, owner_wire: str, a: ir.Attr) -> Member: + name = self._attribute_name(owner_wire, a.name) + literal = a.fixed if a.fixed is not None else a.default + return Member( + name=name, + ident=self._field_ident(name), + kind="attribute", + type_ref=self._plate_ref(a.type), + cardinality="required" if a.required else "optional", + default=a.default, + fixed=a.fixed, + default_variant=self._default_variant(a.type, literal), + import_default=self.cfg.import_attribute_defaults.get( + (owner_wire, a.name) + ), + doc=a.doc, + ) + + def _value_member(self, value_type: ir.Ref) -> Member: + # The text body of a value-shaped type has no wire name of its own; + # it is projected under the fixed root "value". + name = self.factory.make("", fundamental="value") + return Member( + name=name, + ident=self._field_ident(name), + kind="value", + type_ref=self._plate_ref(value_type), + cardinality="required", + ) + + def _element_member(self, element: ir.Element, cardinality: str) -> Member: + pluralize = self.cfg.naming.pluralize_vectors and cardinality == "vector" + name = self._element_name(element.name, pluralize) + return Member( + name=name, + ident=self._field_ident(name), + kind="element", + type_ref=self._plate_ref(element.type), + cardinality=cardinality, + doc=element.doc, + ) + + def _default_variant(self, type_ref: ir.Ref, literal: str | None) -> str | None: + """When a default/fixed literal names a variant of the member's enum + type, resolve it to the variant's target identifier (the wire literal + stays in `default`/`fixed` for the serializer).""" + if literal is None or type_ref.category != "value": + return None + vt = self.values_by_name.get(type_ref.name) + if isinstance(vt, ir.EnumType) and literal in vt.values: + return self._variant(vt.name, literal).ident + return None + + # ----- config-against-IR validation ----------------------------------------- # + + def _validate_config_against_ir(self) -> list[str]: + """Every rename key must address something in the IR, and every + [types] key a real primitive (design 6.5): a typo or a key left stale + after a schema bump is a build error, not a silently ignored line.""" + r = self.cfg.renames + errors: list[str] = [] + + for primitive in self.cfg.types: + if primitive not in PRIMITIVES: + errors.append( + f"[types] {primitive}: not an IR primitive " + f"({', '.join(sorted(PRIMITIVES))})" + ) + + # Structural-group and synthesized content-type wires are addressable + # for renames too (plan §2.9). + type_wires = set(self.values_by_name) | set(self.complex_by_name) + type_wires |= {self.group_wire[g] for g in self.struct_groups} + type_wires |= set(self.synth_content) + for wire in r.types: + if wire not in type_wires: + errors.append(f"rename.type.{wire}: no such type in the IR") + + element_wires: set[str] = set() + for ct in self.m.complex_types: + for e in self.resolver.elements(ct): + element_wires.add(e.name) + element_wires.update(root.element for root in self.m.roots) + for wire in r.elements: + if wire not in element_wires: + errors.append(f"rename.element.{wire}: no element by that name occurs") + + attribute_wires = { + a.name for ct in self.m.complex_types for a in self.resolver.attributes(ct) + } + for wire in r.attributes: + if wire not in attribute_wires: + errors.append(f"rename.attribute.{wire}: no attribute by that name occurs") + + for owner, attr in r.scoped_attributes: + ct = self.complex_by_name.get(owner) + if ct is None: + errors.append(f"rename.attribute.{owner}.{attr}: no such complex type") + elif attr not in {a.name for a in self.resolver.all_attributes(ct)}: + errors.append( + f"rename.attribute.{owner}.{attr}: type '{owner}' has no such attribute" + ) + + for (owner, attr), _literal in self.cfg.import_attribute_defaults.items(): + ct = self.complex_by_name.get(owner) + if ct is None: + errors.append( + f"import.attribute-defaults.{owner}: no such complex type" + ) + continue + match = next( + (a for a in self.resolver.all_attributes(ct) if a.name == attr), None + ) + if match is None: + errors.append( + f"import.attribute-defaults.{owner}.{attr}: type has no " + f"such attribute" + ) + elif not match.required: + errors.append( + f"import.attribute-defaults.{owner}.{attr}: attribute is " + f"optional; a default would change present-vs-absent " + f"round-trip fidelity" + ) + + for enum, value in r.enum_values: + vt = self.values_by_name.get(enum) + if isinstance(vt, ir.EnumType): + if value not in vt.values: + errors.append( + f"rename.enum-value.{enum}.{value!r}: enum has no such value" + ) + elif isinstance(vt, ir.UnionType): + addressable = {lit for m in vt.members for lit in (m.literals or [])} + addressable |= {m.ref.name for m in vt.members if m.ref is not None} + if value not in addressable: + errors.append( + f"rename.enum-value.{enum}.{value!r}: union has no such " + f"literal or member" + ) + else: + errors.append(f"rename.enum-value.{enum}: no such enum type") + + return errors diff --git a/gen/plates/check.py b/gen/plates/check.py new file mode 100644 index 000000000..9c1304a6e --- /dev/null +++ b/gen/plates/check.py @@ -0,0 +1,216 @@ +"""Post-projection collision detection (design section 7). + +After tokenizing, recasing, renames, and reserved-word/validity mangling, two +distinct wire names can collapse to one identifier. The IR's "no element-name +collisions" invariant guarantees nothing here, because these collisions are +induced by the projection. Each scope is checked in the convention the target +actually uses (the identifiers were already produced in it); every report +names the scope, the colliding wire names, and the shared identifier -- +enough to write a targeted rename to resolve it. +""" + +from __future__ import annotations + +from gen.plates.model import ComplexPlate, EnumPlate, Plates, UnionPlate + + +def run_checks(plates: Plates) -> list[str]: + errors: list[str] = [] + if plates.target.variant_scope == "composed": + _check_flat_namespace(plates, errors) + else: + _check_type_idents(plates, errors) + _check_variants_per_type(plates, errors) + _check_members(plates, errors) + _check_template_reserved(plates, errors) + _check_union_member_order(plates, errors) + return errors + + +def _check_union_member_order(plates: Plates, errors: list[str]) -> None: + """An open string member matches ANY input, so every union parser that + tries members in schema order can never reach the members after it. A + fact about union semantics, not about any language, so it gates here + rather than in each target's templates.""" + for p in plates.value_types: + if not isinstance(p, UnionPlate): + continue + for i, m in enumerate(p.members): + open_member = m.ref is not None and m.ref.kind in ( + "primitive-string", "string" + ) + if open_member and i != len(p.members) - 1: + errors.append( + f"union '{p.name.wire}': member '{m.ref.wire}' matches any " + f"string, so the members after it are unreachable; it must " + f"be last" + ) + + +def _check_template_reserved(plates: Plates, errors: list[str]) -> None: + """Names the target's TEMPLATES synthesize cannot be gated structurally, + so the target declares them: [reserved] members (member identifiers its + templates claim on every struct) and [reserved] type-suffixes + (compositions appended to type identifiers, like a Child struct). A + schema name landing on either must fail here, not as a confusing compile + error in committed output.""" + reserved_members = set(plates.target.reserved_members) + if reserved_members: + for p in plates.complex_types: + field_idents = [f.ident for f in p.fields] + for member_list in ( + [m.ident for m in p.members], + [m.ident for m in (p.all_members or [])], + field_idents, + ): + for ident in member_list: + if ident in reserved_members: + errors.append( + f"member identifier '{ident}' in '{p.name.wire}' is " + f"reserved by the target's templates ([reserved] members); " + f"rename it" + ) + suffixes = plates.target.reserved_type_suffixes + if suffixes: + idents = { + p.ident: p.name.wire + for p in list(plates.value_types) + + list(plates.complex_types) + + list(plates.groups) + } + for ident, wire in idents.items(): + for suffix in suffixes: + composed = ident + suffix + if composed in idents: + errors.append( + f"type identifier collision: '{idents[composed]}' is named " + f"'{composed}', which the target's templates compose from " + f"'{wire}' + reserved suffix '{suffix}'" + ) + + +def _variant_pairs(plate) -> list[tuple[str, str]]: + """(ident, claimant description) for every constant a value plate emits: + enum variants, union literal variants, and union member tags (the + discriminator constants) alike.""" + if isinstance(plate, EnumPlate): + return [(v.ident, f"{plate.name.wire}.{v.wire!r}") for v in plate.variants] + if isinstance(plate, UnionPlate): + pairs = [ + (v.ident, f"{plate.name.wire}.{v.wire!r}") + for m in plate.members + if m.literals + for v in m.literals + ] + pairs += [ + (m.tag.ident, f"{plate.name.wire} member {m.ref.wire!r}") + for m in plate.members + if m.tag is not None + ] + return pairs + return [] + + +def _check_flat_namespace(plates: Plates, errors: list[str]) -> None: + """For a composed variant scope, the target has one identifier namespace: + type identifiers and every (already composed) enum/literal constant must + be mutually unique -- this is the namespace the compiler actually sees.""" + pairs = [ + (p.ident, f"type {p.name.wire!r}") + for p in list(plates.value_types) + + list(plates.complex_types) + + list(plates.groups) + ] + for p in plates.value_types: + pairs.extend(_variant_pairs(p)) + for ident, claimants in _collisions(pairs): + errors.append( + f"identifier collision: {sorted(set(claimants))} all project to '{ident}'" + ) + + +def _collisions(pairs: list[tuple[str, str]]) -> list[tuple[str, list[str]]]: + """Group (identifier, wire) pairs; return identifiers claimed by more + than one distinct wire name, with their claimants.""" + by_ident: dict[str, list[str]] = {} + for ident, wire in pairs: + by_ident.setdefault(ident, []).append(wire) + return [ + (ident, wires) + for ident, wires in by_ident.items() + if len(set(wires)) > 1 + ] + + +def _check_type_idents(plates: Plates, errors: list[str]) -> None: + pairs = [ + (p.ident, p.name.wire) + for p in list(plates.value_types) + + list(plates.complex_types) + + list(plates.groups) + ] + for ident, wires in _collisions(pairs): + errors.append( + f"type identifier collision: {sorted(set(wires))} all project to '{ident}'" + ) + + +def _check_variants_per_type(plates: Plates, errors: list[str]) -> None: + """For a bare variant scope, constants live inside their type: uniqueness + is per enum (or per union's literal set).""" + for p in plates.value_types: + pairs = _variant_pairs(p) + for ident, wires in _collisions(pairs): + errors.append( + f"variant identifier collision in '{p.name.wire}': " + f"{sorted(set(wires))} all project to '{ident}'" + ) + + +def _check_members(plates: Plates, errors: list[str]) -> None: + for p in plates.complex_types: + for label, members in (("members", p.members), ("all_members", p.all_members)): + if not members: + continue + pairs = [(m.ident, f"{m.kind} {m.name.wire!r}") for m in members] + for ident, wires in _collisions(pairs): + errors.append( + f"member identifier collision in '{p.name.wire}' ({label}): " + f"{sorted(set(wires))} all project to '{ident}'" + ) + _check_fields(plates, errors) + + +def _check_fields(plates: Plates, errors: list[str]) -> None: + """The grammar-preserving projection's identifier scopes (plan §2.9): a + structural target's class holds the attribute members AND the content + fields, so the two lists must be mutually unique; a choice type's + alternative accessors share one scope likewise.""" + for p in plates.complex_types: + if not p.fields: + continue + pairs = [ + (m.ident, f"attribute {m.name.wire!r}") + for m in p.members + if m.kind == "attribute" + ] + pairs += [(f.ident, f"{f.kind} field {f.name.wire!r}") for f in p.fields] + for ident, wires in _collisions(pairs): + errors.append( + f"field identifier collision in '{p.name.wire}': " + f"{sorted(set(wires))} all project to '{ident}'" + ) + for g in plates.groups: + if g.kind == "group": + pairs = [(f.ident, f"{f.kind} field {f.name.wire!r}") for f in g.fields] + else: + pairs = [ + (a.ident, f"{a.kind} alternative {a.name.wire!r}") + for a in g.alternatives + ] + for ident, wires in _collisions(pairs): + errors.append( + f"field identifier collision in '{g.name.wire}': " + f"{sorted(set(wires))} all project to '{ident}'" + ) + diff --git a/gen/plates/model.py b/gen/plates/model.py new file mode 100644 index 000000000..9dfc43134 --- /dev/null +++ b/gen/plates/model.py @@ -0,0 +1,498 @@ +"""The Plates: the template-facing, per-target projection of the IR. + +The IR (gen.ir) is a pure, language-agnostic function of the schema inputs. +The Plates are its opposite number: one plate per emitted type, carrying +everything a template needs to print code without thinking -- identifier +casings, resolved target types, emit strategy tags, file assignment. This is +where config.toml meets the IR; templates stay dumb renderers. + +Each plate is internally partitioned into two field groups: + + - a neutral core: wire-faithful, target-independent facts (wire name, shape, + resolved structure, value lists, facets, docs), mirrored from the IR and + its Resolver; and + - a target binding: the per-target overlay (casings, sanitized identifiers, + resolved target types, strategy tags, file assignment). + +A code target reads both groups. A neutral target (e.g. a JSON Schema +emitter) reads only the neutral core and renders once-per-target templates, +paying nothing for the binding it ignores. + +The Plates are materialized (built once per target, dumpable via +gen.ir.dump.to_jsonable) rather than computed on demand: collision detection +and rename validation are global build-then-check passes, and templates want +random access to fully resolved plates. Design: docs/ai/design/plates.md. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from gen.ir import model as ir +from gen.names import Name + +__all__ = ["Name"] # re-exported: templates reach all plate vocabulary here + + +@dataclass +class PlateRef: + """A reference to another type, resolved for the target: `wire` and + `category` mirror the IR Ref; `ident` is the spelling a template prints -- + the referenced plate's type identifier, or the mapped target type when the + category is `primitive`. For primitives, `wire` carries the IR's canonical + primitive name (e.g. `non_negative_integer`), not an XSD spelling: builtins + never appear on the wire themselves. + + `name` and `kind` are denormalized from the referenced plate so templates + never perform lookups: `name` is the referenced type's name bundle (for a + primitive, its tokenized canonical name), and `kind` is the referenced + plate's kind (enum/number/string/union/complex) or, for primitives, the + family-qualified `primitive-decimal` / `primitive-integer` / + `primitive-string`.""" + + wire: str + category: str # "complex" | "value" | "primitive" + ident: str + name: Name | None = None + kind: str = "" + # True when the referent is a name-token primitive (id/idref/nmtoken/...): + # 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 + + +@dataclass +class TargetInfo: + """The per-target facts that are global to the projection, not per-type. + Every field here is part of the projection contract: definable without + reference to any language. Anything language-flavored belongs in `vars`, + which passes through to templates verbatim and is never interpreted.""" + + symbol_prefix: str # prepended to type idents and composed constants + type_convention: str + field_convention: str + variant_convention: str + inheritance: bool # derived strategy: True -> inherit, False -> flatten + variant_scope: str # "bare" | "composed" (see Variant) + doc_wrap: int # width doc text is wrapped to (doc_lines), excluding comment syntax + reserved: list[str] = field(default_factory=list) # the target's reserved words, sorted + reserved_members: list[str] = field(default_factory=list) # template-reserved member idents + reserved_type_suffixes: list[str] = field(default_factory=list) # template compositions + vars: dict[str, str] = field(default_factory=dict) # freeform, for templates + + +# --------------------------------------------------------------------------- # +# Value plates (mirror the IR's 4 value shapes) +# --------------------------------------------------------------------------- # + + +@dataclass +class Variant: + """One enum value. `wire` is retained for serialization; `ident` is the + FINAL emitted constant identifier -- templates print it verbatim, and the + collision gate certifies it. Its shape follows the target's variant scope + ([target] variant-scope): `bare` when the target's constants live inside + the type (`_1024th`), `composed` when they share one flat namespace + (`NoteTypeValue1024th`, `MX_NOTE_TYPE_VALUE_1024TH`).""" + + wire: str + name: Name + ident: str + + +@dataclass +class NumberBounds: + """Numeric facets, verbatim from the schema (strings, not parsed).""" + + min_inclusive: str | None = None + max_inclusive: str | None = None + min_exclusive: str | None = None + max_exclusive: str | None = None + + +@dataclass +class ClampStep: + """One resolved clamping rule: `if v then v = `. + This is the corpus leniency POLICY (the thing the .fixup.xml sidecars + encode), decided once in the projection: facet bounds and the + primitive-implied lower bounds are merged, the tightest wins, and an + exclusive bound's replacement is the nearest representable in-range value + (next integer, or bound +/- 1e-6 for decimals). The literals are spelled + neutrally (valid in every current target language); templates print them + verbatim.""" + + op: str # "<" | "<=" | ">" | ">=" + bound: str + replacement: str + + +@dataclass +class EnumPlate: + name: Name + ident: str + base: str # IR primitive the tokens are drawn from + variants: list[Variant] + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) # doc wrapped at doc_wrap + deps: list[PlateRef] = field(default_factory=list) # non-primitive types referenced + kind: str = "enum" + strategy: str = "enum-class" + + +@dataclass +class NumberPlate: + name: Name + ident: str + base: str # IR primitive: decimal/integer/positive_integer/non_negative_integer + bounds: NumberBounds = field(default_factory=NumberBounds) # neutral core: raw facets + family: str = "" # "decimal" | "integer": which parse/format family applies + clamp: list[ClampStep] = field(default_factory=list) # resolved policy (see ClampStep) + target_type: str = "" # type_map[base]: what the wrapper wraps + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + kind: str = "number" + strategy: str = "numeric-wrapper" + + +@dataclass +class StringPlate: + name: Name + ident: str + base: str # IR primitive: string/token/nmtoken/date + patterns: list[str] = field(default_factory=list) # neutral core: raw XSD facets + # The pattern facets as ONE anchored regex in the portable dialect + # (literals, character classes, quantifiers, alternation, grouping -- + # parses identically in RE2, PCRE, ECMAScript, Python). XSD's implicit + # whole-value anchoring is made explicit and its \i/\c name-class + # escapes are expanded; see build.portable_pattern. None when the type + # has no pattern facet. A target that enforces patterns compiles this; + # one that does not simply never mentions it. + pattern: str | None = None + # The structured view of a literal-prefixed name-token pattern + # (`coda\c*`, `(acc|medRenFla|...)\c+`): the literal prefix alternatives + # 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). + prefixes: list[Variant] = field(default_factory=list) + multi_prefix: bool = False + suffix_required: bool = False + min_length: str | None = None + max_length: str | None = None + length: str | None = None + target_type: str = "" # type_map[base] + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + kind: str = "string" + strategy: str = "string-wrapper" + + +@dataclass +class UnionPlateMember: + """Exactly one of ref/literals is set: a resolved reference to a member + type, or an inline literal set projected like a tiny anonymous enum (each + literal carries its wire form and a variant identifier). A ref member also + carries `name`, the referenced type's name bundle, so a template can spell + the member's field without inventing a name (a primitive member like + `positive_integer` has no plate to look it up on), and `tag`, the final + discriminator-constant identifier for this member, scoped exactly like an + enum variant and covered by the same collision gate. A primitive numeric + member carries its `clamp` policy (the primitive-implied bounds), so the + union enforces the same leniency as a named number type would.""" + + ref: PlateRef | None = None + name: Name | None = None + tag: Variant | None = None + literals: list[Variant] | None = None + clamp: list[ClampStep] = field(default_factory=list) + + +@dataclass +class UnionPlate: + name: Name + ident: str + members: list[UnionPlateMember] = field(default_factory=list) + # True when a member accepts ANY string (a string-family member, last by + # the union-order gate): in-order parsers never fall through. + open_ended: bool = False + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + kind: str = "union" + strategy: str = "tagged-variant" + + +ValuePlate = EnumPlate | NumberPlate | StringPlate | UnionPlate + + +# --------------------------------------------------------------------------- # +# Complex plates (mirror the IR's 4 complex shapes) +# --------------------------------------------------------------------------- # + + +@dataclass +class Member: + """One field of a complex plate: an attribute, a child element, or the + text value body of a `value`-shaped type. `cardinality` (required / + optional / vector) plus the target's type map fully determine the concrete + wrapper spelling (by-value, optional, collection); the template prints it. + + `default`/`fixed` keep the wire literal. When that literal names a variant + of the member's enum type, `default_variant` carries the variant's target + identifier so an emitter writes the enum member, not a raw string.""" + + name: Name + ident: str + kind: str # "attribute" | "element" | "value" + type_ref: PlateRef + cardinality: str # "required" | "optional" | "vector" + default: str | None = None + fixed: str | None = None + default_variant: str | None = None + # The configured import repair for a required attribute MISSING from a + # document ([import] attribute-defaults, plan §2.4): the parser injects + # this wire literal; without one, missing is a parse error. + import_default: str | None = None + doc: str | None = None + + +@dataclass +class ComplexPlate: + """One complex type, projected. `members` is the flat, deduped, ordered + field list a code target emits (attributes, then the value body, then + child elements in document order); `content` is the resolved + sequence/choice particle tree for a target that cares about order and + choice structure. + + `content` deliberately re-presents the IR's particle node types + (Sequence/Choice/Element from gen.ir.model, groups already spliced): the + neutral core IS the IR re-presented, and a parallel node hierarchy would + only drift. Those node types are therefore part of this layer's public + contract. A template joining a content occurrence back to the field it + populates uses `member(wire, kind="element")` rather than re-walking. + + A derived plate exposes both the `base` edge (for a target with + inheritance) and `all_members` (the base chain merged, for one without); + `strategy` says which one this target uses. Both views are always + populated for derived plates so the collision gate covers them under + either strategy.""" + + name: Name + ident: str + shape: str # "value" | "composite" | "empty" | "derived" + strategy: str # value-class | composite-class | flag | attrs-class | inherit | flatten + members: list[Member] = field(default_factory=list) + content: ir.Particle | None = None + # The grammar-preserving field projection of `content` (plan §2.9): + # ordered fields a structural target renders instead of the flat + # `members` element view. Empty for content-free shapes. + fields: list[ContentField] = field(default_factory=list) + # Every element name anywhere in the resolved content, for + # misplaced-vs-unknown error classification in strict parsers. + element_names: list[str] = field(default_factory=list) + base: PlateRef | None = None + all_members: list[Member] | None = None + # True when some derived type extends this one (an inheriting target + # cannot seal the class). + is_base: bool = False + presence_only: bool = False + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + # The dependency list of the grammar-preserving view: attribute/value + # members plus content fields plus the base — what a structural target + # imports instead of `deps` (which serves the flat member view). + structural_deps: list[PlateRef] = field(default_factory=list) + kind: str = "complex" + + def member(self, wire: str, kind: str | None = None) -> Member: + """The member a content occurrence or attribute wire name populates. + `kind` disambiguates the rare wire name carried by both an attribute + and an element (e.g. barline's segno).""" + for m in self.members: + if m.name.wire == wire and (kind is None or m.kind == kind): + return m + raise KeyError(f"{self.name.wire}: no member {wire!r} (kind={kind})") + + def members_view(self) -> list[Member]: + """The member list this plate's strategy renders: the merged + base-chain view when flattening a derived type, own members + otherwise. Backends render this; they never re-derive it.""" + if self.strategy == "flatten" and self.all_members is not None: + return self.all_members + return self.members + + +# --------------------------------------------------------------------------- # +# Content fields (the grammar-preserving projection, plan §2.9) +# --------------------------------------------------------------------------- # + + +@dataclass +class ContentField: + """One ordered field of a grammar-preserving content projection: an + element occurrence, a structural group reference, or a (possibly + synthesized) choice. The cardinality plus the bounds fully determine the + wrapper a structural target renders (by-value / optional / collection / + capped collection); `first` is the element-name dispatch set a strict + in-order parser tests the next element against. + + `presence` marks a field whose element type is presence-only (the only + information is whether it appears): a flag in optional position, a + marker value elsewhere. `min1` marks a repeated field that must never be + empty (minOccurs >= 1); `max` carries a finite maxOccurs > 1 as a + literal for bounded-append enforcement, None otherwise.""" + + name: Name + ident: str + kind: str # "element" | "group" | "choice" + type_ref: PlateRef + cardinality: str # "required" | "optional" | "vector" + tag: str | None = None # the element wire name; None for group/choice + presence: bool = False + min1: bool = False + max: str | None = None + # True when the field's content can match the empty element sequence (a + # required all-optional group, a nullable choice): a strict parser + # leaves the default-constructed value instead of erroring. + nullable: bool = False + first: list[str] = field(default_factory=list) + doc: str | None = None + + +@dataclass +class Alternative: + """One alternative of a choice. `cardinality` is "required" (one + instance) or "vector" (the alternative is itself a repeat, e.g. + direction-type's `rehearsal+`); a vector alternative with `min1` False + is nullable (the choice can be satisfied empty, e.g. key's + non-traditional-key*). Alternatives are positional: a target whose sum + type allows duplicate member types must dispatch by index, never by + type (plan §2.3).""" + + name: Name + ident: str + kind: str # "element" | "group" | "choice" + type_ref: PlateRef + cardinality: str # "required" | "vector" + tag: str | None = None + presence: bool = False + min1: bool = False + max: str | None = None + # True on the alternative a default-constructed choice value holds: the + # first nullable alternative when the choice has one (the natural zero + # of a nullable choice is "empty"), else the first alternative. + default_alt: bool = False + first: list[str] = field(default_factory=list) + doc: str | None = None + + +@dataclass +class GroupPlate: + """A named schema group (full-note, editorial, ...) or a synthesized + anonymous sequence hoisted out of a content tree, projected as a shared + type with ordered content fields. Groups are transparent on the wire + (no enclosing element); `element_names` is every element name anywhere + inside, for misplaced-vs-unknown error classification.""" + + name: Name + ident: str + fields: list[ContentField] = field(default_factory=list) + element_names: list[str] = field(default_factory=list) + synthesized: bool = False + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + kind: str = "group" + strategy: str = "group-class" + + +@dataclass +class ChoicePlate: + """A choice projected as a shared sum type: a named schema group whose + content is a single choice (music-data), or a synthesized anonymous + choice hoisted out of a content tree. `nullable` is True when some + alternative can match the empty element sequence (the choice can be + satisfied without consuming anything).""" + + name: Name + ident: str + alternatives: list[Alternative] = field(default_factory=list) + element_names: list[str] = field(default_factory=list) + nullable: bool = False + synthesized: bool = False + doc: str | None = None + doc_lines: list[str] = field(default_factory=list) + deps: list[PlateRef] = field(default_factory=list) + kind: str = "choice" + strategy: str = "choice-class" + + +GroupLikePlate = GroupPlate | ChoicePlate + + +# --------------------------------------------------------------------------- # +# The whole projected target +# --------------------------------------------------------------------------- # + + +def attribute_members(members: list[Member]) -> list[Member]: + """The shape queries backends partition a member list with. They live + here, beside the data, so every backend asks the same question the same + way instead of filtering inline.""" + return [m for m in members if m.kind == "attribute"] + + +def element_members(members: list[Member]) -> list[Member]: + return [m for m in members if m.kind == "element"] + + +def value_member(members: list[Member]) -> Member | None: + return next((m for m in members if m.kind == "value"), None) + + +@dataclass +class Plates: + """The complete projection of one target: every plate, in the IR's + deps-first order (value types never reference complex types, so + `value_types + complex_types` is a valid total emit order).""" + + source: str # provenance: the XSD stem the IR was lowered from + target: TargetInfo + schema_version: str = "" # the MusicXML version in the source stem ("3.1") + value_types: list[ValuePlate] = field(default_factory=list) + complex_types: list[ComplexPlate] = field(default_factory=list) + # The shared content types of the grammar-preserving projection (plan + # §2.9): structural named schema groups plus the synthesized types + # hoisted out of anonymous nested choices/sequences. A target consuming + # only the flat member view never renders these. + groups: list[GroupLikePlate] = field(default_factory=list) + roots: list[PlateRef] = field(default_factory=list) + + def __post_init__(self): + # Random-access index for templates; a plain attribute (not a + # dataclass field) so JSON dumps stay free of the duplication. + self._index = {p.name.wire: p for p in self.value_types} + self._index.update({p.name.wire: p for p in self.complex_types}) + self._index.update({p.name.wire: p for p in self.groups}) + + def plate(self, wire: str) -> ValuePlate | ComplexPlate: + """Look up any plate by its wire type name.""" + return self._index[wire] + + def has_plate(self, wire: str) -> bool: + return wire in self._index + + def children_owner(self, plate: ComplexPlate) -> ComplexPlate | None: + """For an inheriting target: the base-chain plate whose child struct + holds this type's children -- the nearest ancestor (or self) with + element members. Schema reasoning, so it lives here, not in a + template.""" + cur: ComplexPlate | None = plate + while cur is not None: + if element_members(cur.members): + return cur + cur = self.plate(cur.base.wire) if cur.base is not None else None + return None diff --git a/gen/press/__init__.py b/gen/press/__init__.py new file mode 100644 index 000000000..c762fe0fa --- /dev/null +++ b/gen/press/__init__.py @@ -0,0 +1,5 @@ +"""The press: renders the targets' templates. See gen.press.engine.""" + +from gen.press.engine import Press, PressError + +__all__ = ["Press", "PressError"] diff --git a/gen/press/context.py b/gen/press/context.py new file mode 100644 index 000000000..9519b212a --- /dev/null +++ b/gen/press/context.py @@ -0,0 +1,262 @@ +"""Build render contexts from the plates. + +The press is pure Mustache, so everything a template branches on or prints +must arrive as data. This module converts the plates into plain dicts with +three mechanical enrichments -- none of which makes a decision, language or +otherwise: + + 1. Discriminant expansion: every closed enumerated field (`kind`, + `category`, `cardinality`, `strategy`, `shape`, `node`, ...) gets a + boolean companion per vocabulary value (`kind: "enum"` -> `is_enum: + True`, `is_number: False`, ...). All flags are materialized so the + engine's strict mode never trips on a legitimate branch. + 2. Quoted companions: every string field gets `_q`, a double-quoted + backslash-escaped literal (JSON repertoire, non-ASCII as \\uXXXX -- + valid verbatim in C, C++, Go, Java, JavaScript, and Rust). + 3. Loop metadata: every list item gets `is_first` / `is_last` / `index0`; + items that are bare strings are lifted to `{value, value_q, ...}` so the + metadata has somewhere to live. + +Plus the pre-split member views templates iterate (attributes / elements / +value, own and merged), a `type` self-reference so inner scopes can reach +plate-level fields, and the generated-file banner text. +""" + +from __future__ import annotations + +import dataclasses +import json + +from gen.names import Name +from gen.plates.model import ( + ChoicePlate, + ComplexPlate, + GroupPlate, + Plates, + UnionPlate, + attribute_members, + element_members, + value_member, +) +from gen.press.writer import banner + +# The closed vocabularies, by field name. A value outside its field's +# vocabulary is a build bug, so it fails loud here. +_DISCRIMINANTS: dict[str, tuple[str, ...]] = { + "kind": ( + "enum", "number", "string", "union", "complex", "group", "choice", + "attribute", "element", "value", + "primitive-decimal", "primitive-integer", "primitive-string", + ), + "category": ("complex", "value", "primitive", "group", "choice"), + "cardinality": ("required", "optional", "vector"), + "strategy": ( + "enum-class", "numeric-wrapper", "string-wrapper", "tagged-variant", + "value-class", "composite-class", "flag", "attrs-class", + "inherit", "flatten", "group-class", "choice-class", + ), + "shape": ("value", "composite", "empty", "derived"), + "node": ("element", "sequence", "choice", "group"), + "variant_scope": ("bare", "composed"), + "family": ("decimal", "integer"), +} + + +def quoted(value: str) -> str: + return json.dumps(value, ensure_ascii=True) + + +def _flag(value: str) -> str: + return "is_" + value.replace("-", "_") + + +def _convert(obj): + """Dataclasses to dicts, recursively, with the enrichments applied.""" + if isinstance(obj, Name): + # Casings flatten onto the name so templates say {{name.snake}}. + out = {"wire": obj.wire, "wire_q": quoted(obj.wire)} + for convention, ident in obj.cased.items(): + out[convention] = ident + out[convention + "_q"] = quoted(ident) + return out + if dataclasses.is_dataclass(obj): + out: dict = {} + for f in dataclasses.fields(obj): + name = f.name + value = getattr(obj, name) + out[name] = _convert(value) + if isinstance(value, str): + out[name + "_q"] = quoted(value) + vocab = _DISCRIMINANTS.get(name) + if vocab is not None: + if value not in vocab: + raise ValueError( + f"{type(obj).__name__}.{name} = {value!r} is outside " + f"its vocabulary {vocab}" + ) + for v in vocab: + # Vocabularies overlap across fields of one object + # (PlateRef has category "value" and kind "enum"; + # the kind vocabulary also contains "value"). + # Earlier fields win: category's is_value/is_complex + # must not be clobbered by the kind expansion, and + # the two always agree where they overlap. + out.setdefault(_flag(v), v == value) + elif isinstance(value, (list, tuple)): + # Iterating a section already gates emptiness; has_ + # serves the non-iterating tests (wrap-once framing). + out["has_" + name] = bool(value) + return out + if isinstance(obj, (list, tuple)): + return _listify([_convert(item) for item in obj]) + if isinstance(obj, dict): + return {k: _convert(v) for k, v in obj.items()} + return obj + + +def _listify(items: list) -> list: + """Attach loop metadata; lift bare scalars so it has somewhere to live.""" + out = [] + last = len(items) - 1 + for i, item in enumerate(items): + if not isinstance(item, dict): + item = {"value": item} + if isinstance(item["value"], str): + item["value_q"] = quoted(item["value"]) + else: + item = dict(item) + item["is_first"] = i == 0 + item["is_last"] = i == last + item["index0"] = i + out.append(item) + return out + + +def _common(plates: Plates) -> dict: + return { + "target": _convert(plates.target), + "vars": dict(plates.target.vars), + "schema_version": plates.schema_version, + "source": plates.source, + "generated_banner": banner(plates.source), + } + + +def plate_context(plates: Plates, plate) -> dict: + """The context a per-type template renders against: the plate's fields, + the member views, the target facts, and a `type` self-reference so inner + scopes (a member loop, a variant loop) can still reach plate fields that + their own frame shadows.""" + ctx = _convert(plate) + if isinstance(plate, UnionPlate): + # The flattened case view: one entry per ref member and per literal, + # in schema order, each carrying its discriminator constant as + # `tag_ident` -- so loop metadata (ordinals, commas, first-member + # handling) works on the granularity the kind enum actually has. + kind_flags = [_flag(v) for v in _DISCRIMINANTS["kind"]] + cases = [] + for m in plate.members: + if m.ref is not None: + ref = _convert(m.ref) + case = { + "is_literal": False, + "tag_ident": m.tag.ident, + "ref": ref, + "name": _convert(m.name), + "clamp": _convert(m.clamp), + "has_clamp": bool(m.clamp), + "wire": None, + "wire_q": None, + } + # The referenced kind's flags, flattened onto the case so + # templates branch without reaching through `ref`. + for flag in kind_flags: + case[flag] = ref[flag] + cases.append(case) + else: + for variant in m.literals or []: + case = { + "is_literal": True, + "tag_ident": variant.ident, + "ref": None, + "name": _convert(variant.name), + "clamp": [], + "has_clamp": False, + "wire": variant.wire, + "wire_q": quoted(variant.wire), + } + for flag in kind_flags: + case[flag] = False + cases.append(case) + ctx["cases"] = _listify(cases) + if isinstance(plate, ComplexPlate): + ctx["attributes"] = _listify( + [_convert(m) for m in attribute_members(plate.members)] + ) + ctx["elements"] = _listify( + [_convert(m) for m in element_members(plate.members)] + ) + value = value_member(plate.members) + ctx["value"] = _convert(value) if value is not None else None + merged = plate.all_members if plate.all_members is not None else plate.members + ctx["merged_attributes"] = _listify( + [_convert(m) for m in attribute_members(merged)] + ) + ctx["merged_elements"] = _listify( + [_convert(m) for m in element_members(merged)] + ) + merged_value = value_member(merged) + ctx["merged_value"] = _convert(merged_value) if merged_value is not None else None + for key in ("attributes", "elements", "merged_attributes", "merged_elements"): + ctx["has_" + key] = bool(ctx[key]) + if isinstance(plate, (ComplexPlate, GroupPlate, ChoicePlate)): + # Mechanical aggregations over the plate's references and content + # items (no decisions): which kinds of payloads appear anywhere, so + # a template can frame imports/includes without a per-item loop. + refs = [] + items = [] + if isinstance(plate, ComplexPlate): + refs += [m.type_ref for m in plate.members] + refs += [m.type_ref for m in (plate.all_members or [])] + items = list(plate.fields) + elif isinstance(plate, GroupPlate): + items = list(plate.fields) + else: + items = list(plate.alternatives) + 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_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) + ctx["any_optional"] = any( + f.cardinality == "optional" for f in items + ) or any( + m.cardinality == "optional" + for m in (plate.members if isinstance(plate, ComplexPlate) else []) + ) + ctx.update(_common(plates)) + ctx["type"] = ctx + return ctx + + +def target_context(plates: Plates, outputs: list[str]) -> dict: + """The context a once-per-target template renders against: every plate, + the roots, and the full output manifest (`outputs`, plus `outputs_by_ext` + grouped by final extension so a build manifest can list just its + sources).""" + ctx = _common(plates) + ctx["value_types"] = _listify([plate_context(plates, p) for p in plates.value_types]) + ctx["complex_types"] = _listify( + [plate_context(plates, p) for p in plates.complex_types] + ) + ctx["groups"] = _listify([plate_context(plates, p) for p in plates.groups]) + ctx["roots"] = _convert(list(plates.roots)) + paths = sorted(outputs) + ctx["outputs"] = _listify([{"path": p, "path_q": quoted(p)} for p in paths]) + by_ext: dict[str, list] = {} + for p in paths: + ext = p.rsplit(".", 1)[-1] if "." in p else "" + by_ext.setdefault(ext, []).append({"path": p, "path_q": quoted(p)}) + ctx["outputs_by_ext"] = {ext: _listify(items) for ext, items in by_ext.items()} + return ctx diff --git a/gen/press/engine.py b/gen/press/engine.py new file mode 100644 index 000000000..b368336b9 --- /dev/null +++ b/gen/press/engine.py @@ -0,0 +1,399 @@ +"""The press: a Mustache template engine. + +The press renders the targets' templates. The template language is Mustache +-- the published spec's interpolation, sections, inverted sections, partials, +comments, and set-delimiter core, with spec whitespace semantics (standalone +lines, partial call-site indentation) -- and three deliberate deviations, +because code generation is not HTML (design: generator-agnosticism.md): + + 1. Missing keys are render errors (template:line in the message). The spec + mandates silent empty output, which is the worst failure mode a code + generator can have. A key that is PRESENT with a None/empty value + renders empty and is falsey in sections; only absence is an error. + 2. No HTML escaping: `{{x}}` interpolates verbatim ({{{x}}} and {{&x}} are + accepted synonyms). + 3. No lambdas (the spec's escape hatch into logic). A callable in the + context is an error. + +Conformance to everything else is tested against the vendored official spec +suite (gen/tests/mustache_spec/); the constructor's `strict` and `escape` +parameters exist so that suite can exercise the spec's own semantics -- the +production pipeline never passes them. + +What the engine will never grow: expressions, comparisons, arithmetic, +filters, string manipulation, casing, assignment, or new syntax. Dispatch +data (booleans, loop metadata, quoted literals) is the context builder's +job; if a template cannot express something, the plates must carry it. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field + + +class PressError(Exception): + """A template problem, always reported as `template:line: message`.""" + + def __init__(self, name: str, line: int, message: str): + self.template = name + self.line = line + super().__init__(f"{name}:{line}: {message}") + + +# --------------------------------------------------------------------------- # +# Parse tree +# --------------------------------------------------------------------------- # + + +@dataclass +class _Text: + text: str + + +@dataclass +class _Var: + path: tuple[str, ...] + raw: bool # {{{x}}} / {{&x}}: spec semantics; identical here by default + line: int + + +@dataclass +class _Section: + path: tuple[str, ...] + inverted: bool + line: int + children: list = field(default_factory=list) + + +@dataclass +class _Partial: + name: str + indent: str + line: int + + +# --------------------------------------------------------------------------- # +# Tokenizer (with set-delimiter support and the spec's standalone-line rules) +# --------------------------------------------------------------------------- # + +_STANDALONE_KINDS = {"open", "inv", "close", "comment", "delim", "partial"} + + +def _tokenize(template: str, name: str) -> list: + """Produce ('text', str) and ('tag', kind, key, line, indent) tokens. + kind: var | raw | open | inv | close | partial | comment | delim.""" + tokens: list = [] + odelim, cdelim = "{{", "}}" + pos = 0 + line = 1 + while pos < len(template): + start = template.find(odelim, pos) + if start < 0: + tokens.append(("text", template[pos:])) + break + if start > pos: + text = template[pos:start] + tokens.append(("text", text)) + line += text.count("\n") + + # Triple mustache is only meaningful with the default delimiters. + if odelim == "{{" and template.startswith("{{{", start): + end = template.find("}}}", start + 3) + if end < 0: + raise PressError(name, line, "unclosed '{{{' tag") + key = template[start + 3 : end].strip() + tokens.append(("tag", "raw", key, line, "")) + pos = end + 3 + continue + + end = template.find(cdelim, start + len(odelim)) + if end < 0: + raise PressError(name, line, f"unclosed '{odelim}' tag") + content = template[start + len(odelim) : end] + pos = end + len(cdelim) + line += content.count("\n") + + sigil = content[:1] + if sigil == "#": + tokens.append(("tag", "open", content[1:].strip(), line, "")) + elif sigil == "^": + tokens.append(("tag", "inv", content[1:].strip(), line, "")) + elif sigil == "/": + tokens.append(("tag", "close", content[1:].strip(), line, "")) + elif sigil == ">": + tokens.append(("tag", "partial", content[1:].strip(), line, "")) + elif sigil == "!": + tokens.append(("tag", "comment", "", line, "")) + elif sigil == "&": + tokens.append(("tag", "raw", content[1:].strip(), line, "")) + elif sigil == "=": + inner = content[1:].rstrip() + if not inner.endswith("="): + raise PressError(name, line, "malformed set-delimiter tag") + parts = inner[:-1].split() + if len(parts) != 2: + raise PressError(name, line, "malformed set-delimiter tag") + tokens.append(("tag", "delim", "", line, "")) + odelim, cdelim = parts + else: + tokens.append(("tag", "var", content.strip(), line, "")) + return _strip_standalone(tokens) + + +def _strip_standalone(tokens: list) -> list: + """The spec's standalone-line rule: a line whose text is all whitespace + and which carries exactly one section/inverted/close/comment/partial/ + set-delimiter tag contributes no output for the line itself. A standalone + partial keeps the line's leading whitespace as the indentation applied to + its rendered content.""" + # Split text tokens so each line of the template is its own token run. + split: list = [] + for tok in tokens: + if tok[0] != "text": + split.append(tok) + continue + text = tok[1] + while True: + nl = text.find("\n") + if nl < 0: + if text: + split.append(("text", text)) + break + split.append(("text", text[: nl + 1])) + text = text[nl + 1 :] + + out: list = [] + line: list = [] + + def flush(line_tokens: list) -> None: + tags = [t for t in line_tokens if t[0] == "tag"] + texts = [t[1] for t in line_tokens if t[0] == "text"] + standalone = ( + len(tags) == 1 + and tags[0][1] in _STANDALONE_KINDS + and all(not t.strip() for t in texts) + ) + if not standalone: + out.extend(line_tokens) + return + tag = tags[0] + if tag[1] == "partial": + indent = "" + for t in line_tokens: + if t[0] == "tag": + break + indent += t[1] + tag = ("tag", "partial", tag[2], tag[3], indent) + out.append(tag) + + for tok in split: + line.append(tok) + if tok[0] == "text" and tok[1].endswith("\n"): + flush(line) + line = [] + if line: + flush(line) + return out + + +# --------------------------------------------------------------------------- # +# Parser +# --------------------------------------------------------------------------- # + + +def _path(key: str) -> tuple[str, ...]: + return (".",) if key == "." else tuple(key.split(".")) + + +def _parse(tokens: list, name: str) -> list: + root: list = [] + stack: list[tuple[_Section, str]] = [] + current = root + for tok in tokens: + if tok[0] == "text": + if tok[1]: + current.append(_Text(tok[1])) + continue + _, kind, key, line, indent = tok + if kind in ("var", "raw"): + current.append(_Var(_path(key), kind == "raw", line)) + elif kind in ("open", "inv"): + section = _Section(_path(key), kind == "inv", line) + current.append(section) + stack.append((section, key)) + current = section.children + elif kind == "close": + if not stack or stack[-1][1] != key: + raise PressError(name, line, f"unexpected section close '{key}'") + stack.pop() + current = stack[-1][0].children if stack else root + elif kind == "partial": + current.append(_Partial(key, indent, line)) + # comments and delim changes contribute nothing + if stack: + section, key = stack[-1] + raise PressError(name, section.line, f"unclosed section '{key}'") + return root + + +# --------------------------------------------------------------------------- # +# Renderer +# --------------------------------------------------------------------------- # + +_MISS = object() + + +class Press: + """Renders Mustache templates. `partials` maps a partial name to its + template text (a dict, or a callable for file-backed loading). The + `strict` and `escape` knobs exist for the spec conformance suite; the + production pipeline uses the defaults (strict, verbatim).""" + + def __init__( + self, + partials: Mapping[str, str] | Callable[[str], str] | None = None, + strict: bool = True, + escape: Callable[[str], str] | None = None, + max_partial_depth: int = 64, + ): + self._partials = partials or {} + self._strict = strict + self._escape = escape + self._max_depth = max_partial_depth + self._cache: dict[tuple[str, str, str], list] = {} + + def render(self, template: str, context, name: str = "