Skip to content

feat: carry structured location and exception cause in api errors - #436

Merged
webern merged 1 commit into
m/mxdev-raiifrom
m/mxdev-errinfo
Sep 16, 2026
Merged

webern merged 1 commit into
m/mxdev-raiifrom
m/mxdev-errinfo

Conversation

@webern

@webern webern commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Human Summary

Introduces a mechanism for getting better error information out of mx::api. It won't be very helpful to humans, but an LLM can probably make some sense of the indices involved in an error. And xmlPath seems like it could be useful even to a human. Nothing pretty or fancy here, but information nonetheless.

Summary

internalError was a dead end: it swallowed the exception type, said nothing about where the problem was, and a TODO in Result.h said so. This gives ApiError two things it was missing: a place and a cause.

Location says where, in whichever world the error happened:

struct Location
{
    std::string xmlPath;  // /score-partwise/part[1]/measure[3]/note[2]
    int partIndex = -1;         // ScoreData coordinates; -1 = unknown
    int measureIndex = -1;
    int staffIndex = -1;
    int voiceIndex = -1;
    int tickTimePosition = -1;
    long long byteOffset = -1;  // raw XML syntax errors
};

cause (std::exception_ptr) keeps the actual exception that was caught, so a caller can rethrow it, inspect its type, or read its message; std::bad_alloc now maps to its own ResultCode::outOfMemory instead of hiding in internalError. The catch blocks in MusicXml.cpp capture std::current_exception() into cause.

Two sources of position are wired now, without threading any new context through the call stack:

  • The WriteRefusal sites in NoteWriter were already holding a MeasureCursor (part, measure, staff, voice, tick) when they refused a note; the error now carries it. A ninth beam comes back as tooManyElements at part=0 measure=0 staff=0 voice=0 tick=0.
  • XML syntax errors carry pugixml's byte offset. Parse/schema errors keep the element path they already had; the io errors say the file path in their message instead of pretending it was an XML path.

formatError(const ApiError&) renders an error the same way every time (mx: tooManyElements at part=0 measure=0: ...), for logs, tests, and anyone (or anything) reading test output.

The path field is replaced by location; aggregate initialization of ApiError changes shape, so this is breaking, but only for code that constructs errors directly. mx::core is untouched: the core mirror maps Error::path into location.xmlPath and stays lossless.

This is groundwork for #432: a warnings mechanism wants somewhere to say where and something to attach; this interface now has both.

Testing

  • ResultTest (new): Location defaults, formatError output pinned for bare codes, XML paths, score coordinates, and byte offsets
  • tooManyElementsCarriesThePlaceInTheScore: a 9-beam note refuses with the full cursor position, rendered by formatError
  • outOfMemoryIsReportedNotThrown: a std::bad_alloc thrown mid-parse comes back as outOfMemory, rethrowable from cause
  • internalErrorKeepsTheException: an unexpected exception comes back as internalError with the original type and message preserved in cause
  • Full suite passes: 5525 assertions in 610 test cases
  • make api-roundtrip regression: 414 passed, 0 failed
  • make fmt-check passes

References

@webern webern added feature new feature request breaking fixes or implementation that require breaking changes api Affects the mx::api layer ai Issues opened by, or through, a coding agent. labels Sep 12, 2026
@webern
webern added this pull request to stack #438 September 16, 2026 08:40
ApiError now says where and keeps what. Location holds the place in the
XML (element path, byte offset) or in the score (part, measure, staff,
voice, tick); the WriteRefusal sites stamp it from the cursor already
in scope. std::bad_alloc reaching the boundary is reported as
ResultCode::outOfMemory; every other caught exception is internalError
with the exception itself kept in std::exception_ptr cause.
formatError renders an error the same way every time.
@webern
webern merged commit eee8fd7 into main Sep 16, 2026
8 checks passed
@webern
webern deleted the m/mxdev-errinfo branch September 16, 2026 17:08
webern added a commit that referenced this pull request Sep 16, 2026
## Summary

Adds an optional `Diagnostics` collector to the `getScore`, `intoScore`,
and `fromScore` translation APIs. Callers can inspect reports after a
call or receive them synchronously through a handler while fatal
failures remain in `Result`.

The first reports cover two existing recoveries: an out-of-range
time-signature staff number being applied to all staves, and an
octave-shift stop with no matching start using the MusicXML default size
of 8. Diagnostics carry score locations and have a canonical formatter.
More than 16 concurrent numbered spanners remains fatal, but now returns
a located `tooManyElements` error instead of an unlocated internal
error.

## Testing

- [x] Diagnostics tests: 55 assertions in 7 test cases
- [x] `make api-test`: 5580 assertions in 617 test cases
- [x] `make api-roundtrip`: 414 passed, 0 failed
- [x] `make test-all`
- [x] `make wasm-test`
- [x] `make fmt`

## References

- Progresses #432
- Stacked on #436; merges after it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. api Affects the mx::api layer breaking fixes or implementation that require breaking changes feature new feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant