Skip to main content

sim_lib_music_notation/
runtime.rs

1//! Loadable notation profile and its single import callable.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    Args, Callable, ClassRef, Cx, Error, Export, ExportKind, ExportRecord, ExportState, Expr, Lib,
7    LibManifest, Linker, LoadCx, Object, ObjectCompat, RawArgs, Result, RuntimeId, ShapeRef,
8    Symbol, Value,
9};
10use sim_lib_core::{SurfaceField, SurfacePackLib, SurfacePackSpec, SurfaceValueSpec, install_once};
11use sim_lib_music_shapes::{MusicScoreDescriptor, encode_score, install_music_shapes_lib};
12use sim_shape::{AnyShape, ExactExprShape, ListShape, shape_value};
13
14use crate::{
15    MusicXmlLimits, NotationIdentityKind, NotationLossKind, import_musicxml_partwise_report,
16};
17
18const MUSIC_NOTATION_LIB_ID: &str = "music-notation";
19const EXPORT_KIND_NAME: &str = "NotationCodec";
20
21/// Host-registered notation profile exporting browse metadata and the
22/// Shape-described `music/notation/import` callable.
23pub struct MusicNotationLib;
24
25impl Lib for MusicNotationLib {
26    fn manifest(&self) -> LibManifest {
27        let mut manifest = music_notation_pack().manifest();
28        manifest.exports.push(Export::Function {
29            symbol: notation_import_symbol(),
30            function_id: None,
31        });
32        manifest
33    }
34
35    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
36        music_notation_pack().load(cx, linker)?;
37        linker.function_value(
38            notation_import_symbol(),
39            cx.factory().opaque(Arc::new(NotationImportFunction))?,
40        )?;
41        Ok(())
42    }
43}
44
45/// Installs music shapes and the notation profile into `cx`.
46///
47/// Idempotent: returns early if the lib is already installed.
48pub fn install_music_notation_lib(cx: &mut Cx) -> Result<()> {
49    install_music_shapes_lib(cx)?;
50    if !install_once(cx, &MusicNotationLib)? {
51        return Ok(());
52    }
53    let lib = Symbol::new(MUSIC_NOTATION_LIB_ID);
54    cx.registry_mut().append_export_record(
55        &lib,
56        ExportRecord {
57            kind: ExportKind::named(EXPORT_KIND_NAME),
58            symbol: notation_symbol(),
59            state: ExportState::Resolved {
60                id: RuntimeId::Value,
61            },
62        },
63    )?;
64    Ok(())
65}
66
67/// Symbol of the one notation import callable.
68pub fn notation_import_symbol() -> Symbol {
69    Symbol::qualified("music/notation", "import")
70}
71
72fn notation_symbol() -> Symbol {
73    Symbol::qualified("music", "LilyPondSubsetCodec")
74}
75
76fn notation_value_spec() -> SurfaceValueSpec {
77    SurfaceValueSpec {
78        symbol: notation_symbol(),
79        fields: vec![
80            (
81                Symbol::new("symbol"),
82                SurfaceField::Symbol(notation_symbol()),
83            ),
84            (Symbol::new("layer"), SurfaceField::Str("music".to_owned())),
85            (Symbol::new("kind"), SurfaceField::Str("plugin".to_owned())),
86            (
87                Symbol::new("shape"),
88                SurfaceField::Symbol(Symbol::qualified("music", "NotationCodec")),
89            ),
90            (
91                Symbol::new("dependencies"),
92                SurfaceField::Strs(vec![
93                    "music-core".to_owned(),
94                    "music-shapes".to_owned(),
95                    "pitch-core".to_owned(),
96                ]),
97            ),
98            (Symbol::new("lossless"), SurfaceField::Bool(false)),
99            (Symbol::new("capabilities"), SurfaceField::Symbols(vec![])),
100            (
101                Symbol::new("surface"),
102                SurfaceField::Str("lilypond-subset,musicxml-partwise".to_owned()),
103            ),
104        ],
105    }
106}
107
108fn music_notation_pack() -> SurfacePackLib {
109    SurfacePackLib {
110        spec: SurfacePackSpec {
111            lib_id: Symbol::new(MUSIC_NOTATION_LIB_ID),
112            values: vec![notation_value_spec()],
113        },
114    }
115}
116
117struct NotationImportFunction;
118
119impl Object for NotationImportFunction {
120    fn display(&self, _cx: &mut Cx) -> Result<String> {
121        Ok("#<function music/notation/import>".to_owned())
122    }
123
124    fn as_any(&self) -> &dyn std::any::Any {
125        self
126    }
127}
128
129impl ObjectCompat for NotationImportFunction {
130    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
131        cx.factory().class_stub(
132            sim_kernel::CORE_FUNCTION_CLASS_ID,
133            Symbol::qualified("core", "Function"),
134        )
135    }
136
137    fn as_callable(&self) -> Option<&dyn Callable> {
138        Some(self)
139    }
140}
141
142impl Callable for NotationImportFunction {
143    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
144        let exprs = args
145            .into_vec()
146            .into_iter()
147            .map(|value| value.object().as_expr(cx))
148            .collect::<Result<Vec<_>>>()?;
149        import_call(cx, &exprs, false)
150    }
151
152    fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
153        import_call(cx, args.exprs(), true)
154    }
155
156    fn browse_args_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
157        let keyword = |name| {
158            Arc::new(ExactExprShape::new(Expr::Symbol(Symbol::new(name))))
159                as Arc<dyn sim_shape::Shape>
160        };
161        Ok(Some(shape_value(
162            Symbol::qualified("music/notation/import", "args"),
163            Arc::new(ListShape::tuple(vec![
164                keyword(":format"),
165                Arc::new(AnyShape),
166                keyword(":source"),
167                Arc::new(AnyShape),
168                keyword(":limits"),
169                Arc::new(AnyShape),
170            ])),
171        )))
172    }
173
174    fn browse_result_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
175        Ok(Some(shape_value(
176            Symbol::qualified("music/notation/import", "result"),
177            Arc::new(AnyShape),
178        )))
179    }
180}
181
182fn import_call(cx: &mut Cx, args: &[Expr], evaluate_values: bool) -> Result<Value> {
183    let [format_key, format, source_key, source, limits_key, limits] = args else {
184        return Err(Error::Eval(
185            "music/notation/import expects :format FORMAT :source BYTES :limits MAP".to_owned(),
186        ));
187    };
188    expect_keyword(format_key, "format")?;
189    expect_keyword(source_key, "source")?;
190    expect_keyword(limits_key, "limits")?;
191    if symbolish(format)? != "musicxml-partwise" {
192        return Err(Error::Eval(
193            "music/notation/import supports only 'musicxml-partwise".to_owned(),
194        ));
195    }
196    let source = value_expr(cx, source, evaluate_values)?;
197    let source = match source {
198        Expr::Bytes(bytes) => bytes,
199        Expr::String(text) => text.into_bytes(),
200        other => {
201            return Err(Error::TypeMismatch {
202                expected: "MusicXML bytes or string",
203                found: expr_kind(&other),
204            });
205        }
206    };
207    let limits = value_expr(cx, limits, evaluate_values)?;
208    let limits = parse_limits(&limits)?;
209    let report = import_musicxml_partwise_report(&source, limits)
210        .map_err(|error| Error::Eval(error.to_string()))?;
211    let score_form = encode_score(&report.value).map_err(|error| Error::Eval(error.to_string()))?;
212    let score = MusicScoreDescriptor::read_construct_expr_from_text(&score_form)?;
213    let identities = report
214        .identities
215        .into_iter()
216        .map(|identity| {
217            map(vec![
218                (
219                    "kind",
220                    Expr::Symbol(Symbol::new(match identity.kind {
221                        NotationIdentityKind::Part => "part",
222                        NotationIdentityKind::Event => "event",
223                    })),
224                ),
225                ("path", Expr::String(identity.canonical_path)),
226                ("id", Expr::String(identity.xml_id)),
227            ])
228        })
229        .collect();
230    let losses = report
231        .losses
232        .into_iter()
233        .map(|loss| {
234            map(vec![
235                (
236                    "kind",
237                    Expr::Symbol(Symbol::new(match loss.kind {
238                        NotationLossKind::Clef => "clef",
239                        NotationLossKind::PartName => "part-name",
240                        NotationLossKind::PitchSpelling => "pitch-spelling",
241                        NotationLossKind::DefaultedTempo => "defaulted-tempo",
242                        NotationLossKind::DefaultedTimeSignature => "defaulted-time-signature",
243                        NotationLossKind::Velocity => "velocity",
244                        NotationLossKind::Channel => "channel",
245                    })),
246                ),
247                ("path", loss.canonical_path.map_or(Expr::Nil, Expr::String)),
248                ("detail", Expr::String(loss.detail)),
249            ])
250        })
251        .collect();
252    cx.factory().expr(map(vec![
253        ("format", Expr::Symbol(Symbol::new("musicxml-partwise"))),
254        ("score", score),
255        ("identities", Expr::Vector(identities)),
256        ("losses", Expr::Vector(losses)),
257    ]))
258}
259
260fn value_expr(cx: &mut Cx, expr: &Expr, evaluate: bool) -> Result<Expr> {
261    if evaluate {
262        cx.eval_expr(expr.clone())?.object().as_expr(cx)
263    } else {
264        Ok(expr.clone())
265    }
266}
267
268fn parse_limits(expr: &Expr) -> Result<MusicXmlLimits> {
269    let Expr::Map(entries) = unquote_ref(expr) else {
270        return Err(Error::TypeMismatch {
271            expected: "MusicXML limits map",
272            found: expr_kind(expr),
273        });
274    };
275    let mut limits = MusicXmlLimits::default();
276    for (key, value) in entries {
277        let name = keyword_name(key)?;
278        let parsed = usize_expr(value)?;
279        match name.as_str() {
280            "bytes" => limits.bytes = parsed,
281            "nodes" => limits.nodes = parsed,
282            "depth" => limits.depth = parsed,
283            "text" => limits.text = parsed,
284            "parts" => limits.parts = parsed,
285            "events" => limits.events = parsed,
286            other => {
287                return Err(Error::Eval(format!(
288                    "unknown music/notation/import limit :{other}"
289                )));
290            }
291        }
292    }
293    Ok(limits)
294}
295
296fn expect_keyword(expr: &Expr, expected: &str) -> Result<()> {
297    if keyword_name(expr)? == expected {
298        Ok(())
299    } else {
300        Err(Error::Eval(format!(
301            "music/notation/import expected :{expected}"
302        )))
303    }
304}
305
306fn keyword_name(expr: &Expr) -> Result<String> {
307    match unquote_ref(expr) {
308        Expr::Symbol(symbol) => Ok(symbol
309            .name
310            .strip_prefix(':')
311            .unwrap_or(symbol.name.as_ref())
312            .to_owned()),
313        _ => Err(Error::TypeMismatch {
314            expected: "keyword symbol",
315            found: expr_kind(expr),
316        }),
317    }
318}
319
320fn symbolish(expr: &Expr) -> Result<String> {
321    match unquote_ref(expr) {
322        Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
323        Expr::String(value) => Ok(value.clone()),
324        _ => Err(Error::TypeMismatch {
325            expected: "format symbol",
326            found: expr_kind(expr),
327        }),
328    }
329}
330
331fn usize_expr(expr: &Expr) -> Result<usize> {
332    let text = match unquote_ref(expr) {
333        Expr::Number(number) => number.canonical.as_str(),
334        Expr::String(value) => value.as_str(),
335        _ => {
336            return Err(Error::TypeMismatch {
337                expected: "non-negative integer",
338                found: expr_kind(expr),
339            });
340        }
341    };
342    text.parse()
343        .map_err(|_| Error::Eval(format!("invalid MusicXML limit {text:?}")))
344}
345
346fn unquote_ref(expr: &Expr) -> &Expr {
347    match expr {
348        Expr::Quote { expr, .. } => expr,
349        other => other,
350    }
351}
352
353fn map(entries: Vec<(&str, Expr)>) -> Expr {
354    Expr::Map(
355        entries
356            .into_iter()
357            .map(|(key, value)| (Expr::Symbol(Symbol::new(key)), value))
358            .collect(),
359    )
360}
361
362fn expr_kind(expr: &Expr) -> &'static str {
363    match expr {
364        Expr::Nil => "nil",
365        Expr::Bool(_) => "bool",
366        Expr::Number(_) => "number",
367        Expr::Symbol(_) => "symbol",
368        Expr::Local(_) => "local",
369        Expr::String(_) => "string",
370        Expr::Bytes(_) => "bytes",
371        Expr::List(_) => "list",
372        Expr::Vector(_) => "vector",
373        Expr::Map(_) => "map",
374        Expr::Set(_) => "set",
375        Expr::Call { .. } => "call",
376        Expr::Infix { .. } => "infix",
377        Expr::Prefix { .. } => "prefix",
378        Expr::Postfix { .. } => "postfix",
379        Expr::Block(_) => "block",
380        Expr::Quote { .. } => "quote",
381        Expr::Annotated { .. } => "annotated",
382        Expr::Extension { .. } => "extension",
383    }
384}