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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/mx-api-doctrine/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ Responses to wrong api usage, in order of preference:
returns a default-constructed copy; the writer drops the half of an encoding that is
meaningless for the note it is on (a tie on a silent cue note is written as `<tied>`
notation only, never as a sound-level `<tie>`). No signal to the caller.
3. `Result<T>` (`Result.h`): the error channel of last resort. It exists for the
`DocumentManager` I/O boundary, where failure is real (unreadable file, unparseable XML). Do
not spread it into the data model.
3. `Result<T>` (`Result.h`): the error channel of last resort. It exists for the `MusicXml`
I/O boundary, where failure is real (unreadable file, unparseable XML). Do not spread it
into the data model.

Never:

- UB. No public call sequence may reach undefined behavior: no unchecked `std::get`, no
returned reference whose validity depends on a precondition, no "caller must check first or
else".
- Exceptions. Nothing throws across the api boundary, and an exception is never how a failed
precondition is reported to the caller. `DocumentManager` catches everything
precondition is reported to the caller. The `MusicXml` functions catch everything
(`ResultCode::internalError`).

## Choice types: when you wish for a Rust enum
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/api-headers.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ simpler model (doctrine: `.claude/skills/mx-api-doctrine/SKILL.md`). Review for:
duplicated, or id-linked; check the change against the principles doc.
- A new positioned-in-a-measure type needs `int tickTimePosition`; durations are in ticks.
- No UB or exceptions reachable through the public interface; a failed precondition must never
throw. Flag new `Result` usage outside `DocumentManager`, unchecked `std::get`, or accessors
throw. Flag new `Result` usage outside `MusicXml`, unchecked `std::get`, or accessors
that return references guarded only by a precondition.
- Kind-specific payloads use the choice-class pattern (`TimeChoice.h`, `MarkDataChoice.h`), not
loose fields that apply only to some kinds.
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ comment on a PR or the Coverage workflow's "Run workflow" button (`.github/workf

| File | What it is |
|------|------------|
| `src/include/mx/api/DocumentManager.h` | The public API entry point: createFromFile, createFromScore, getData, writeToFile |
| `src/include/mx/api/MusicXml.h` | The public API entry point: fromFile, fromStream, fromScore, getScore, intoScore, clone, writeTo* |
| `src/include/mx/api/ScoreData.h` | The primary api data model (ScoreData, PartData, MeasureData, ...) |
| `src/private/mx/api/DocumentManager.cpp` | API implementation: error channel, parse/serialize orchestration |
| `src/private/mx/api/MusicXml.cpp` | API implementation: error channel, parse/serialize orchestration |
| `src/private/mx/impl/ScoreReader.cpp` | Translates mx::core -> mx::api ScoreData |
| `src/private/mx/impl/ScoreWriter.cpp` | Translates mx::api ScoreData -> mx::core |
| `src/private/mx/impl/NoteReader.cpp` | Core -> api note translation (one of the largest impl files) |
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})

# Emscripten disables exception catching by default; mx's internal MX_THROW
# (Throw.h) needs it, so DocumentManager can actually catch and convert it.
# (Throw.h) needs it, so the api boundary can actually catch and convert it.
#
# Emscripten's default wasm stack is 64 KiB. mx's write path is a deep,
# unoptimized (Debug) C++ call chain -- DocumentManager -> ScoreWriter ->
# unoptimized (Debug) C++ call chain -- fromScore -> ScoreWriter ->
# PartWriter -> MeasureWriter -> NoteWriter -> NotationsWriter, several of
# which carry their own sizable locals -- and it overflows that default with
# only a little headroom to spare (issue #389 hit this by adding two more
Expand Down
49 changes: 18 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ git add --all && git commit -m'mx sourcecode'
# create a main.cpp file
cat <<- "EOF" > main.cpp
#include <iostream>
#include "mx/api/MusicXml.h"
#include "mx/api/ScoreData.h"
#include "mx/api/DocumentManager.h"

int main () {
using namespace mx::api;
Expand All @@ -148,10 +148,8 @@ int main () {
PartData part{};
part.measures.push_back(measure);
score.parts.push_back(part);
auto& mgr = DocumentManager::getInstance();
const auto idResult = mgr.createFromScore(score);
mgr.writeToStream(idResult.value(), std::cout);
mgr.destroyDocument(idResult.value());
const auto docResult = fromScore(score);
std::move(docResult).value().writeToStream(std::cout);
}
EOF

Expand Down Expand Up @@ -207,7 +205,7 @@ in `mx::api`, such as the need to manage beam starts and stops explicitly.
#include <cstdint>
#include <sstream>

#include "mx/api/DocumentManager.h"
#include "mx/api/MusicXml.h"
#include "mx/api/ScoreData.h"

// set this to 1 if you want to see the xml in your console
Expand Down Expand Up @@ -313,26 +311,22 @@ int main(int argc, const char * argv[])
note.beams.clear();
voice.notes.push_back( note );

// the document manager is the liaison between our score data and the MusicXML DOM.
// it completely hides the MusicXML DOM from us when using mx::api
auto& mgr = DocumentManager::getInstance();
const auto idResult = mgr.createFromScore( score );
if( !idResult.ok() ) { return 1; }
const auto documentID = idResult.value();
// a MusicXml document is created from the score data and owns the
// underlying MusicXML model, which is hidden from us when using mx::api
const auto docResult = fromScore( score );
if( !docResult.ok() ) { return 1; }
const auto document = std::move( docResult ).value();

// write to the console
#if MX_WRITE_THIS_TO_THE_CONSOLE
mgr.writeToStream( documentID, std::cout );
document.writeToStream( std::cout );
std::cout << std::endl;
#endif

// write to a file. argv[1] overrides the default output path so the build
// system can send the file to a gitignored location (see issue #150).
const std::string outputPath = ( argc > 1 ) ? argv[1] : "./example.musicxml";
const auto writeResult = mgr.writeToFile( documentID, outputPath );

// we need to explicitly delete the object held by the manager
mgr.destroyDocument( documentID );
const auto writeResult = document.writeToFile( outputPath );

return writeResult.ok() ? 0 : 1;
}
Expand All @@ -341,7 +335,7 @@ int main(int argc, const char * argv[])
#### Reading MusicXML with `mx::api`

```C++
#include "mx/api/DocumentManager.h"
#include "mx/api/MusicXml.h"
#include "mx/api/ScoreData.h"

#include <string>
Expand Down Expand Up @@ -396,23 +390,16 @@ int main(int argc, const char * argv[])
{
using namespace mx::api;

// create a reference to the singleton which holds documents in memory for us
auto& mgr = DocumentManager::getInstance();

// place the xml from above into a stream object
std::istringstream istr{ xml };

// ask the document manager to parse the xml into memory for us, returns a document ID.
const auto idResult = mgr.createFromStream( istr );
if( !idResult.ok() ) { return MX_IS_A_FAILURE; }
const auto documentID = idResult.value();

// get the structural representation of the score from the document manager
const auto scoreResult = mgr.getData( documentID );

// we need to explicitly destroy the document from memory
mgr.destroyDocument( documentID );
// parse the xml into a MusicXml document that we own
const auto docResult = MusicXml::fromStream( istr );
if( !docResult.ok() ) { return MX_IS_A_FAILURE; }

// take the score out of the document. intoScore also consumes the
// document, so its memory is freed as the function returns
const auto scoreResult = intoScore( std::move( docResult ).value() );
if( !scoreResult.ok() ) { return MX_IS_A_FAILURE; }
const auto& score = scoreResult.value();

Expand Down
91 changes: 0 additions & 91 deletions src/include/mx/api/DocumentManager.h

This file was deleted.

89 changes: 89 additions & 0 deletions src/include/mx/api/MusicXml.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#pragma once

#include "mx/api/Result.h"
#include "mx/api/ScoreData.h"

#include <iosfwd>
#include <memory>
#include <string>

namespace mx
{
namespace core
{
class Document;
} // namespace core

namespace api
{
// A MusicXML document, either parsed or constructed from ScoreData.
class MusicXml
{
public:
// Parses a MusicXML file. Logical errors and caught exceptions are
// represented by an error result.
static Result<MusicXml> fromFile(const std::string &filePath);

// Parses a MusicXML document from a character stream. Logical errors and
// caught exceptions are represented by an error result.
static Result<MusicXml> fromStream(std::istream &stream);

MusicXml(const MusicXml &other) = delete;
MusicXml &operator=(const MusicXml &other) = delete;
MusicXml(MusicXml &&other);
MusicXml &operator=(MusicXml &&other) noexcept;
~MusicXml();

// A deep copy of the document.
MusicXml clone() const;

// Writes the document to a file. Logical errors and caught exceptions
// are represented by an error result.
Result<void> writeToFile(const std::string &filePath) const;

// Writes the document to a character stream.
Result<void> writeToStream(std::ostream &stream) const;

// This is an escape hatch in case mx::api does not do what you need and
// you want to edit the core DOM directly. You will need to include the
// private mx::core headers in your header search paths to do so. Not
// recommended, try opening an issue first!
//
// The reference is only good for as long as this MusicXml is alive and
// you have not moved it away: do not keep it past a std::move of this
// object into another MusicXml or into intoScore, which destroys the
// document when it returns.
core::Document &getCoreDocument();
const core::Document &getCoreDocument() const;

private:
MusicXml();
MusicXml(core::Document document, bool writeMxVersion);
class Impl;
std::unique_ptr<Impl> myImpl;
Comment thread
webern marked this conversation as resolved.

friend Result<ScoreData> getScore(const MusicXml &document);
friend Result<MusicXml> fromScore(const ScoreData &score);
};

// Reads the score out of the document. The document stays alive and can be
// read again or written out.
Result<ScoreData> getScore(const MusicXml &document);

// Reads the score out of the document and consumes it: the underlying tree
// is freed when this function returns rather than when your MusicXml binding
// goes out of scope. Pass the document with std::move, or hand over the
// Result's value directly.
Result<ScoreData> intoScore(MusicXml document);

// Authors a new document from ScoreData. Fails with an error result when the
// ScoreData describes something the core model will not represent (e.g. more
// than 8 beams) rather than silently dropping data.
Result<MusicXml> fromScore(const ScoreData &score);

} // namespace api
Comment thread
webern marked this conversation as resolved.
} // namespace mx
7 changes: 4 additions & 3 deletions src/include/mx/api/Result.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace api
// The mx::api error vocabulary. mx::api owns its own codes: the core-boundary
// failures are mirrored (public headers never
// include private mx::core headers), and the api adds the codes core has no
// business knowing. No exceptions escape the DocumentManager boundary.
// business knowing. No exceptions escape the MusicXml boundary.
enum class ResultCode
{
ioError, // file open/read/write failure (api-level)
Expand All @@ -31,8 +31,9 @@ enum class ResultCode
tooManyElements,
invalidDocument,
unsupportedVersion, // mirrored from the core parse boundary
badDocumentId, // handle not in the registry (api-level)
internalError, // caught exception; nothing escapes (api-level)
// TODO: this badly swallows exception information. we need more variants
// or something like a site and message.
internalError, // caught exception; nothing escapes (api-level)
};

struct ApiError
Expand Down
Loading
Loading