Skip to main content

sim_lib_music_core/
arranger_expr.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2
3use crate::arranger::{
4    Arranger, ArrangerPlacement, FilterRef, PitchRemap, PlacementTransform, PlayableRef,
5    StretchPolicy, TracePolicy,
6};
7use crate::arranger_music_expr::{music_from_expr, music_to_expr};
8use crate::{LaneId, LaneTarget, Pitch, PitchClass, Time};
9
10const NS: &str = "music/arranger";
11
12impl Arranger {
13    /// Encodes the arrangement as a tagged `music/arranger` expression map.
14    pub fn to_expr(&self) -> Expr {
15        map(vec![
16            ("tag", tag_expr("arranger")),
17            (
18                "lanes",
19                Expr::Vector(
20                    self.lanes
21                        .iter()
22                        .map(|lane| Expr::String(lane.0.clone()))
23                        .collect(),
24                ),
25            ),
26            (
27                "placements",
28                Expr::Vector(
29                    self.placements
30                        .iter()
31                        .map(ArrangerPlacement::to_expr)
32                        .collect(),
33                ),
34            ),
35        ])
36    }
37
38    /// Decodes an arrangement from a tagged `music/arranger` expression map.
39    pub fn from_expr(expr: &Expr) -> Result<Self> {
40        let entries = expr_map(expr, "arranger")?;
41        expect_tag(entries, "arranger", "arranger")?;
42        let lanes = optional_vector(entries, "lanes")?
43            .iter()
44            .map(|expr| Ok(LaneId::new(expr_string(expr, "lane")?.to_owned())))
45            .collect::<Result<Vec<_>>>()?;
46        let placements = expr_vector(lookup_required(entries, "placements")?, "placements")?
47            .iter()
48            .map(ArrangerPlacement::from_expr)
49            .collect::<Result<Vec<_>>>()?;
50        Self::new(placements, lanes)
51    }
52}
53
54impl ArrangerPlacement {
55    /// Encodes the placement as a tagged `placement` expression map.
56    pub fn to_expr(&self) -> Expr {
57        map(vec![
58            ("tag", tag_expr("placement")),
59            ("id", Expr::Symbol(self.id.clone())),
60            ("playable", self.playable.to_expr()),
61            ("at", time_expr(self.at)),
62            (
63                "duration",
64                self.duration.map(time_expr).unwrap_or(Expr::Nil),
65            ),
66            ("lane", Expr::String(self.lane.0.clone())),
67            (
68                "targets",
69                Expr::Vector(self.targets.iter().map(lane_target_expr).collect()),
70            ),
71            ("stretch", self.stretch.to_expr()),
72            (
73                "transform",
74                Expr::Vector(
75                    self.transform
76                        .iter()
77                        .map(PlacementTransform::to_expr)
78                        .collect(),
79                ),
80            ),
81            ("remap-pitch", self.remap_pitch.to_expr()),
82            (
83                "filter",
84                self.filter
85                    .as_ref()
86                    .map(FilterRef::to_expr)
87                    .unwrap_or(Expr::Nil),
88            ),
89            (
90                "seed",
91                self.seed
92                    .map(|seed| Expr::String(seed.to_string()))
93                    .unwrap_or(Expr::Nil),
94            ),
95            ("trace", Expr::Symbol(self.trace.symbol())),
96        ])
97    }
98
99    /// Decodes a placement from a tagged `placement` expression map.
100    pub fn from_expr(expr: &Expr) -> Result<Self> {
101        let entries = expr_map(expr, "arranger placement")?;
102        expect_tag(entries, "placement", "arranger placement")?;
103        let duration = match lookup_required(entries, "duration")? {
104            Expr::Nil => None,
105            expr => Some(time_from_expr(expr)?),
106        };
107        let placement = Self {
108            id: expr_symbol(lookup_required(entries, "id")?, "placement id")?,
109            playable: PlayableRef::from_expr(lookup_required(entries, "playable")?)?,
110            at: time_from_expr(lookup_required(entries, "at")?)?,
111            duration,
112            lane: LaneId::new(expr_string(lookup_required(entries, "lane")?, "lane")?.to_owned()),
113            targets: expr_vector(lookup_required(entries, "targets")?, "placement targets")?
114                .iter()
115                .map(lane_target_from_expr)
116                .collect::<Result<Vec<_>>>()?,
117            stretch: StretchPolicy::from_expr(lookup_required(entries, "stretch")?)?,
118            transform: expr_vector(lookup_required(entries, "transform")?, "transform")?
119                .iter()
120                .map(PlacementTransform::from_expr)
121                .collect::<Result<Vec<_>>>()?,
122            remap_pitch: PitchRemap::from_expr(lookup_required(entries, "remap-pitch")?)?,
123            filter: match lookup_required(entries, "filter")? {
124                Expr::Nil => None,
125                expr => Some(FilterRef::from_expr(expr)?),
126            },
127            seed: match lookup_required(entries, "seed")? {
128                Expr::Nil => None,
129                expr => Some(expr_u64(expr, "placement seed")?),
130            },
131            trace: TracePolicy::from_expr(lookup_required(entries, "trace")?)?,
132        };
133        placement.validate()?;
134        Ok(placement)
135    }
136}
137
138impl PlayableRef {
139    /// Encodes the reference as a tagged `playable-ref` expression map.
140    pub fn to_expr(&self) -> Expr {
141        match self {
142            Self::Inline(music) => map(vec![
143                ("tag", tag_expr("playable-ref")),
144                ("kind", tag_expr("inline")),
145                ("value", music_to_expr(music)),
146            ]),
147            Self::Symbol(symbol) => map(vec![
148                ("tag", tag_expr("playable-ref")),
149                ("kind", tag_expr("symbol")),
150                ("value", Expr::Symbol(symbol.clone())),
151            ]),
152        }
153    }
154
155    /// Decodes the reference from a tagged `playable-ref` expression map.
156    pub fn from_expr(expr: &Expr) -> Result<Self> {
157        let entries = expr_map(expr, "playable ref")?;
158        expect_tag(entries, "playable-ref", "playable ref")?;
159        match symbol_name(lookup_required(entries, "kind")?, "playable ref kind")? {
160            "inline" => Ok(Self::inline(music_from_expr(lookup_required(
161                entries, "value",
162            )?)?)),
163            "symbol" => Ok(Self::symbol(expr_symbol(
164                lookup_required(entries, "value")?,
165                "playable symbol",
166            )?)),
167            _ => Err(Error::Eval("playable ref kind is invalid".to_owned())),
168        }
169    }
170}
171
172impl FilterRef {
173    /// Encodes the filter as a tagged `filter` expression map.
174    pub fn to_expr(&self) -> Expr {
175        map(vec![
176            ("tag", tag_expr("filter")),
177            ("id", Expr::Symbol(self.id.clone())),
178            (
179                "keep-lanes",
180                Expr::Vector(
181                    self.keep_lanes
182                        .iter()
183                        .map(|lane| Expr::String(lane.0.clone()))
184                        .collect(),
185                ),
186            ),
187        ])
188    }
189
190    /// Decodes the filter from a tagged `filter` expression map.
191    pub fn from_expr(expr: &Expr) -> Result<Self> {
192        let entries = expr_map(expr, "filter")?;
193        expect_tag(entries, "filter", "filter")?;
194        Ok(Self {
195            id: expr_symbol(lookup_required(entries, "id")?, "filter id")?,
196            keep_lanes: expr_vector(lookup_required(entries, "keep-lanes")?, "keep lanes")?
197                .iter()
198                .map(|expr| Ok(LaneId::new(expr_string(expr, "keep lane")?.to_owned())))
199                .collect::<Result<Vec<_>>>()?,
200        })
201    }
202}
203
204impl StretchPolicy {
205    fn to_expr(&self) -> Expr {
206        match self {
207            Self::None => map(vec![
208                ("tag", tag_expr("stretch")),
209                ("kind", tag_expr("none")),
210            ]),
211            Self::TempoRatio(ratio) => map(vec![
212                ("tag", tag_expr("stretch")),
213                ("kind", tag_expr("tempo-ratio")),
214                ("value", time_expr(*ratio)),
215            ]),
216            Self::TimeRatio(ratio) => map(vec![
217                ("tag", tag_expr("stretch")),
218                ("kind", tag_expr("time-ratio")),
219                ("value", time_expr(*ratio)),
220            ]),
221            Self::FitToDuration => map(vec![
222                ("tag", tag_expr("stretch")),
223                ("kind", tag_expr("fit")),
224            ]),
225        }
226    }
227
228    fn from_expr(expr: &Expr) -> Result<Self> {
229        let entries = expr_map(expr, "stretch policy")?;
230        expect_tag(entries, "stretch", "stretch policy")?;
231        match symbol_name(lookup_required(entries, "kind")?, "stretch kind")? {
232            "none" => Ok(Self::None),
233            "tempo-ratio" => Ok(Self::TempoRatio(time_from_expr(lookup_required(
234                entries, "value",
235            )?)?)),
236            "time-ratio" => Ok(Self::TimeRatio(time_from_expr(lookup_required(
237                entries, "value",
238            )?)?)),
239            "fit" => Ok(Self::FitToDuration),
240            _ => Err(Error::Eval("stretch policy kind is invalid".to_owned())),
241        }
242    }
243}
244
245impl PlacementTransform {
246    fn to_expr(&self) -> Expr {
247        match self {
248            Self::TransposeSemitones(semitones) => {
249                value_transform("transpose-semitones", *semitones)
250            }
251            Self::TransposeOctaves(octaves) => value_transform("transpose-octaves", *octaves),
252            Self::InvertAroundPitch(axis) => map(vec![
253                ("tag", tag_expr("transform")),
254                ("kind", tag_expr("invert-pitch")),
255                ("value", pitch_expr(*axis)),
256            ]),
257            Self::InvertAroundPitchClass(axis) => {
258                value_transform("invert-pitch-class", axis.value())
259            }
260            Self::Retrograde => map(vec![
261                ("tag", tag_expr("transform")),
262                ("kind", tag_expr("retrograde")),
263            ]),
264        }
265    }
266
267    fn from_expr(expr: &Expr) -> Result<Self> {
268        let entries = expr_map(expr, "placement transform")?;
269        expect_tag(entries, "transform", "placement transform")?;
270        match symbol_name(lookup_required(entries, "kind")?, "transform kind")? {
271            "transpose-semitones" => Ok(Self::TransposeSemitones(expr_i32(
272                lookup_required(entries, "value")?,
273                "transpose semitones",
274            )?)),
275            "transpose-octaves" => Ok(Self::TransposeOctaves(expr_i16(
276                lookup_required(entries, "value")?,
277                "transpose octaves",
278            )?)),
279            "invert-pitch" => Ok(Self::InvertAroundPitch(pitch_from_expr(lookup_required(
280                entries, "value",
281            )?)?)),
282            "invert-pitch-class" => Ok(Self::InvertAroundPitchClass(
283                PitchClass::new(expr_u8(lookup_required(entries, "value")?, "pitch class")?)
284                    .map_err(|_| Error::Eval("pitch class is invalid".to_owned()))?,
285            )),
286            "retrograde" => Ok(Self::Retrograde),
287            _ => Err(Error::Eval(
288                "placement transform kind is invalid".to_owned(),
289            )),
290        }
291    }
292}
293
294impl PitchRemap {
295    fn to_expr(&self) -> Expr {
296        match self {
297            Self::None => map(vec![
298                ("tag", tag_expr("pitch-remap")),
299                ("kind", tag_expr("none")),
300            ]),
301            Self::Chromatic(semitones) => value_remap("chromatic", *semitones),
302            Self::PitchClass { from, to } => map(vec![
303                ("tag", tag_expr("pitch-remap")),
304                ("kind", tag_expr("pitch-class")),
305                ("from", Expr::String(from.value().to_string())),
306                ("to", Expr::String(to.value().to_string())),
307            ]),
308            Self::DrumKey(items) => map(vec![
309                ("tag", tag_expr("pitch-remap")),
310                ("kind", tag_expr("drum-key")),
311                (
312                    "items",
313                    Expr::Vector(items.iter().map(drum_key_expr).collect()),
314                ),
315            ]),
316            Self::ScaleDegree(symbol) => symbolic_remap_expr("scale-degree", symbol),
317            Self::ChordTone(symbol) => symbolic_remap_expr("chord-tone", symbol),
318            Self::Tuning(symbol) => symbolic_remap_expr("tuning", symbol),
319            Self::Vector(symbol) => symbolic_remap_expr("vector", symbol),
320            Self::Matrix(symbol) => symbolic_remap_expr("matrix", symbol),
321            Self::Callable(symbol) => symbolic_remap_expr("callable", symbol),
322        }
323    }
324
325    fn from_expr(expr: &Expr) -> Result<Self> {
326        let entries = expr_map(expr, "pitch remap")?;
327        expect_tag(entries, "pitch-remap", "pitch remap")?;
328        match symbol_name(lookup_required(entries, "kind")?, "pitch remap kind")? {
329            "none" => Ok(Self::None),
330            "chromatic" => Ok(Self::Chromatic(expr_i32(
331                lookup_required(entries, "value")?,
332                "chromatic remap",
333            )?)),
334            "pitch-class" => Ok(Self::PitchClass {
335                from: PitchClass::new(expr_u8(lookup_required(entries, "from")?, "from")?)
336                    .map_err(|_| Error::Eval("source pitch class is invalid".to_owned()))?,
337                to: PitchClass::new(expr_u8(lookup_required(entries, "to")?, "to")?)
338                    .map_err(|_| Error::Eval("target pitch class is invalid".to_owned()))?,
339            }),
340            "drum-key" => expr_vector(lookup_required(entries, "items")?, "drum key items")?
341                .iter()
342                .map(drum_key_from_expr)
343                .collect::<Result<Vec<_>>>()
344                .map(Self::DrumKey),
345            "scale-degree" => symbolic_remap(entries, Self::ScaleDegree),
346            "chord-tone" => symbolic_remap(entries, Self::ChordTone),
347            "tuning" => symbolic_remap(entries, Self::Tuning),
348            "vector" => symbolic_remap(entries, Self::Vector),
349            "matrix" => symbolic_remap(entries, Self::Matrix),
350            "callable" => symbolic_remap(entries, Self::Callable),
351            _ => Err(Error::Eval("pitch remap kind is invalid".to_owned())),
352        }
353    }
354}
355
356impl TracePolicy {
357    /// Returns the `music/arranger` symbol that names this trace policy.
358    pub fn symbol(self) -> Symbol {
359        match self {
360            Self::Off => tag("trace-off"),
361            Self::Diagnostics => tag("trace-diagnostics"),
362            Self::Full => tag("trace-full"),
363        }
364    }
365
366    fn from_expr(expr: &Expr) -> Result<Self> {
367        match symbol_name(expr, "trace policy")? {
368            "trace-off" => Ok(Self::Off),
369            "trace-diagnostics" => Ok(Self::Diagnostics),
370            "trace-full" => Ok(Self::Full),
371            _ => Err(Error::Eval("trace policy is invalid".to_owned())),
372        }
373    }
374}
375
376impl PartialEq for Arranger {
377    fn eq(&self, other: &Self) -> bool {
378        self.to_expr().canonical_eq(&other.to_expr())
379    }
380}
381
382impl Eq for Arranger {}
383
384impl PartialEq for ArrangerPlacement {
385    fn eq(&self, other: &Self) -> bool {
386        self.to_expr().canonical_eq(&other.to_expr())
387    }
388}
389
390impl Eq for ArrangerPlacement {}
391
392impl PartialEq for PlayableRef {
393    fn eq(&self, other: &Self) -> bool {
394        self.to_expr().canonical_eq(&other.to_expr())
395    }
396}
397
398impl Eq for PlayableRef {}
399
400fn time_expr(time: Time) -> Expr {
401    map(vec![
402        ("numer", Expr::String(time.numer().to_string())),
403        ("denom", Expr::String(time.denom().to_string())),
404    ])
405}
406
407fn time_from_expr(expr: &Expr) -> Result<Time> {
408    let entries = expr_map(expr, "time")?;
409    let denominator = expr_i64(lookup_required(entries, "denom")?, "time denominator")?;
410    if denominator == 0 {
411        return Err(Error::Eval("time denominator cannot be zero".to_owned()));
412    }
413    Ok(Time::new(
414        expr_i64(lookup_required(entries, "numer")?, "time numerator")?,
415        denominator,
416    ))
417}
418
419fn pitch_expr(pitch: Pitch) -> Expr {
420    Expr::String(
421        pitch
422            .to_midi()
423            .map(|midi| format!("midi:{midi}"))
424            .unwrap_or_else(|| format!("semitone:{}", pitch.semitone())),
425    )
426}
427
428fn pitch_from_expr(expr: &Expr) -> Result<Pitch> {
429    let value = expr_string(expr, "pitch")?;
430    if let Some(midi) = value.strip_prefix("midi:") {
431        return Ok(Pitch::from_midi(
432            midi.parse::<u8>()
433                .map_err(|_| Error::Eval("MIDI pitch is invalid".to_owned()))?,
434        ));
435    }
436    if let Some(semitone) = value.strip_prefix("semitone:") {
437        return Ok(Pitch::from_semitone(semitone.parse::<i32>().map_err(
438            |_| Error::Eval("semitone pitch is invalid".to_owned()),
439        )?));
440    }
441    crate::parse_pitch(value).map_err(|_| Error::Eval("pitch is invalid".to_owned()))
442}
443
444fn lane_target_expr(target: &LaneTarget) -> Expr {
445    match target {
446        LaneTarget::Instrument(symbol) => target_expr("instrument", symbol),
447        LaneTarget::Stream(symbol) => target_expr("stream", symbol),
448        LaneTarget::Control(symbol) => target_expr("control", symbol),
449        LaneTarget::None => map(vec![("kind", tag_expr("none"))]),
450    }
451}
452
453fn lane_target_from_expr(expr: &Expr) -> Result<LaneTarget> {
454    let entries = expr_map(expr, "lane target")?;
455    match symbol_name(lookup_required(entries, "kind")?, "lane target kind")? {
456        "instrument" => Ok(LaneTarget::Instrument(expr_symbol(
457            lookup_required(entries, "symbol")?,
458            "target symbol",
459        )?)),
460        "stream" => Ok(LaneTarget::Stream(expr_symbol(
461            lookup_required(entries, "symbol")?,
462            "target symbol",
463        )?)),
464        "control" => Ok(LaneTarget::Control(expr_symbol(
465            lookup_required(entries, "symbol")?,
466            "target symbol",
467        )?)),
468        "none" => Ok(LaneTarget::None),
469        _ => Err(Error::Eval("lane target kind is invalid".to_owned())),
470    }
471}
472
473fn value_transform<T: ToString>(kind: &'static str, value: T) -> Expr {
474    map(vec![
475        ("tag", tag_expr("transform")),
476        ("kind", tag_expr(kind)),
477        ("value", Expr::String(value.to_string())),
478    ])
479}
480
481fn value_remap<T: ToString>(kind: &'static str, value: T) -> Expr {
482    map(vec![
483        ("tag", tag_expr("pitch-remap")),
484        ("kind", tag_expr(kind)),
485        ("value", Expr::String(value.to_string())),
486    ])
487}
488
489fn drum_key_expr((from, to): &(u8, u8)) -> Expr {
490    map(vec![
491        ("from", Expr::String(from.to_string())),
492        ("to", Expr::String(to.to_string())),
493    ])
494}
495
496fn drum_key_from_expr(expr: &Expr) -> Result<(u8, u8)> {
497    let item = expr_map(expr, "drum key item")?;
498    Ok((
499        expr_u8(lookup_required(item, "from")?, "drum key from")?,
500        expr_u8(lookup_required(item, "to")?, "drum key to")?,
501    ))
502}
503
504fn symbolic_remap_expr(kind: &'static str, symbol: &Symbol) -> Expr {
505    map(vec![
506        ("tag", tag_expr("pitch-remap")),
507        ("kind", tag_expr(kind)),
508        ("symbol", Expr::Symbol(symbol.clone())),
509    ])
510}
511
512fn symbolic_remap(
513    entries: &[(Expr, Expr)],
514    build: impl FnOnce(Symbol) -> PitchRemap,
515) -> Result<PitchRemap> {
516    Ok(build(expr_symbol(
517        lookup_required(entries, "symbol")?,
518        "pitch remap symbol",
519    )?))
520}
521
522fn target_expr(kind: &'static str, symbol: &Symbol) -> Expr {
523    map(vec![
524        ("kind", tag_expr(kind)),
525        ("symbol", Expr::Symbol(symbol.clone())),
526    ])
527}
528
529fn map(entries: Vec<(&'static str, Expr)>) -> Expr {
530    Expr::Map(
531        entries
532            .into_iter()
533            .map(|(key, value)| (field(key), value))
534            .collect(),
535    )
536}
537
538fn field(name: &'static str) -> Expr {
539    sim_value::build::qsym(NS, name)
540}
541
542fn tag(name: &'static str) -> Symbol {
543    Symbol::qualified(NS, name)
544}
545
546fn tag_expr(name: &'static str) -> Expr {
547    Expr::Symbol(tag(name))
548}
549
550fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
551    match expr {
552        Expr::Map(entries) => Ok(entries),
553        _ => Err(Error::Eval(format!("{context} must be a map"))),
554    }
555}
556
557fn expr_vector<'a>(expr: &'a Expr, context: &str) -> Result<&'a [Expr]> {
558    match expr {
559        Expr::Vector(items) => Ok(items),
560        _ => Err(Error::Eval(format!("{context} must be a vector"))),
561    }
562}
563
564fn optional_vector<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
565    match lookup(entries, name) {
566        Some(expr) => expr_vector(expr, name),
567        None => Ok(&[]),
568    }
569}
570
571fn lookup_required<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
572    lookup(entries, name).ok_or_else(|| Error::Eval(format!("arranger field is missing: {name}")))
573}
574
575fn lookup<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
576    entries.iter().find_map(|(key, value)| match key {
577        Expr::Symbol(symbol)
578            if symbol.namespace.as_deref() == Some(NS) && symbol.name.as_ref() == name =>
579        {
580            Some(value)
581        }
582        _ => None,
583    })
584}
585
586fn expect_tag(entries: &[(Expr, Expr)], name: &'static str, context: &str) -> Result<()> {
587    match lookup(entries, "tag") {
588        Some(Expr::Symbol(symbol))
589            if symbol.namespace.as_deref() == Some(NS) && symbol.name.as_ref() == name =>
590        {
591            Ok(())
592        }
593        Some(_) => Err(Error::Eval(format!("{context} tag is invalid"))),
594        None => Err(Error::Eval(format!("{context} tag is missing"))),
595    }
596}
597
598fn symbol_name<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
599    match expr {
600        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some(NS) => Ok(symbol.name.as_ref()),
601        _ => Err(Error::Eval(format!("{context} must be an arranger symbol"))),
602    }
603}
604
605fn expr_symbol(expr: &Expr, context: &str) -> Result<Symbol> {
606    match expr {
607        Expr::Symbol(symbol) => Ok(symbol.clone()),
608        _ => Err(Error::Eval(format!("{context} must be a symbol"))),
609    }
610}
611
612fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
613    match expr {
614        Expr::String(value) => Ok(value),
615        _ => Err(Error::Eval(format!("{context} must be a string"))),
616    }
617}
618
619fn expr_i16(expr: &Expr, context: &str) -> Result<i16> {
620    parse_number(expr, context)
621}
622
623fn expr_i32(expr: &Expr, context: &str) -> Result<i32> {
624    parse_number(expr, context)
625}
626
627fn expr_i64(expr: &Expr, context: &str) -> Result<i64> {
628    parse_number(expr, context)
629}
630
631fn expr_u8(expr: &Expr, context: &str) -> Result<u8> {
632    parse_number(expr, context)
633}
634
635fn expr_u64(expr: &Expr, context: &str) -> Result<u64> {
636    parse_number(expr, context)
637}
638
639fn parse_number<T>(expr: &Expr, context: &str) -> Result<T>
640where
641    T: std::str::FromStr,
642{
643    let text = match expr {
644        Expr::String(value) => value.as_str(),
645        Expr::Number(value) => value.canonical.as_str(),
646        _ => return Err(Error::Eval(format!("{context} must be a number"))),
647    };
648    text.parse()
649        .map_err(|_| Error::Eval(format!("{context} is invalid")))
650}