Skip to main content

treble_lang/parser/
mini.rs

1//! Mini-notation parser using nom combinators.
2//!
3//! Parses the content inside double-quoted pattern strings into a
4//! [`MiniNotation`] AST.
5
6use nom::{
7    IResult, Parser,
8    branch::alt,
9    bytes::complete::tag,
10    character::complete::{char, digit1, one_of, space0, space1},
11    combinator::{map, map_res, opt, recognize, value},
12    multi::{many0, separated_list1},
13    sequence::{delimited, preceded},
14};
15
16use crate::ast::mini::*;
17use crate::ast::program::{Accidental, NoteLetter, Ramp};
18
19/// Parse a mini-notation string into a [`MiniNotation`].
20pub fn parse_mini(input: &str) -> Result<MiniNotation, String> {
21    let trimmed = input.trim();
22    if trimmed.is_empty() {
23        return Ok(MiniNotation {
24            sequence: Sequence { steps: vec![] },
25        });
26    }
27    match parse_sequence(trimmed) {
28        Ok(("", mut seq)) => {
29            // Steps record where they sit as the input left after them, which
30            // is all a nom parser over `&str` knows; turn that into offsets from
31            // the start of the notation as written, leading spaces included.
32            let leading = input.len() - input.trim_start().len();
33            rebase_sources(&mut seq, (leading + trimmed.len()) as u32);
34            let notation = MiniNotation { sequence: seq };
35            validate_sequence(&notation.sequence)?;
36            Ok(notation)
37        }
38        Ok((rest, _)) => Err(format!("unexpected trailing input: '{}'", rest)),
39        // A non-recoverable failure is always a real authoring mistake rather
40        // than a wrong turn in `alt`. nom's error carries only an `ErrorKind`,
41        // so the few of them are told apart by which kind they raise.
42        Err(nom::Err::Failure(error)) => Err(match error.code {
43            FAIL_DOUBLE_VELOCITY => "a step takes one velocity, not two".to_string(),
44            FAIL_ACCENT_VELOCITY => {
45                "'X' is already a velocity of 1.0; write 'x:v' to choose one".to_string()
46            }
47            _ => "a group uses ',' for a chord or '|' for a random choice, not both".to_string(),
48        }),
49        Err(e) => Err(format!("mini-notation parse error: {}", e)),
50    }
51}
52
53/// Turn every step's [`Source`], recorded by [`parse_step`] as bytes remaining
54/// after its start and end, into offsets from the start of a notation whose
55/// parsed text ends `end` bytes in.
56fn rebase_sources(sequence: &mut Sequence, end: u32) {
57    for step in &mut sequence.steps {
58        step.source = Source {
59            start: end - step.source.start,
60            end: end - step.source.end,
61        };
62        match &mut step.atom {
63            Atom::Group(group) => {
64                for layer in &mut group.layers {
65                    rebase_sources(layer, end);
66                }
67            }
68            Atom::Alternation(alternation) => rebase_sources(&mut alternation.sequence, end),
69            _ => {}
70        }
71    }
72}
73
74/// Raised by [`parse_group`] when a group mixes `,` and `|`.
75const FAIL_MIXED_SEPARATORS: nom::error::ErrorKind = nom::error::ErrorKind::Verify;
76/// Raised by [`parse_step`] when one step carries two `:v` suffixes.
77const FAIL_DOUBLE_VELOCITY: nom::error::ErrorKind = nom::error::ErrorKind::Many1;
78/// Raised by [`parse_step`] for `X:v`, which asks for two velocities at once.
79const FAIL_ACCENT_VELOCITY: nom::error::ErrorKind = nom::error::ErrorKind::ManyMN;
80
81/// Check the invariants the grammar itself cannot express.
82///
83/// Runs after a successful parse so the messages can name the offending value,
84/// rather than surfacing a generic nom error.
85fn validate_sequence(sequence: &Sequence) -> Result<(), String> {
86    for step in sequence.steps.iter() {
87        for modifier in step.modifiers.iter() {
88            validate_modifier(modifier)?;
89        }
90        validate_velocity(step)?;
91        match &step.atom {
92            Atom::Group(group) => {
93                for layer in group.layers.iter() {
94                    // A chord layer is a fixed set of notes; a generated walk
95                    // has no single note to contribute to one.
96                    if group.mode == GroupMode::Chord
97                        && layer
98                            .steps
99                            .iter()
100                            .any(|step| matches!(step.atom, Atom::Solo(_)))
101                    {
102                        return Err(
103                            "solo(..) can't be a chord layer; put it in its own step".to_string()
104                        );
105                    }
106                    validate_sequence(layer)?;
107                }
108            }
109            Atom::Alternation(alternation) => validate_sequence(&alternation.sequence)?,
110            Atom::Solo(solo) => {
111                if solo.high <= solo.low {
112                    return Err(format!(
113                        "solo({}..{}) needs at least two degrees to walk between",
114                        solo.low, solo.high
115                    ));
116                }
117                if solo.steps.values().contains(&0) {
118                    return Err("solo needs at least one step per cycle".to_string());
119                }
120            }
121            _ => {}
122        }
123    }
124    Ok(())
125}
126
127/// `:v` (and the `X` it desugars from) must be a playable strike, on something
128/// that actually strikes.
129fn validate_velocity(step: &Step) -> Result<(), String> {
130    let Some(velocity) = &step.velocity else {
131        return Ok(());
132    };
133    match step.atom {
134        // Silence has no strike, and a hold sustains whatever it extends, so
135        // neither has a velocity of its own to set. Ignoring the suffix would
136        // hide the misunderstanding instead of correcting it.
137        Atom::Rest => {
138            return Err(
139                "a rest can't carry a velocity: '~' is silence, so ':v' has nothing to strike"
140                    .to_string(),
141            );
142        }
143        Atom::Hold => {
144            return Err("a hold can't carry a velocity: '_' sustains the event before it, which was already struck".to_string());
145        }
146        _ => {}
147    }
148    for value in velocity.values() {
149        if !value.is_finite() || !(0.0..=1.0).contains(&value) {
150            return Err(format!(
151                "velocity {value} is outside the supported range 0.0-1.0"
152            ));
153        }
154    }
155    Ok(())
156}
157
158fn validate_modifier(modifier: &Modifier) -> Result<(), String> {
159    let positive = |value: u32, sigil: &str| {
160        if value == 0 {
161            Err(format!(
162                "'{sigil}0' is not a count; use at least '{sigil}1'"
163            ))
164        } else {
165            Ok(())
166        }
167    };
168    match modifier {
169        Modifier::Repeat(count) => {
170            // Every value a ramp passes through has to be legal, not just the
171            // one it starts on.
172            for value in count.values() {
173                positive(value, "*")?;
174            }
175            Ok(())
176        }
177        Modifier::Replicate(0) => Err("'!0' is not a count; use at least '!1'".to_string()),
178        Modifier::Slow(0) => Err("'/0' is not a count; use at least '/1'".to_string()),
179        Modifier::Weight(0) => Err("'@0' is not a weight; use at least '@1'".to_string()),
180        Modifier::Euclidean(_, positions, _) => {
181            if positions.values().contains(&0) {
182                return Err("a Euclidean rhythm needs at least one step".to_string());
183            }
184            Ok(())
185        }
186        Modifier::Drop(Some(probability)) => {
187            for value in probability.values() {
188                if !value.is_finite() || !(0.0..=1.0).contains(&value) {
189                    return Err(format!(
190                        "drop probability {value} is outside the supported range 0.0-1.0"
191                    ));
192                }
193            }
194            Ok(())
195        }
196        _ => Ok(()),
197    }
198}
199
200// --- Sequence ---
201
202fn parse_sequence(input: &str) -> IResult<&str, Sequence> {
203    let (input, _) = space0(input)?;
204    let (input, steps) = separated_list1(space1, parse_step).parse(input)?;
205    let (input, _) = space0(input)?;
206    Ok((input, Sequence { steps }))
207}
208
209// --- Step = Atom { Modifier } ---
210
211fn parse_step(input: &str) -> IResult<&str, Step> {
212    let remaining_at_start = input.len() as u32;
213    // `X` is stored as `x:1.0` rather than as an atom of its own, so a consumer
214    // only has to implement velocity once.
215    let (mut input, accented) = match parse_accent(input) {
216        Ok((rest, ())) => (rest, true),
217        Err(_) => (input, false),
218    };
219    let (rest, atom) = if accented {
220        (input, Atom::Trigger)
221    } else {
222        parse_atom(input)?
223    };
224    input = rest;
225    let mut modifiers = Vec::new();
226    let mut velocity = accented.then(|| Ramp::fixed(1.0));
227    // Modifiers and the velocity suffix may be written in either order, and a
228    // velocity means the same thing wherever it sits (see [`Step`]), so both
229    // are taken in one pass. Every one of them consumes at least its sigil, so
230    // the loop always advances.
231    loop {
232        if let Ok((rest, next)) = parse_velocity(input) {
233            if velocity.is_some() {
234                let code = if accented {
235                    FAIL_ACCENT_VELOCITY
236                } else {
237                    FAIL_DOUBLE_VELOCITY
238                };
239                return Err(nom::Err::Failure(nom::error::Error::new(input, code)));
240            }
241            velocity = Some(next);
242            input = rest;
243            continue;
244        }
245        match parse_modifier(input) {
246            Ok((rest, modifier)) => {
247                modifiers.push(modifier);
248                input = rest;
249            }
250            Err(_) => break,
251        }
252    }
253    Ok((
254        input,
255        Step {
256            atom,
257            modifiers,
258            velocity,
259            // Bytes remaining, not offsets: see `rebase_sources`.
260            source: Source {
261                start: remaining_at_start,
262                end: input.len() as u32,
263            },
264        },
265    ))
266}
267
268/// `X` — an accented trigger. Unambiguous because note letters are `a`–`g`.
269fn parse_accent(input: &str) -> IResult<&str, ()> {
270    let (input, _) = char('X').parse(input)?;
271    if input
272        .chars()
273        .next()
274        .map(|c| c.is_ascii_alphanumeric())
275        .unwrap_or(false)
276    {
277        return Err(nom::Err::Error(nom::error::Error::new(
278            input,
279            nom::error::ErrorKind::Char,
280        )));
281    }
282    Ok((input, ()))
283}
284
285/// `:0.6`, `:0.3..0.9` — an explicit velocity for this step.
286fn parse_velocity(input: &str) -> IResult<&str, Ramp<f64>> {
287    preceded(char(':'), parse_ramp_f64).parse(input)
288}
289
290// --- Atom ---
291
292fn parse_atom(input: &str) -> IResult<&str, Atom> {
293    alt((
294        parse_solo,
295        parse_group,
296        parse_alternation,
297        parse_note_atom,
298        parse_degree_atom,
299        parse_trigger,
300        parse_rest,
301        parse_hold,
302    ))
303    .parse(input)
304}
305
306fn parse_note_atom(input: &str) -> IResult<&str, Atom> {
307    map(parse_note, Atom::Note).parse(input)
308}
309
310fn parse_note(input: &str) -> IResult<&str, Note> {
311    let (input, letter) = parse_note_letter(input)?;
312    let (input, accidental) = parse_accidental(input, letter)?;
313    let (input, octave) = parse_octave(input)?;
314    Ok((
315        input,
316        Note {
317            letter,
318            accidental,
319            octave,
320        },
321    ))
322}
323
324fn parse_note_letter(input: &str) -> IResult<&str, NoteLetter> {
325    alt((
326        value(NoteLetter::C, char('c')),
327        value(NoteLetter::D, char('d')),
328        value(NoteLetter::E, char('e')),
329        value(NoteLetter::F, char('f')),
330        value(NoteLetter::G, char('g')),
331        value(NoteLetter::A, char('a')),
332        value(NoteLetter::B, char('b')),
333    ))
334    .parse(input)
335}
336
337/// Parse accidental after a note letter.
338///
339/// Context-sensitive for the note B: `bb3` = B-flat 3, `b4` = B natural 4.
340fn parse_accidental(input: &str, letter: NoteLetter) -> IResult<&str, Accidental> {
341    if letter == NoteLetter::B {
342        // After 'b': '#'/'##' for sharp
343        if let Ok((rest, _)) = tag::<&str, &str, nom::error::Error<&str>>("##").parse(input) {
344            return Ok((rest, Accidental::DoubleSharp));
345        }
346        if let Ok((rest, _)) = char::<&str, nom::error::Error<&str>>('#').parse(input) {
347            return Ok((rest, Accidental::Sharp));
348        }
349        // 'b' followed by digit = flat
350        if let Some(after_b) = input.strip_prefix('b')
351            && after_b.starts_with(|c: char| c.is_ascii_digit())
352        {
353            return Ok((after_b, Accidental::Flat));
354        }
355        Ok((input, Accidental::Natural))
356    } else {
357        // Non-B note
358        let result: IResult<&str, Accidental> = alt((
359            value(Accidental::DoubleSharp, tag("##")),
360            value(Accidental::Sharp, char('#')),
361            value(Accidental::DoubleFlat, tag("bb")),
362            value(Accidental::Flat, char('b')),
363        ))
364        .parse(input);
365        match result {
366            Ok(r) => Ok(r),
367            Err(_) => Ok((input, Accidental::Natural)),
368        }
369    }
370}
371
372fn parse_octave(input: &str) -> IResult<&str, u8> {
373    map_res(one_of("0123456789"), |c: char| {
374        c.to_digit(10).map(|d| d as u8).ok_or("invalid octave")
375    })
376    .parse(input)
377}
378
379fn parse_degree_atom(input: &str) -> IResult<&str, Atom> {
380    // Negative degrees: -N
381    if input.starts_with('-') {
382        let (input, _) = char('-').parse(input)?;
383        let (input, digits) = digit1(input)?;
384        let n: i32 = digits.parse().map_err(|_| {
385            nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
386        })?;
387        return Ok((input, Atom::Degree(-n)));
388    }
389    // Positive degrees: just digits
390    let (input, digits) = digit1(input)?;
391    let n: i32 = digits.parse().map_err(|_| {
392        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
393    })?;
394    Ok((input, Atom::Degree(n)))
395}
396
397fn parse_trigger(input: &str) -> IResult<&str, Atom> {
398    let (input, _) = char('x').parse(input)?;
399    // Ensure not followed by an alphanumeric char
400    if input
401        .chars()
402        .next()
403        .map(|c| c.is_ascii_alphanumeric())
404        .unwrap_or(false)
405    {
406        return Err(nom::Err::Error(nom::error::Error::new(
407            input,
408            nom::error::ErrorKind::Char,
409        )));
410    }
411    Ok((input, Atom::Trigger))
412}
413
414fn parse_rest(input: &str) -> IResult<&str, Atom> {
415    value(Atom::Rest, char('~')).parse(input)
416}
417
418fn parse_hold(input: &str) -> IResult<&str, Atom> {
419    value(Atom::Hold, char('_')).parse(input)
420}
421
422// --- Solo: solo(low..high, steps) ---
423
424fn parse_solo(input: &str) -> IResult<&str, Atom> {
425    let (input, _) = tag("solo(").parse(input)?;
426    let (input, _) = space0(input)?;
427    let (input, low) = parse_i32(input)?;
428    let (input, _) = tag("..").parse(input)?;
429    let (input, high) = parse_i32(input)?;
430    let (input, _) = delimited(space0, char(','), space0).parse(input)?;
431    let (input, steps) = parse_ramp_u32(input)?;
432    let (input, _) = space0(input)?;
433    let (input, _) = char(')').parse(input)?;
434    Ok((input, Atom::Solo(Solo { low, high, steps })))
435}
436
437fn parse_i32(input: &str) -> IResult<&str, i32> {
438    let (rest, negative) = opt(char('-')).parse(input)?;
439    let (rest, digits) = digit1(rest)?;
440    let value: i32 = digits
441        .parse()
442        .map_err(|_| nom::Err::Error(nom::error::Error::new(rest, nom::error::ErrorKind::Digit)))?;
443    Ok((rest, if negative.is_some() { -value } else { value }))
444}
445
446// --- Group: [ sequence { ("," | "|") sequence } ] ---
447
448fn parse_group(input: &str) -> IResult<&str, Atom> {
449    let (input, _) = char('[').parse(input)?;
450    // `parse_sequence` eats the surrounding whitespace of every layer.
451    let (mut input, first) = parse_sequence(input)?;
452    let mut layers = vec![first];
453    let mut chord = false;
454    let mut random = false;
455    while let Ok((rest, separator)) =
456        one_of::<&str, &str, nom::error::Error<&str>>(",|").parse(input)
457    {
458        let (rest, layer) = parse_sequence(rest)?;
459        match separator {
460            ',' => chord = true,
461            _ => random = true,
462        }
463        layers.push(layer);
464        input = rest;
465    }
466    let (input, _) = char(']').parse(input)?;
467    if chord && random {
468        // Non-recoverable on purpose: `alt` in `parse_atom` must not retry this
469        // as some other atom, and `parse_mini` turns it into a real message.
470        return Err(nom::Err::Failure(nom::error::Error::new(
471            input,
472            FAIL_MIXED_SEPARATORS,
473        )));
474    }
475    let mode = match (chord, random) {
476        (true, _) => GroupMode::Chord,
477        (_, true) => GroupMode::Random,
478        _ => GroupMode::Subdivide,
479    };
480    Ok((input, Atom::Group(Group { mode, layers })))
481}
482
483// --- Alternation: < sequence > ---
484
485fn parse_alternation(input: &str) -> IResult<&str, Atom> {
486    let (input, _) = char('<').parse(input)?;
487    let (input, _) = space0(input)?;
488    let (input, sequence) = parse_sequence(input)?;
489    let (input, _) = space0(input)?;
490    let (input, _) = char('>').parse(input)?;
491    Ok((input, Atom::Alternation(Alternation { sequence })))
492}
493
494// --- Modifiers ---
495
496fn parse_modifier(input: &str) -> IResult<&str, Modifier> {
497    alt((
498        parse_repeat,
499        parse_slow_mod,
500        parse_replicate,
501        parse_euclidean,
502        parse_drop,
503        parse_weight,
504    ))
505    .parse(input)
506}
507
508fn parse_repeat(input: &str) -> IResult<&str, Modifier> {
509    let (input, _) = char('*').parse(input)?;
510    let (input, n) = parse_ramp_u32(input)?;
511    Ok((input, Modifier::Repeat(n)))
512}
513
514fn parse_slow_mod(input: &str) -> IResult<&str, Modifier> {
515    let (input, _) = char('/').parse(input)?;
516    let (input, n) = parse_u32(input)?;
517    Ok((input, Modifier::Slow(n)))
518}
519
520fn parse_replicate(input: &str) -> IResult<&str, Modifier> {
521    let (input, _) = char('!').parse(input)?;
522    let (input, n) = parse_u32(input)?;
523    Ok((input, Modifier::Replicate(n)))
524}
525
526fn parse_euclidean(input: &str) -> IResult<&str, Modifier> {
527    let (input, _) = char('(').parse(input)?;
528    let (input, _) = space0(input)?;
529    let (input, beats) = parse_ramp_u32(input)?;
530    let (input, _) = delimited(space0, char(','), space0).parse(input)?;
531    let (input, steps) = parse_ramp_u32(input)?;
532    let (input, offset) =
533        opt(preceded(delimited(space0, char(','), space0), parse_u32)).parse(input)?;
534    let (input, _) = space0(input)?;
535    let (input, _) = char(')').parse(input)?;
536    Ok((input, Modifier::Euclidean(beats, steps, offset)))
537}
538
539fn parse_drop(input: &str) -> IResult<&str, Modifier> {
540    let (input, _) = char('?').parse(input)?;
541    // No space is allowed before the probability, so `x? 0.5` still reads as a
542    // bare drop followed by a separate step.
543    let (input, probability) = opt(parse_ramp_f64).parse(input)?;
544    Ok((input, Modifier::Drop(probability)))
545}
546
547fn parse_weight(input: &str) -> IResult<&str, Modifier> {
548    let (input, _) = char('@').parse(input)?;
549    let (input, n) = parse_u32(input)?;
550    Ok((input, Modifier::Weight(n)))
551}
552
553fn parse_u32(input: &str) -> IResult<&str, u32> {
554    map_res(digit1, |s: &str| s.parse::<u32>()).parse(input)
555}
556
557/// `4`, `4..16` or `2>4>8>16` — a value that may travel.
558///
559/// `digit1` followed by an optional fraction means `4..16` backtracks cleanly:
560/// the fraction needs a digit after its dot, and the second dot is not one.
561/// A `>` chain backtracks too, so the closing `>` of `<x*4>` is left alone.
562fn parse_ramp_u32(input: &str) -> IResult<&str, Ramp<u32>> {
563    if let Some(result) = parse_r_form(input, |part| {
564        part.parse::<u32>().map_err(|error| error.to_string())
565    }) {
566        return result;
567    }
568    let (input, first) = parse_u32(input)?;
569    if let Ok((rest, to)) = preceded(tag(".."), parse_u32).parse(input) {
570        return Ok((rest, Ramp::Sweep { from: first, to }));
571    }
572    let (input, steps) = many0(preceded(tag(">"), parse_u32)).parse(input)?;
573    Ok((input, Ramp::steps(first, steps)))
574}
575
576fn parse_ramp_f64(input: &str) -> IResult<&str, Ramp<f64>> {
577    if let Some(result) = parse_r_form(input, |part| {
578        part.parse::<f64>().map_err(|error| error.to_string())
579    }) {
580        return result;
581    }
582    let (input, first) = parse_f64(input)?;
583    if let Ok((rest, to)) = preceded(tag(".."), parse_f64).parse(input) {
584        return Ok((rest, Ramp::Sweep { from: first, to }));
585    }
586    let (input, steps) = many0(preceded(tag(">"), parse_f64)).parse(input)?;
587    Ok((input, Ramp::steps(first, steps)))
588}
589
590/// `r(...)` wherever a ramp value is legal — `x(r(2,16,8),4)` grows a
591/// Euclidean count over its own eight-beat window. Written without spaces, so
592/// the closing parenthesis is unambiguous; the interpretation is shared with
593/// the transform parser.
594fn parse_r_form<T: Copy>(
595    input: &str,
596    parse: impl Fn(&str) -> Result<T, String>,
597) -> Option<IResult<&str, Ramp<T>>> {
598    let inner = input.strip_prefix("r(")?;
599    let close = inner.find(')')?;
600    let text = format!("r({})", &inner[..close]);
601    Some(
602        match crate::parser::lines::ramp_from_text(&text, "r", &parse) {
603            Ok(ramp) => Ok((&inner[close + 1..], ramp)),
604            Err(_) => Err(nom::Err::Error(nom::error::Error::new(
605                input,
606                nom::error::ErrorKind::Verify,
607            ))),
608        },
609    )
610}
611
612fn parse_f64(input: &str) -> IResult<&str, f64> {
613    map_res(recognize((digit1, opt((char('.'), digit1)))), |s: &str| {
614        s.parse::<f64>()
615    })
616    .parse(input)
617}
618
619// ============================================================
620// Tests
621// ============================================================
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::ast::program::RampCurve;
627    use pretty_assertions::assert_eq;
628
629    // --- Sources ---
630
631    /// What each step's recorded range covers in the text it came from.
632    fn written<'a>(text: &'a str, step: &Step) -> &'a str {
633        &text[step.source.start as usize..step.source.end as usize]
634    }
635
636    #[test]
637    fn steps_record_where_they_were_written() {
638        let text = "  c4 [e4 g4:0.5]*2 <x X> 0:0.6(3,8)  ";
639        let m = parse_mini(text).unwrap();
640        let steps = &m.sequence.steps;
641        assert_eq!(written(text, &steps[0]), "c4");
642        assert_eq!(written(text, &steps[1]), "[e4 g4:0.5]*2");
643        assert_eq!(written(text, &steps[2]), "<x X>");
644        assert_eq!(written(text, &steps[3]), "0:0.6(3,8)");
645        let Atom::Group(group) = &steps[1].atom else {
646            panic!("expected a group")
647        };
648        assert_eq!(written(text, &group.layers[0].steps[1]), "g4:0.5");
649        let Atom::Alternation(alternation) = &steps[2].atom else {
650            panic!("expected an alternation")
651        };
652        assert_eq!(written(text, &alternation.sequence.steps[1]), "X");
653    }
654
655    #[test]
656    fn sources_do_not_change_what_a_step_means() {
657        assert_eq!(parse_mini("c4  e4").unwrap(), parse_mini("c4 e4").unwrap());
658    }
659
660    // --- Helper to build notes quickly ---
661    fn n(letter: NoteLetter, acc: Accidental, oct: u8) -> Atom {
662        Atom::Note(Note {
663            letter,
664            accidental: acc,
665            octave: oct,
666        })
667    }
668
669    fn nat(letter: NoteLetter, oct: u8) -> Atom {
670        n(letter, Accidental::Natural, oct)
671    }
672
673    // ---- Notes ----
674
675    #[test]
676    fn r_form_ranges_fit_inside_mini_notation() {
677        let m = parse_mini("x(r(2,16,8),4)").unwrap();
678        let modifiers = &m.sequence.steps[0].modifiers;
679        assert_eq!(
680            modifiers,
681            &vec![Modifier::Euclidean(
682                Ramp::Timed {
683                    ramp: Box::new(Ramp::Sweep { from: 2, to: 16 }),
684                    span_divisions: 8.0,
685                    curve: RampCurve::Linear,
686                },
687                Ramp::fixed(4),
688                None
689            )]
690        );
691
692        let m = parse_mini("x*r(2,8,4,osc)").unwrap();
693        assert_eq!(
694            m.sequence.steps[0].modifiers,
695            vec![Modifier::Repeat(Ramp::Timed {
696                ramp: Box::new(Ramp::Sweep { from: 2, to: 8 }),
697                span_divisions: 4.0,
698                curve: RampCurve::Oscillate,
699            })]
700        );
701
702        let m = parse_mini("x:r(0.3,1.0,2)").unwrap();
703        assert!(m.sequence.steps[0].velocity.as_ref().unwrap().travels());
704    }
705
706    #[test]
707    fn test_note_c4() {
708        let m = parse_mini("c4").unwrap();
709        assert_eq!(m.sequence.steps.len(), 1);
710        assert_eq!(m.sequence.steps[0].atom, nat(NoteLetter::C, 4));
711    }
712
713    #[test]
714    fn test_note_eb3() {
715        let m = parse_mini("eb3").unwrap();
716        assert_eq!(
717            m.sequence.steps[0].atom,
718            n(NoteLetter::E, Accidental::Flat, 3)
719        );
720    }
721
722    #[test]
723    fn test_note_f_sharp_5() {
724        let m = parse_mini("f#5").unwrap();
725        assert_eq!(
726            m.sequence.steps[0].atom,
727            n(NoteLetter::F, Accidental::Sharp, 5)
728        );
729    }
730
731    #[test]
732    fn test_note_bb3_is_b_flat() {
733        let m = parse_mini("bb3").unwrap();
734        assert_eq!(
735            m.sequence.steps[0].atom,
736            n(NoteLetter::B, Accidental::Flat, 3)
737        );
738    }
739
740    #[test]
741    fn test_note_b4_natural() {
742        let m = parse_mini("b4").unwrap();
743        assert_eq!(
744            m.sequence.steps[0].atom,
745            n(NoteLetter::B, Accidental::Natural, 4)
746        );
747    }
748
749    #[test]
750    fn test_note_b_sharp() {
751        let m = parse_mini("b#4").unwrap();
752        assert_eq!(
753            m.sequence.steps[0].atom,
754            n(NoteLetter::B, Accidental::Sharp, 4)
755        );
756    }
757
758    // ---- Sequences ----
759
760    #[test]
761    fn test_sequence_three_notes() {
762        let m = parse_mini("c4 e4 g4").unwrap();
763        assert_eq!(m.sequence.steps.len(), 3);
764        assert_eq!(m.sequence.steps[0].atom, nat(NoteLetter::C, 4));
765        assert_eq!(m.sequence.steps[1].atom, nat(NoteLetter::E, 4));
766        assert_eq!(m.sequence.steps[2].atom, nat(NoteLetter::G, 4));
767    }
768
769    #[test]
770    fn test_rest_and_hold() {
771        let m = parse_mini("c4 ~ _ e4").unwrap();
772        assert_eq!(m.sequence.steps.len(), 4);
773        assert_eq!(m.sequence.steps[0].atom, nat(NoteLetter::C, 4));
774        assert_eq!(m.sequence.steps[1].atom, Atom::Rest);
775        assert_eq!(m.sequence.steps[2].atom, Atom::Hold);
776        assert_eq!(m.sequence.steps[3].atom, nat(NoteLetter::E, 4));
777    }
778
779    #[test]
780    fn test_trigger() {
781        let m = parse_mini("x ~ x ~").unwrap();
782        assert_eq!(m.sequence.steps.len(), 4);
783        assert_eq!(m.sequence.steps[0].atom, Atom::Trigger);
784        assert_eq!(m.sequence.steps[1].atom, Atom::Rest);
785        assert_eq!(m.sequence.steps[2].atom, Atom::Trigger);
786        assert_eq!(m.sequence.steps[3].atom, Atom::Rest);
787    }
788
789    #[test]
790    fn test_scale_degrees() {
791        let m = parse_mini("0 2 4 6").unwrap();
792        assert_eq!(m.sequence.steps.len(), 4);
793        assert_eq!(m.sequence.steps[0].atom, Atom::Degree(0));
794        assert_eq!(m.sequence.steps[1].atom, Atom::Degree(2));
795        assert_eq!(m.sequence.steps[2].atom, Atom::Degree(4));
796        assert_eq!(m.sequence.steps[3].atom, Atom::Degree(6));
797    }
798
799    // ---- Modifiers ----
800
801    #[test]
802    fn test_repeat() {
803        let m = parse_mini("x*4").unwrap();
804        assert_eq!(m.sequence.steps[0].atom, Atom::Trigger);
805        assert_eq!(
806            m.sequence.steps[0].modifiers,
807            vec![Modifier::Repeat(Ramp::fixed(4))]
808        );
809    }
810
811    #[test]
812    fn test_slow_modifier() {
813        let m = parse_mini("[c4 e4 g4]/2").unwrap();
814        assert!(matches!(m.sequence.steps[0].atom, Atom::Group(_)));
815        assert_eq!(m.sequence.steps[0].modifiers, vec![Modifier::Slow(2)]);
816    }
817
818    #[test]
819    fn test_replicate() {
820        let m = parse_mini("c4!3").unwrap();
821        assert_eq!(m.sequence.steps[0].modifiers, vec![Modifier::Replicate(3)]);
822    }
823
824    #[test]
825    fn test_euclidean() {
826        let m = parse_mini("c4(3,8)").unwrap();
827        assert_eq!(
828            m.sequence.steps[0].modifiers,
829            vec![Modifier::Euclidean(Ramp::fixed(3), Ramp::fixed(8), None)]
830        );
831    }
832
833    #[test]
834    fn test_euclidean_with_offset() {
835        let m = parse_mini("x(5,8,2)").unwrap();
836        assert_eq!(
837            m.sequence.steps[0].modifiers,
838            vec![Modifier::Euclidean(Ramp::fixed(5), Ramp::fixed(8), Some(2))]
839        );
840    }
841
842    #[test]
843    fn test_drop() {
844        let m = parse_mini("c4?").unwrap();
845        assert_eq!(m.sequence.steps[0].modifiers, vec![Modifier::Drop(None)]);
846    }
847
848    #[test]
849    fn test_weight() {
850        let m = parse_mini("c4@3 e4").unwrap();
851        assert_eq!(m.sequence.steps[0].modifiers, vec![Modifier::Weight(3)]);
852        assert!(m.sequence.steps[1].modifiers.is_empty());
853    }
854
855    // ---- Groups and chords ----
856
857    #[test]
858    fn test_group() {
859        let m = parse_mini("[c4 e4] g4").unwrap();
860        assert_eq!(m.sequence.steps.len(), 2);
861        if let Atom::Group(ref g) = m.sequence.steps[0].atom {
862            assert_eq!(g.mode, GroupMode::Subdivide);
863            assert_eq!(g.layers.len(), 1);
864            assert_eq!(g.layers[0].steps.len(), 2);
865        } else {
866            panic!("expected group");
867        }
868        assert_eq!(m.sequence.steps[1].atom, nat(NoteLetter::G, 4));
869    }
870
871    #[test]
872    fn test_chord() {
873        let m = parse_mini("[c3,e3,g3]").unwrap();
874        assert_eq!(m.sequence.steps.len(), 1);
875        if let Atom::Group(ref g) = m.sequence.steps[0].atom {
876            assert_eq!(g.mode, GroupMode::Chord);
877            assert_eq!(g.layers.len(), 3);
878            assert_eq!(g.layers[0].steps[0].atom, nat(NoteLetter::C, 3));
879            assert_eq!(g.layers[1].steps[0].atom, nat(NoteLetter::E, 3));
880            assert_eq!(g.layers[2].steps[0].atom, nat(NoteLetter::G, 3));
881        } else {
882            panic!("expected group/chord");
883        }
884    }
885
886    #[test]
887    fn test_nested_groups() {
888        let m = parse_mini("[c4 [e4 g4]] b4").unwrap();
889        assert_eq!(m.sequence.steps.len(), 2);
890        if let Atom::Group(ref outer) = m.sequence.steps[0].atom {
891            assert_eq!(outer.layers[0].steps.len(), 2);
892            assert!(matches!(outer.layers[0].steps[1].atom, Atom::Group(_)));
893        } else {
894            panic!("expected nested group");
895        }
896    }
897
898    // ---- Alternation ----
899
900    #[test]
901    fn test_alternation() {
902        let m = parse_mini("<c4 e4 g4>").unwrap();
903        assert_eq!(m.sequence.steps.len(), 1);
904        if let Atom::Alternation(ref a) = m.sequence.steps[0].atom {
905            assert_eq!(a.sequence.steps.len(), 3);
906        } else {
907            panic!("expected alternation");
908        }
909    }
910
911    #[test]
912    fn test_alternation_in_sequence() {
913        let m = parse_mini("c4 <e4 g4> c5").unwrap();
914        assert_eq!(m.sequence.steps.len(), 3);
915        assert_eq!(m.sequence.steps[0].atom, nat(NoteLetter::C, 4));
916        assert!(matches!(m.sequence.steps[1].atom, Atom::Alternation(_)));
917        assert_eq!(m.sequence.steps[2].atom, nat(NoteLetter::C, 5));
918    }
919
920    // ---- Empty ----
921
922    #[test]
923    fn test_empty_notation() {
924        let m = parse_mini("").unwrap();
925        assert_eq!(m.sequence.steps.len(), 0);
926    }
927
928    #[test]
929    fn test_whitespace_only() {
930        let m = parse_mini("   ").unwrap();
931        assert_eq!(m.sequence.steps.len(), 0);
932    }
933
934    // ---- Complex patterns ----
935
936    #[test]
937    fn test_drum_pattern() {
938        let m = parse_mini("x ~ [x x] ~").unwrap();
939        assert_eq!(m.sequence.steps.len(), 4);
940        assert_eq!(m.sequence.steps[0].atom, Atom::Trigger);
941        assert_eq!(m.sequence.steps[1].atom, Atom::Rest);
942        assert!(matches!(m.sequence.steps[2].atom, Atom::Group(_)));
943        assert_eq!(m.sequence.steps[3].atom, Atom::Rest);
944    }
945
946    #[test]
947    fn test_chord_sequence() {
948        let m = parse_mini("[c3,e3,g3] ~ [f3,a3,c4] ~").unwrap();
949        assert_eq!(m.sequence.steps.len(), 4);
950        if let Atom::Group(ref g) = m.sequence.steps[0].atom {
951            assert_eq!(g.layers.len(), 3);
952        } else {
953            panic!("expected chord");
954        }
955    }
956
957    #[test]
958    fn test_repeat_group() {
959        let m = parse_mini("[c4 e4]*3").unwrap();
960        assert_eq!(m.sequence.steps.len(), 1);
961        assert!(matches!(m.sequence.steps[0].atom, Atom::Group(_)));
962        assert_eq!(
963            m.sequence.steps[0].modifiers,
964            vec![Modifier::Repeat(Ramp::fixed(3))]
965        );
966    }
967
968    // ---- Stacked modifiers ----
969
970    #[test]
971    fn test_stacked_repeat_and_drop() {
972        let m = parse_mini("x*8?").unwrap();
973        assert_eq!(m.sequence.steps.len(), 1);
974        assert_eq!(m.sequence.steps[0].atom, Atom::Trigger);
975        assert_eq!(
976            m.sequence.steps[0].modifiers,
977            vec![Modifier::Repeat(Ramp::fixed(8)), Modifier::Drop(None)]
978        );
979    }
980
981    #[test]
982    fn test_stacked_euclidean_and_drop_keeps_written_order() {
983        let m = parse_mini("x(5,8)?0.25").unwrap();
984        assert_eq!(
985            m.sequence.steps[0].modifiers,
986            vec![
987                Modifier::Euclidean(Ramp::fixed(5), Ramp::fixed(8), None),
988                Modifier::Drop(Some(Ramp::fixed(0.25)))
989            ]
990        );
991    }
992
993    #[test]
994    fn test_stacked_weight_and_repeat() {
995        let m = parse_mini("[c4 e4]*2@3 g4").unwrap();
996        assert_eq!(
997            m.sequence.steps[0].modifiers,
998            vec![Modifier::Repeat(Ramp::fixed(2)), Modifier::Weight(3)]
999        );
1000    }
1001
1002    // ---- Drop probability ----
1003
1004    #[test]
1005    fn test_drop_with_probability() {
1006        let m = parse_mini("x?0.3").unwrap();
1007        assert_eq!(
1008            m.sequence.steps[0].modifiers,
1009            vec![Modifier::Drop(Some(Ramp::fixed(0.3)))]
1010        );
1011    }
1012
1013    // ---- Solo ----
1014
1015    #[test]
1016    fn test_solo_atom() {
1017        let m = parse_mini("solo(0..7, 8)").unwrap();
1018        assert_eq!(
1019            m.sequence.steps[0].atom,
1020            Atom::Solo(Solo {
1021                low: 0,
1022                high: 7,
1023                steps: Ramp::fixed(8)
1024            })
1025        );
1026        // Negative degrees walk below the root octave.
1027        let m = parse_mini("solo(-3..4,6)").unwrap();
1028        assert_eq!(
1029            m.sequence.steps[0].atom,
1030            Atom::Solo(Solo {
1031                low: -3,
1032                high: 4,
1033                steps: Ramp::fixed(6)
1034            })
1035        );
1036        // The step count may travel across the line's ramp span.
1037        let m = parse_mini("solo(0..12, 4..16)").unwrap();
1038        assert_eq!(
1039            m.sequence.steps[0].atom,
1040            Atom::Solo(Solo {
1041                low: 0,
1042                high: 12,
1043                steps: Ramp::Sweep { from: 4, to: 16 }
1044            })
1045        );
1046    }
1047
1048    #[test]
1049    fn test_solo_composes_with_sequences_and_modifiers() {
1050        // Half a cycle of kick-space, half of solo.
1051        let m = parse_mini("~ solo(0..7,4)").unwrap();
1052        assert_eq!(m.sequence.steps.len(), 2);
1053        assert!(matches!(m.sequence.steps[1].atom, Atom::Solo(_)));
1054        // A drop chance applies to the whole generated slot.
1055        let m = parse_mini("solo(0..7,8)?0.3").unwrap();
1056        assert_eq!(
1057            m.sequence.steps[0].modifiers,
1058            vec![Modifier::Drop(Some(Ramp::fixed(0.3)))]
1059        );
1060    }
1061
1062    #[test]
1063    fn test_solo_validation() {
1064        // A walk needs somewhere to go, and at least one note.
1065        assert!(parse_mini("solo(5..5,8)").is_err());
1066        assert!(parse_mini("solo(7..0,8)").is_err());
1067        assert!(parse_mini("solo(0..7,0)").is_err());
1068        assert!(parse_mini("solo(0..7,4..0)").is_err());
1069        // A generated walk has no single note to lend a chord, but it can sit
1070        // beside other choice options or subdivision steps.
1071        assert!(parse_mini("[c4,solo(0..7,4)]").is_err());
1072        assert!(parse_mini("[c4|solo(0..7,4)]").is_ok());
1073        assert!(parse_mini("[c4 solo(0..7,4)]").is_ok());
1074    }
1075
1076    // ---- Ramps ----
1077
1078    #[test]
1079    fn test_euclidean_onsets_may_ramp() {
1080        // The density crescendo: four hits growing to sixteen.
1081        let m = parse_mini("x(4..16,4)").unwrap();
1082        assert_eq!(
1083            m.sequence.steps[0].modifiers,
1084            vec![Modifier::Euclidean(
1085                Ramp::Sweep { from: 4, to: 16 },
1086                Ramp::fixed(4),
1087                None
1088            )]
1089        );
1090    }
1091
1092    #[test]
1093    fn test_ranges_do_not_confuse_the_float_parser() {
1094        // `4..16` must not read the first dot as a decimal point.
1095        let m = parse_mini("x?0.1..0.9").unwrap();
1096        assert_eq!(
1097            m.sequence.steps[0].modifiers,
1098            vec![Modifier::Drop(Some(Ramp::Sweep { from: 0.1, to: 0.9 }))]
1099        );
1100        let m = parse_mini("x*2..8").unwrap();
1101        assert_eq!(
1102            m.sequence.steps[0].modifiers,
1103            vec![Modifier::Repeat(Ramp::Sweep { from: 2, to: 8 })]
1104        );
1105        // Positions can travel too, and an offset still follows.
1106        let m = parse_mini("x(3,4..16,1)").unwrap();
1107        assert_eq!(
1108            m.sequence.steps[0].modifiers,
1109            vec![Modifier::Euclidean(
1110                Ramp::fixed(3),
1111                Ramp::Sweep { from: 4, to: 16 },
1112                Some(1)
1113            )]
1114        );
1115    }
1116
1117    #[test]
1118    fn test_a_step_chain_holds_each_value_in_turn() {
1119        // The doubling build: not a smooth sweep but four held stages.
1120        let m = parse_mini("x(2>4>8>16,4)").unwrap();
1121        assert_eq!(
1122            m.sequence.steps[0].modifiers,
1123            vec![Modifier::Euclidean(
1124                Ramp::Steps {
1125                    first: 2,
1126                    rest: vec![4, 8, 16]
1127                },
1128                Ramp::fixed(4),
1129                None
1130            )]
1131        );
1132
1133        let m = parse_mini("x*4>8>16").unwrap();
1134        assert_eq!(
1135            m.sequence.steps[0].modifiers,
1136            vec![Modifier::Repeat(Ramp::Steps {
1137                first: 4,
1138                rest: vec![8, 16]
1139            })]
1140        );
1141
1142        let m = parse_mini("x?0.2>0.6>0.9").unwrap();
1143        assert_eq!(
1144            m.sequence.steps[0].modifiers,
1145            vec![Modifier::Drop(Some(Ramp::Steps {
1146                first: 0.2,
1147                rest: vec![0.6, 0.9]
1148            }))]
1149        );
1150    }
1151
1152    #[test]
1153    fn test_a_step_chain_does_not_swallow_an_alternation_close() {
1154        // `<x*4>` must still parse: the chain has to backtrack off the `>`.
1155        let m = parse_mini("<x*4 x*8>").unwrap();
1156        let Atom::Alternation(ref alternation) = m.sequence.steps[0].atom else {
1157            panic!(
1158                "expected an alternation, got {:?}",
1159                m.sequence.steps[0].atom
1160            );
1161        };
1162        assert_eq!(alternation.sequence.steps.len(), 2);
1163        assert_eq!(
1164            alternation.sequence.steps[0].modifiers,
1165            vec![Modifier::Repeat(Ramp::fixed(4))]
1166        );
1167        assert_eq!(
1168            alternation.sequence.steps[1].modifiers,
1169            vec![Modifier::Repeat(Ramp::fixed(8))]
1170        );
1171    }
1172
1173    #[test]
1174    fn test_every_stage_of_a_chain_is_validated() {
1175        assert!(parse_mini("x*2>0>8").is_err());
1176        assert!(parse_mini("x(3,4>0)").is_err());
1177        assert!(parse_mini("x?0.2>2.0").is_err());
1178    }
1179
1180    #[test]
1181    fn test_both_ends_of_a_range_are_validated() {
1182        assert!(parse_mini("x*1..0").is_err());
1183        assert!(parse_mini("x(3,4..0)").is_err());
1184        assert!(parse_mini("x?0.5..2").is_err());
1185    }
1186
1187    #[test]
1188    fn test_drop_probability_out_of_range_is_rejected() {
1189        let error = parse_mini("x?2").unwrap_err();
1190        assert!(error.contains("0.0-1.0"), "unexpected message: {error}");
1191    }
1192
1193    #[test]
1194    fn test_bare_drop_then_degree_is_two_steps() {
1195        // A space means the number is the next step, not the drop probability.
1196        let m = parse_mini("x? 0").unwrap();
1197        assert_eq!(m.sequence.steps.len(), 2);
1198        assert_eq!(m.sequence.steps[0].modifiers, vec![Modifier::Drop(None)]);
1199        assert_eq!(m.sequence.steps[1].atom, Atom::Degree(0));
1200    }
1201
1202    // ---- Velocity and accent ----
1203
1204    #[test]
1205    fn test_accent_is_a_full_velocity_trigger() {
1206        let m = parse_mini("X ~ x ~").unwrap();
1207        assert_eq!(m.sequence.steps[0].atom, Atom::Trigger);
1208        assert_eq!(m.sequence.steps[0].velocity, Some(Ramp::fixed(1.0)));
1209        // Lowercase keeps meaning "normal", i.e. take the line's `vel`.
1210        assert_eq!(m.sequence.steps[2].atom, Atom::Trigger);
1211        assert_eq!(m.sequence.steps[2].velocity, None);
1212        // And it is exactly the `:1.0` spelling.
1213        assert_eq!(parse_mini("X").unwrap(), parse_mini("x:1.0").unwrap());
1214    }
1215
1216    #[test]
1217    fn test_explicit_velocity_on_any_sounding_atom() {
1218        let velocity = |source: &str| {
1219            parse_mini(source).unwrap().sequence.steps[0]
1220                .velocity
1221                .clone()
1222        };
1223        assert_eq!(velocity("x:0.6"), Some(Ramp::fixed(0.6)));
1224        assert_eq!(velocity("c4:0.35"), Some(Ramp::fixed(0.35)));
1225        assert_eq!(velocity("3:0.5"), Some(Ramp::fixed(0.5)));
1226        assert_eq!(velocity("[c4 e4]:0.8"), Some(Ramp::fixed(0.8)));
1227        assert_eq!(velocity("<c4 e4>:0.8"), Some(Ramp::fixed(0.8)));
1228        assert_eq!(velocity("solo(0..7,8):0.7"), Some(Ramp::fixed(0.7)));
1229        // An inner step keeps its own, so a group's velocity is only a default.
1230        let m = parse_mini("[c4 e4:0.4]:0.9").unwrap();
1231        assert_eq!(m.sequence.steps[0].velocity, Some(Ramp::fixed(0.9)));
1232        let Atom::Group(ref group) = m.sequence.steps[0].atom else {
1233            panic!("expected a group");
1234        };
1235        assert_eq!(group.layers[0].steps[1].velocity, Some(Ramp::fixed(0.4)));
1236    }
1237
1238    #[test]
1239    fn test_velocity_composes_with_slot_generating_modifiers() {
1240        // Written once, whichever side of the modifier — one velocity for every
1241        // slot the step generates.
1242        let m = parse_mini("X*4").unwrap();
1243        assert_eq!(
1244            m.sequence.steps[0].modifiers,
1245            vec![Modifier::Repeat(Ramp::fixed(4))]
1246        );
1247        assert_eq!(m.sequence.steps[0].velocity, Some(Ramp::fixed(1.0)));
1248
1249        assert_eq!(
1250            parse_mini("x:0.6*4").unwrap(),
1251            parse_mini("x*4:0.6").unwrap()
1252        );
1253
1254        let m = parse_mini("x:0.6?0.25").unwrap();
1255        assert_eq!(
1256            m.sequence.steps[0].modifiers,
1257            vec![Modifier::Drop(Some(Ramp::fixed(0.25)))]
1258        );
1259        assert_eq!(m.sequence.steps[0].velocity, Some(Ramp::fixed(0.6)));
1260
1261        let m = parse_mini("x(3,8):0.9").unwrap();
1262        assert_eq!(
1263            m.sequence.steps[0].modifiers,
1264            vec![Modifier::Euclidean(Ramp::fixed(3), Ramp::fixed(8), None)]
1265        );
1266        assert_eq!(m.sequence.steps[0].velocity, Some(Ramp::fixed(0.9)));
1267    }
1268
1269    #[test]
1270    fn test_velocity_may_travel() {
1271        let m = parse_mini("x:0.3..0.9").unwrap();
1272        assert_eq!(
1273            m.sequence.steps[0].velocity,
1274            Some(Ramp::Sweep { from: 0.3, to: 0.9 })
1275        );
1276        assert!(m.sequence.steps[0].velocity.as_ref().unwrap().travels());
1277        let m = parse_mini("x:0.3>0.6>0.9").unwrap();
1278        assert_eq!(
1279            m.sequence.steps[0].velocity,
1280            Some(Ramp::Steps {
1281                first: 0.3,
1282                rest: vec![0.6, 0.9]
1283            })
1284        );
1285        // A closing `>` is still left for the alternation.
1286        let m = parse_mini("<x:0.4 x:0.8>").unwrap();
1287        let Atom::Alternation(ref alternation) = m.sequence.steps[0].atom else {
1288            panic!("expected an alternation");
1289        };
1290        assert_eq!(
1291            alternation.sequence.steps[1].velocity,
1292            Some(Ramp::fixed(0.8))
1293        );
1294    }
1295
1296    #[test]
1297    fn test_velocity_out_of_range_is_rejected_at_every_value() {
1298        let error = parse_mini("x:1.4").unwrap_err();
1299        assert!(
1300            error.contains("1.4") && error.contains("0.0-1.0"),
1301            "{error}"
1302        );
1303        assert!(parse_mini("x:0.5..1.2").unwrap_err().contains("1.2"));
1304        assert!(parse_mini("x:0.2>0.9>1.1").unwrap_err().contains("1.1"));
1305        // Validation reaches nested layers like every other check.
1306        assert!(parse_mini("[c4 e4:2]").is_err());
1307    }
1308
1309    #[test]
1310    fn test_velocity_on_a_rest_or_a_hold_is_rejected() {
1311        let error = parse_mini("~:0.5").unwrap_err();
1312        assert!(error.contains("rest"), "{error}");
1313        let error = parse_mini("x _:0.5").unwrap_err();
1314        assert!(error.contains("hold"), "{error}");
1315    }
1316
1317    #[test]
1318    fn test_one_velocity_per_step() {
1319        let error = parse_mini("x:0.6:0.8").unwrap_err();
1320        assert!(error.contains("one velocity"), "{error}");
1321        // `X` already set one.
1322        let error = parse_mini("X:0.6").unwrap_err();
1323        assert!(error.contains("already a velocity of 1.0"), "{error}");
1324    }
1325
1326    // ---- Random choice ----
1327
1328    #[test]
1329    fn test_random_choice_group() {
1330        let m = parse_mini("[c4|e4|g4]").unwrap();
1331        assert_eq!(m.sequence.steps.len(), 1);
1332        if let Atom::Group(ref g) = m.sequence.steps[0].atom {
1333            assert_eq!(g.mode, GroupMode::Random);
1334            assert_eq!(g.layers.len(), 3);
1335            assert_eq!(g.layers[0].steps[0].atom, nat(NoteLetter::C, 4));
1336            assert_eq!(g.layers[2].steps[0].atom, nat(NoteLetter::G, 4));
1337        } else {
1338            panic!("expected random-choice group");
1339        }
1340    }
1341
1342    #[test]
1343    fn test_random_choice_layers_may_be_sequences() {
1344        let m = parse_mini("[x x|x ~ x]").unwrap();
1345        if let Atom::Group(ref g) = m.sequence.steps[0].atom {
1346            assert_eq!(g.mode, GroupMode::Random);
1347            assert_eq!(g.layers[0].steps.len(), 2);
1348            assert_eq!(g.layers[1].steps.len(), 3);
1349        } else {
1350            panic!("expected random-choice group");
1351        }
1352    }
1353
1354    #[test]
1355    fn test_mixing_chord_and_random_separators_is_rejected() {
1356        let error = parse_mini("[c4,e4|g4]").unwrap_err();
1357        assert!(error.contains("not both"), "unexpected message: {error}");
1358    }
1359
1360    // ---- Validation ----
1361
1362    #[test]
1363    fn test_euclidean_accepts_more_onsets_than_steps() {
1364        // `(9,8)` is nine onsets over eight slots, so one slot subdivides.
1365        let m = parse_mini("x(9,8)").unwrap();
1366        assert_eq!(
1367            m.sequence.steps[0].modifiers,
1368            vec![Modifier::Euclidean(Ramp::fixed(9), Ramp::fixed(8), None)]
1369        );
1370        assert!(parse_mini("x(20,8)").is_ok());
1371    }
1372
1373    #[test]
1374    fn test_zero_counts_are_rejected() {
1375        assert!(parse_mini("x*0").is_err());
1376        assert!(parse_mini("x!0").is_err());
1377        assert!(parse_mini("x@0").is_err());
1378        assert!(parse_mini("x(3,0)").is_err());
1379        // A zero-onset Euclidean is a legitimate silence, not an error.
1380        assert!(parse_mini("x(0,8)").is_ok());
1381    }
1382
1383    #[test]
1384    fn test_validation_reaches_nested_layers() {
1385        assert!(parse_mini("[c4 e4*0]").is_err());
1386        assert!(parse_mini("<c4 e4?3>").is_err());
1387    }
1388}