Skip to main content

audiobox/synth/
parse.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use super::score::{
5    Arpeggio, ChannelWriter, DutySweep, Envelope, Instrument, Length, Pitch, Score, ScoreLimits,
6    Vibrato,
7};
8use super::wave::{Duty, NoiseMode, Waveform};
9use crate::{Error, ErrorCode};
10mod chip;
11mod synthwave;
12
13/// An error in score text, with the 1-based line and column (in characters) where it was found.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct ParseError {
16    pub line: usize,
17    pub column: usize,
18    /// [`ErrorCode::Syntax`] for malformed text, [`ErrorCode::InvalidArgument`] for
19    /// out-of-range values, and [`ErrorCode::LimitExceeded`] for exceeded [`ScoreLimits`].
20    pub code: ErrorCode,
21    pub message: String,
22}
23impl fmt::Display for ParseError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(
26            f,
27            "line {}, column {}: {}",
28            self.line, self.column, self.message
29        )
30    }
31}
32impl std::error::Error for ParseError {}
33impl From<ParseError> for Error {
34    fn from(e: ParseError) -> Self {
35        Error::new(e.code, e.to_string())
36    }
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq)]
40struct Pos {
41    line: usize,
42    column: usize,
43}
44impl Pos {
45    fn error(self, code: ErrorCode, message: impl Into<String>) -> ParseError {
46        ParseError {
47            line: self.line,
48            column: self.column,
49            code,
50            message: message.into(),
51        }
52    }
53    fn syntax(self, message: impl Into<String>) -> ParseError {
54        self.error(ErrorCode::Syntax, message)
55    }
56    fn invalid(self, message: impl Into<String>) -> ParseError {
57        self.error(ErrorCode::InvalidArgument, message)
58    }
59    fn wrap(self, error: Error) -> ParseError {
60        self.error(error.code, error.message)
61    }
62}
63
64type Parsed<T> = std::result::Result<T, ParseError>;
65
66/// Characters of one line with their positions.
67struct Cursor<'a> {
68    chars: std::iter::Peekable<std::str::CharIndices<'a>>,
69    text: &'a str,
70    line: usize,
71    column: usize,
72}
73impl<'a> Cursor<'a> {
74    fn new(text: &'a str, line: usize, column: usize) -> Self {
75        Self {
76            chars: text.char_indices().peekable(),
77            text,
78            line,
79            column,
80        }
81    }
82    fn pos(&self) -> Pos {
83        Pos {
84            line: self.line,
85            column: self.column,
86        }
87    }
88    fn peek(&mut self) -> Option<char> {
89        self.chars.peek().map(|&(_, c)| c)
90    }
91    fn bump(&mut self) -> Option<char> {
92        let c = self.chars.next().map(|(_, c)| c);
93        if c.is_some() {
94            self.column += 1;
95        }
96        c
97    }
98    fn eat(&mut self, c: char) -> bool {
99        if self.peek() == Some(c) {
100            self.bump();
101            true
102        } else {
103            false
104        }
105    }
106    fn skip_space(&mut self) {
107        while self.peek().is_some_and(char::is_whitespace) {
108            self.bump();
109        }
110    }
111    fn take_while(&mut self, f: impl Fn(char) -> bool) -> &'a str {
112        let start = self.chars.peek().map_or(self.text.len(), |&(i, _)| i);
113        while self.peek().is_some_and(&f) {
114            self.bump();
115        }
116        let end = self.chars.peek().map_or(self.text.len(), |&(i, _)| i);
117        &self.text[start..end]
118    }
119    fn word(&mut self) -> (Pos, &'a str) {
120        self.skip_space();
121        let pos = self.pos();
122        (pos, self.take_while(|c| !c.is_whitespace()))
123    }
124    fn name(&mut self, what: &str) -> Parsed<String> {
125        let pos = self.pos();
126        let name = self.take_while(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
127        if name.is_empty() {
128            return Err(pos.syntax(format!("Expected {what} name")));
129        }
130        Ok(name.to_owned())
131    }
132    fn number(&mut self) -> Parsed<Option<u32>> {
133        let pos = self.pos();
134        let digits = self.take_while(|c| c.is_ascii_digit());
135        if digits.is_empty() {
136            return Ok(None);
137        }
138        digits
139            .parse()
140            .map(Some)
141            .map_err(|_| pos.invalid("Number is too large"))
142    }
143    fn required(&mut self, what: &str) -> Parsed<u32> {
144        let pos = self.pos();
145        self.number()?
146            .ok_or_else(|| pos.syntax(format!("Expected {what}")))
147    }
148}
149
150fn ranged(pos: Pos, value: u32, range: std::ops::RangeInclusive<u32>, what: &str) -> Parsed<u32> {
151    if range.contains(&value) {
152        Ok(value)
153    } else {
154        Err(pos.invalid(format!(
155            "{what} must be {}..={}, not {value}",
156            range.start(),
157            range.end()
158        )))
159    }
160}
161
162#[derive(Clone, Copy, Debug)]
163enum LengthKind {
164    Default,
165    Note(u16),
166    Ticks(u32),
167}
168#[derive(Clone, Copy, Debug)]
169struct LengthSpec {
170    kind: LengthKind,
171    dots: u8,
172}
173
174#[derive(Clone, Debug)]
175enum Token {
176    Note(char, i8, LengthSpec, Option<u8>),
177    Chord(Vec<(char, i8, i8)>, LengthSpec, Option<u8>),
178    Rest(LengthSpec),
179    Extend(LengthSpec),
180    TieNext,
181    Octave(u8),
182    Up,
183    Down,
184    DefaultLength(LengthSpec),
185    Volume(u8),
186    Gate(u8),
187    Instrument(String),
188    Call(String),
189    Loop,
190    Open,
191    Break,
192    Close(u32),
193}
194
195fn length_spec(cursor: &mut Cursor) -> Parsed<LengthSpec> {
196    let pos = cursor.pos();
197    let kind = if cursor.eat('%') {
198        LengthKind::Ticks(ranged(
199            pos,
200            cursor.required("ticks after %")?,
201            1..=u32::MAX,
202            "Ticks",
203        )?)
204    } else {
205        match cursor.number()? {
206            Some(n) => LengthKind::Note(ranged(pos, n, 1..=3840, "Note length")? as u16),
207            None => LengthKind::Default,
208        }
209    };
210    let mut dots = 0u8;
211    while cursor.eat('.') {
212        dots = dots.saturating_add(1);
213    }
214    if dots > 0 && matches!(kind, LengthKind::Ticks(_)) {
215        return Err(pos.syntax("A tick length cannot be dotted"));
216    }
217    Ok(LengthSpec { kind, dots })
218}
219
220fn lex_body(cursor: &mut Cursor, out: &mut Vec<(Token, Pos)>) -> Parsed<()> {
221    loop {
222        cursor.skip_space();
223        let pos = cursor.pos();
224        let Some(c) = cursor.bump() else {
225            return Ok(());
226        };
227        let token = match c {
228            'a'..='g' => {
229                let mut accidental = 0i8;
230                loop {
231                    if cursor.eat('+') || cursor.eat('#') {
232                        accidental = accidental.saturating_add(1);
233                    } else if cursor.eat('-') {
234                        accidental = accidental.saturating_sub(1);
235                    } else {
236                        break;
237                    }
238                }
239                let length = length_spec(cursor)?;
240                let velocity = if cursor.eat('!') {
241                    let velocity_pos = cursor.pos();
242                    Some(ranged(
243                        velocity_pos,
244                        cursor.required("velocity after !")?,
245                        0..=127,
246                        "Velocity",
247                    )? as u8)
248                } else {
249                    None
250                };
251                Token::Note(c, accidental, length, velocity)
252            }
253            'r' => Token::Rest(length_spec(cursor)?),
254            '{' => {
255                let mut members = Vec::new();
256                let mut octave = 0i8;
257                loop {
258                    cursor.skip_space();
259                    match cursor.bump() {
260                        Some('}') => break,
261                        Some('>') => {
262                            octave = octave.saturating_add(1);
263                        }
264                        Some('<') => {
265                            octave = octave.saturating_sub(1);
266                        }
267                        Some(letter @ 'a'..='g') => {
268                            if members.len() == 16 {
269                                return Err(pos.error(
270                                    ErrorCode::LimitExceeded,
271                                    "At most 16 notes in a chord",
272                                ));
273                            }
274                            let mut accidental = 0i8;
275                            loop {
276                                if cursor.eat('+') || cursor.eat('#') {
277                                    accidental = accidental.saturating_add(1);
278                                } else if cursor.eat('-') {
279                                    accidental = accidental.saturating_sub(1);
280                                } else {
281                                    break;
282                                }
283                            }
284                            members.push((letter, accidental, octave));
285                        }
286                        _ => {
287                            return Err(
288                                pos.syntax("Chord needs notes/octave shifts and a closing '}'")
289                            );
290                        }
291                    }
292                    if !(-9..=9).contains(&octave) {
293                        return Err(pos.invalid("Chord octave shift out of range"));
294                    }
295                }
296                if members.is_empty() {
297                    return Err(pos.invalid("Chord is empty"));
298                }
299                let length = length_spec(cursor)?;
300                let velocity = if cursor.eat('!') {
301                    Some(ranged(pos, cursor.required("velocity")?, 0..=127, "Velocity")? as u8)
302                } else {
303                    None
304                };
305                Token::Chord(members, length, velocity)
306            }
307            '^' => Token::Extend(length_spec(cursor)?),
308            '&' => Token::TieNext,
309            'o' => Token::Octave(
310                ranged(pos, cursor.required("octave after o")?, 0..=9, "Octave")? as u8,
311            ),
312            '>' => Token::Up,
313            '<' => Token::Down,
314            'l' => {
315                let spec = length_spec(cursor)?;
316                if matches!(spec.kind, LengthKind::Default) {
317                    return Err(pos.syntax("Expected a length after l"));
318                }
319                Token::DefaultLength(spec)
320            }
321            'v' => Token::Volume(
322                ranged(pos, cursor.required("volume after v")?, 0..=15, "Volume")? as u8,
323            ),
324            'q' => Token::Gate(ranged(pos, cursor.required("gate after q")?, 1..=8, "Gate")? as u8),
325            '@' => Token::Instrument(cursor.name("an instrument")?),
326            '$' => Token::Call(cursor.name("a pattern")?),
327            'L' => Token::Loop,
328            '[' => Token::Open,
329            '|' => Token::Break,
330            ']' => {
331                let count_pos = cursor.pos();
332                let count = cursor.number()?.unwrap_or(2);
333                Token::Close(ranged(count_pos, count, 1..=65535, "Repeat count")?)
334            }
335            other => return Err(pos.syntax(format!("Unexpected character '{other}'"))),
336        };
337        out.push((token, pos));
338    }
339}
340
341#[derive(Clone, Debug)]
342enum Node {
343    Token(Token, Pos),
344    Repeat {
345        body: Vec<Node>,
346        tail: Vec<Node>,
347        count: u32,
348        pos: Pos,
349    },
350}
351
352fn build_tree(tokens: Vec<(Token, Pos)>) -> Parsed<Vec<Node>> {
353    // Each open repeat holds its start, the nodes before it, and its body if a `|` was seen.
354    let mut stack: Vec<(Pos, Vec<Node>, Option<Vec<Node>>)> = Vec::new();
355    let mut current = Vec::new();
356    for (token, pos) in tokens {
357        match token {
358            Token::Open => stack.push((pos, std::mem::take(&mut current), None)),
359            Token::Break => match stack.last_mut() {
360                Some((_, _, body @ None)) => *body = Some(std::mem::take(&mut current)),
361                Some(_) => return Err(pos.syntax("A repeat has only one '|'")),
362                None => return Err(pos.syntax("'|' outside a repeat")),
363            },
364            Token::Close(count) => {
365                let (open, outer, body) = stack
366                    .pop()
367                    .ok_or_else(|| pos.syntax("']' without a matching '['"))?;
368                let inner = std::mem::replace(&mut current, outer);
369                let (body, tail) = match body {
370                    Some(body) => (body, inner),
371                    None => (inner, Vec::new()),
372                };
373                current.push(Node::Repeat {
374                    body,
375                    tail,
376                    count,
377                    pos: open,
378                });
379            }
380            token => current.push(Node::Token(token, pos)),
381        }
382    }
383    match stack.last() {
384        Some((open, ..)) => Err(open.syntax("'[' is never closed")),
385        None => Ok(current),
386    }
387}
388
389fn parse_instrument(cursor: &mut Cursor) -> Parsed<(String, Instrument, Pos)> {
390    cursor.skip_space();
391    let name_pos = cursor.pos();
392    let name = cursor.name("an instrument")?;
393    let mut instrument = Instrument::default();
394    let (mut wave, mut duties, mut noise) = ("pulse", None, None);
395    let (mut arpeggio, mut arpeggio_step, mut duty_step) = (None, 16, 16);
396    let (mut drum_kind, mut drum_kit) = (None, None);
397    let mut drum_options = Vec::new();
398    let mut melodic_key = None;
399    let mut chip_options = Vec::new();
400    let mut wide_options = Vec::new();
401    let mut chip_wave_key = None;
402    loop {
403        let (pos, word) = cursor.word();
404        if word.is_empty() {
405            break;
406        }
407        let (key, value) = word
408            .split_once('=')
409            .ok_or_else(|| pos.syntax(format!("Expected key=value, found {word}")))?;
410        let value_pos = Pos {
411            column: pos.column + key.chars().count() + 1,
412            ..pos
413        };
414        let numbers = |count: std::ops::RangeInclusive<usize>| -> Parsed<Vec<i64>> {
415            let list: Vec<_> = value.split(',').map(|v| v.trim().parse::<i64>()).collect();
416            match list.into_iter().collect::<std::result::Result<Vec<_>, _>>() {
417                Ok(list) if count.contains(&list.len()) => Ok(list),
418                _ => Err(value_pos.syntax(format!(
419                    "{key} needs {} to {} comma-separated integers",
420                    count.start(),
421                    count.end()
422                ))),
423            }
424        };
425        let to_u32 = |v: i64| -> Parsed<u32> {
426            u32::try_from(v).map_err(|_| value_pos.invalid(format!("{key} is out of range")))
427        };
428        if !matches!(key, "volume" | "drum" | "kit") && !key.starts_with("drum-") {
429            melodic_key = Some(pos);
430        }
431        if matches!(key, "wave" | "duty" | "duty-step" | "noise") {
432            chip_wave_key = Some(pos);
433        }
434        match key {
435            key if synthwave::is_key(key) => {
436                wide_options.push((key.to_owned(), value.to_owned(), value_pos))
437            }
438            key if chip::is_key(key) => {
439                chip_options.push((key.to_owned(), value.to_owned(), value_pos))
440            }
441            "drum" => {
442                drum_kind = Some(match value {
443                    "kick" => super::DrumKind::Kick,
444                    "snare" => super::DrumKind::Snare,
445                    "closed-hat" => super::DrumKind::ClosedHat,
446                    "open-hat" => super::DrumKind::OpenHat,
447                    "tom" => super::DrumKind::Tom,
448                    "clap" => super::DrumKind::Clap,
449                    "crash" => super::DrumKind::Crash,
450                    _ => return Err(value_pos.syntax("Unknown drum kind")),
451                })
452            }
453            "kit" => {
454                drum_kit = Some(match value {
455                    "chiptune" => super::DrumKit::Chiptune,
456                    "electronic" => super::DrumKit::Electronic,
457                    _ => return Err(value_pos.syntax("Kit must be chiptune or electronic")),
458                })
459            }
460            "drum-pitch" | "drum-tone" | "drum-noise" | "drum-mix" | "drum-click" | "drum-seed"
461            | "drum-sweep" => {
462                let count = if key == "drum-sweep" { 2 } else { 1 };
463                let values = numbers(count..=count)?
464                    .into_iter()
465                    .map(to_u32)
466                    .collect::<Parsed<Vec<_>>>()?;
467                drum_options.push((key.to_owned(), values, value_pos));
468            }
469            "wave" => match value {
470                "pulse" | "triangle" | "saw" | "noise" => wave = value,
471                _ => {
472                    return Err(value_pos.syntax(format!(
473                        "wave must be pulse, triangle, saw or noise, not {value}"
474                    )));
475                }
476            },
477            "duty" => {
478                let list = value
479                    .split(',')
480                    .map(|d| d.trim().parse::<Duty>())
481                    .collect::<crate::Result<Vec<_>>>()
482                    .map_err(|e| value_pos.wrap(e))?;
483                duties = Some((list, pos));
484            }
485            "duty-step" => duty_step = to_u32(numbers(1..=1)?[0])?,
486            "noise" => match value {
487                "long" => noise = Some((NoiseMode::Long, pos)),
488                "short" => noise = Some((NoiseMode::Short, pos)),
489                _ => {
490                    return Err(
491                        value_pos.syntax(format!("noise must be long or short, not {value}"))
492                    );
493                }
494            },
495            "adsr" => {
496                let v = numbers(4..=4)?;
497                instrument.envelope = Envelope {
498                    attack_ms: to_u32(v[0])?,
499                    decay_ms: to_u32(v[1])?,
500                    sustain: ranged(value_pos, to_u32(v[2])?, 0..=100, "Sustain")? as u8,
501                    release_ms: to_u32(v[3])?,
502                };
503            }
504            "volume" => {
505                instrument.volume =
506                    ranged(value_pos, to_u32(numbers(1..=1)?[0])?, 0..=15, "Volume")? as u8;
507            }
508            "slide" => {
509                instrument.slide = i32::try_from(numbers(1..=1)?[0])
510                    .map_err(|_| value_pos.invalid("slide is out of range"))?;
511            }
512            "vibrato" => {
513                let v = numbers(2..=3)?;
514                instrument.vibrato = Some(Vibrato {
515                    depth_cents: u16::try_from(v[0])
516                        .map_err(|_| value_pos.invalid("Vibrato depth is out of range"))?,
517                    period_ms: to_u32(v[1])?,
518                    delay_ms: to_u32(v.get(2).copied().unwrap_or(0))?,
519                });
520            }
521            "arpeggio" => {
522                let offsets = numbers(1..=16)?
523                    .into_iter()
524                    .map(|v| {
525                        i8::try_from(v)
526                            .map_err(|_| value_pos.invalid("Arpeggio offset is out of range"))
527                    })
528                    .collect::<Parsed<Vec<_>>>()?;
529                arpeggio = Some(offsets);
530            }
531            "arpeggio-step" => arpeggio_step = to_u32(numbers(1..=1)?[0])?,
532            _ => return Err(pos.syntax(format!("Unknown instrument key {key}"))),
533        }
534    }
535    if let Some(kind) = drum_kind {
536        if let Some(pos) = melodic_key {
537            return Err(pos.invalid("Drums cannot use melodic oscillator or modulation keys"));
538        }
539        let mut drum = super::Drum::new(kind, drum_kit.unwrap_or(super::DrumKit::Electronic));
540        for (key, values, pos) in drum_options {
541            let as_u8 = |v| u8::try_from(v).map_err(|_| pos.invalid("Drum value is out of range"));
542            let as_u16 =
543                |v| u16::try_from(v).map_err(|_| pos.invalid("Drum value is out of range"));
544            match key.as_str() {
545                "drum-pitch" => drum.pitch_hz = as_u16(values[0])?,
546                "drum-tone" => drum.tone_ms = values[0],
547                "drum-noise" => drum.noise_ms = values[0],
548                "drum-mix" => drum.noise_percent = as_u8(values[0])?,
549                "drum-click" => drum.click_percent = as_u8(values[0])?,
550                "drum-seed" => drum.seed = values[0],
551                "drum-sweep" => {
552                    drum.sweep_cents = as_u16(values[0])?;
553                    drum.sweep_ms = values[1];
554                }
555                _ => unreachable!(),
556            }
557        }
558        drum.validate().map_err(|e| name_pos.wrap(e))?;
559        instrument.drum = Some(drum);
560    } else if drum_kit.is_some() || !drum_options.is_empty() {
561        return Err(name_pos.invalid("kit and drum parameters require drum=KIND"));
562    }
563    instrument.waveform = match wave {
564        "pulse" => Waveform::Pulse(match &duties {
565            Some((list, _)) => list[0],
566            None => Duty::Half,
567        }),
568        "triangle" => Waveform::Triangle,
569        "saw" => Waveform::Sawtooth,
570        _ => Waveform::Noise(noise.map_or(NoiseMode::Long, |(m, _)| m)),
571    };
572    if let Some((_, pos)) = noise.filter(|_| wave != "noise") {
573        return Err(pos.invalid("noise needs wave=noise"));
574    }
575    if let Some((list, pos)) = duties {
576        if wave != "pulse" {
577            return Err(pos.invalid("duty needs wave=pulse"));
578        }
579        if list.len() > 1 {
580            instrument.duty_sweep = Some(DutySweep {
581                duties: list[1..].to_vec(),
582                step_ms: duty_step,
583            });
584        }
585    }
586    instrument.arpeggio = arpeggio.map(|offsets| Arpeggio {
587        offsets,
588        step_ms: arpeggio_step,
589    });
590    if !chip_options.is_empty() {
591        chip::apply(chip_options, &mut instrument, chip_wave_key)?;
592    }
593    if !wide_options.is_empty() {
594        synthwave::apply(wide_options, &mut instrument)?;
595    }
596    instrument.validate().map_err(|e| name_pos.wrap(e))?;
597    Ok((name, instrument, name_pos))
598}
599
600/// The channel state that only exists in text: octave, default length and a pending `&`.
601struct TextState {
602    octave: i8,
603    length: Length,
604    last_pitch: Option<Pitch>,
605    tie: Option<Pos>,
606}
607
608struct Expander<'a> {
609    patterns: &'a HashMap<String, Vec<Node>>,
610    limits: &'a ScoreLimits,
611    events: usize,
612    calls: Vec<&'a str>,
613}
614impl<'a> Expander<'a> {
615    fn run(
616        &mut self,
617        nodes: &'a [Node],
618        writer: &mut ChannelWriter,
619        state: &mut TextState,
620        depth: usize,
621    ) -> Parsed<()> {
622        for node in nodes {
623            let pos = match node {
624                Node::Token(_, pos) | Node::Repeat { pos, .. } => *pos,
625            };
626            self.spend(pos)?;
627            match node {
628                Node::Repeat {
629                    body,
630                    tail,
631                    count,
632                    pos,
633                } => {
634                    if depth >= self.limits.max_nesting {
635                        return Err(pos.error(
636                            ErrorCode::LimitExceeded,
637                            "Repeats and patterns are nested too deeply",
638                        ));
639                    }
640                    for i in 0..*count {
641                        self.spend(*pos)?;
642                        self.run(body, writer, state, depth + 1)?;
643                        if i + 1 < *count {
644                            self.run(tail, writer, state, depth + 1)?;
645                        }
646                    }
647                }
648                Node::Token(token, pos) => self.token(token, *pos, writer, state, depth)?,
649            }
650        }
651        Ok(())
652    }
653    fn spend(&mut self, pos: Pos) -> Parsed<()> {
654        if self.events >= self.limits.max_events {
655            return Err(pos.error(ErrorCode::LimitExceeded, "Score exceeds its event limit"));
656        }
657        self.events += 1;
658        Ok(())
659    }
660    fn token(
661        &mut self,
662        token: &'a Token,
663        pos: Pos,
664        writer: &mut ChannelWriter,
665        state: &mut TextState,
666        depth: usize,
667    ) -> Parsed<()> {
668        if let Some(tie) = state.tie {
669            if !matches!(
670                token,
671                Token::Note(..) | Token::Octave(_) | Token::Up | Token::Down
672            ) {
673                return Err(tie.syntax("'&' must be followed by a note"));
674            }
675        }
676        let length = |spec: &LengthSpec, state: &TextState| -> Length {
677            match spec.kind {
678                LengthKind::Ticks(t) => Length::Ticks(t),
679                LengthKind::Note(denominator) => Length::Note {
680                    denominator,
681                    dots: spec.dots,
682                },
683                LengthKind::Default => (0..spec.dots).fold(state.length, |l, _| l.dotted()),
684            }
685        };
686        let wrap = |e| pos.wrap(e);
687        match token {
688            Token::Chord(members, spec, velocity) => {
689                let pitches = members
690                    .iter()
691                    .map(|&(letter, accidental, shift)| {
692                        Pitch::from_name(letter, accidental, state.octave + shift).map_err(wrap)
693                    })
694                    .collect::<Parsed<Vec<_>>>()?;
695                writer
696                    .chord_with_velocity(&pitches, length(spec, state), velocity.unwrap_or(127))
697                    .map_err(wrap)?;
698                // '&' is deliberately limited to single notes; '^' extends a chord group.
699                state.last_pitch = None;
700            }
701            Token::Note(letter, accidental, spec, velocity) => {
702                let pitch = Pitch::from_name(*letter, *accidental, state.octave).map_err(wrap)?;
703                let len = length(spec, state);
704                if let Some(tie) = state.tie.take() {
705                    if velocity.is_some() {
706                        return Err(pos.invalid("A tied continuation cannot set velocity"));
707                    }
708                    if state.last_pitch != Some(pitch) {
709                        return Err(tie.syntax("'&' joins notes of different pitch"));
710                    }
711                    writer.tie(len).map_err(wrap)?;
712                } else {
713                    writer
714                        .note_with_velocity(pitch, len, velocity.unwrap_or(127))
715                        .map_err(wrap)?;
716                }
717                state.last_pitch = Some(pitch);
718            }
719            Token::Rest(spec) => {
720                writer.rest(length(spec, state)).map_err(wrap)?;
721                state.last_pitch = None;
722            }
723            Token::Extend(spec) => {
724                writer.tie(length(spec, state)).map_err(wrap)?;
725            }
726            Token::TieNext => {
727                if state.last_pitch.is_none() {
728                    return Err(pos.syntax("'&' must follow a note"));
729                }
730                state.tie = Some(pos);
731            }
732            Token::Octave(o) => state.octave = *o as i8,
733            Token::Up | Token::Down => {
734                let octave = state.octave + if matches!(token, Token::Up) { 1 } else { -1 };
735                if !(0..=9).contains(&octave) {
736                    return Err(pos.invalid("Octave must stay within 0..=9"));
737                }
738                state.octave = octave;
739            }
740            Token::DefaultLength(spec) => {
741                let len = length(spec, state);
742                len.ticks(writer.score().ticks_per_quarter())
743                    .map_err(wrap)?;
744                state.length = len;
745            }
746            Token::Volume(v) => {
747                writer.volume(*v).map_err(wrap)?;
748            }
749            Token::Gate(g) => {
750                writer.gate(*g).map_err(wrap)?;
751            }
752            Token::Instrument(name) => {
753                let index = writer
754                    .score()
755                    .instrument_index(name)
756                    .ok_or_else(|| pos.syntax(format!("Unknown instrument {name}")))?;
757                writer.instrument(index).map_err(wrap)?;
758            }
759            Token::Loop => {
760                writer.loop_point().map_err(wrap)?;
761            }
762            Token::Call(name) => {
763                let (key, body) = self
764                    .patterns
765                    .get_key_value(name)
766                    .ok_or_else(|| pos.syntax(format!("Unknown pattern {name}")))?;
767                if self.calls.contains(&key.as_str()) {
768                    return Err(pos.syntax(format!("Pattern {name} refers to itself")));
769                }
770                if depth >= self.limits.max_nesting {
771                    return Err(pos.error(
772                        ErrorCode::LimitExceeded,
773                        "Repeats and patterns are nested too deeply",
774                    ));
775                }
776                self.calls.push(key);
777                self.run(body, writer, state, depth + 1)?;
778                self.calls.pop();
779            }
780            Token::Open | Token::Break | Token::Close(_) => unreachable!(),
781        }
782        Ok(())
783    }
784}
785
786struct Body {
787    name: String,
788    pos: Pos,
789    tokens: Vec<(Token, Pos)>,
790}
791
792fn body_entry(bodies: &mut Vec<Body>, name: String, pos: Pos) -> &mut Body {
793    let index = match bodies.iter().position(|b| b.name == name) {
794        Some(i) => i,
795        None => {
796            bodies.push(Body {
797                name,
798                pos,
799                tokens: Vec::new(),
800            });
801            bodies.len() - 1
802        }
803    };
804    &mut bodies[index]
805}
806
807pub(super) fn parse(text: &str, limits: &ScoreLimits) -> Parsed<Score> {
808    let start = Pos { line: 1, column: 1 };
809    if text.len() > limits.max_text_bytes {
810        return Err(start.error(
811            ErrorCode::LimitExceeded,
812            "Score text exceeds its length limit",
813        ));
814    }
815    let (mut tempo, mut ticks) = (None, None);
816    let mut instruments = Vec::new();
817    let mut patterns: Vec<Body> = Vec::new();
818    let mut channels: Vec<Body> = Vec::new();
819    for (index, raw) in text.lines().enumerate() {
820        let content = raw.split(';').next().unwrap_or("");
821        let mut cursor = Cursor::new(content, index + 1, 1);
822        let (pos, keyword) = cursor.word();
823        match keyword {
824            "" => continue,
825            "tempo" | "ticks" => {
826                cursor.skip_space();
827                let value_pos = cursor.pos();
828                let value = cursor.required(&format!("a number after {keyword}"))?;
829                let slot = if keyword == "tempo" {
830                    &mut tempo
831                } else {
832                    &mut ticks
833                };
834                if slot.is_some() {
835                    return Err(pos.syntax(format!("{keyword} is set twice")));
836                }
837                let range = if keyword == "tempo" { 1..=999 } else { 1..=960 };
838                *slot = Some(ranged(value_pos, value, range, keyword)? as u16);
839            }
840            "instrument" => instruments.push(parse_instrument(&mut cursor)?),
841            "pattern" | "channel" => {
842                cursor.skip_space();
843                let name_pos = cursor.pos();
844                let name = cursor.name(keyword)?;
845                cursor.skip_space();
846                let eq = cursor.pos();
847                if !cursor.eat('=') {
848                    return Err(eq.syntax(format!("Expected '=' after the {keyword} name")));
849                }
850                let list = if keyword == "pattern" {
851                    &mut patterns
852                } else {
853                    &mut channels
854                };
855                lex_body(&mut cursor, &mut body_entry(list, name, name_pos).tokens)?;
856                continue;
857            }
858            other => return Err(pos.syntax(format!("Unknown directive {other}"))),
859        }
860        cursor.skip_space();
861        if cursor.peek().is_some() {
862            return Err(cursor
863                .pos()
864                .syntax("Unexpected text at the end of the line"));
865        }
866    }
867    let mut score = Score::with_limits(tempo.unwrap_or(120), ticks.unwrap_or(48), limits.clone())
868        .map_err(|e| start.wrap(e))?;
869    for (name, instrument, pos) in instruments {
870        score
871            .add_instrument(name, instrument)
872            .map_err(|e| pos.wrap(e))?;
873    }
874    let mut trees = HashMap::new();
875    for body in patterns {
876        trees.insert(body.name, build_tree(body.tokens)?);
877    }
878    let channel_trees = channels
879        .into_iter()
880        .map(|body| Ok((body.name, body.pos, build_tree(body.tokens)?)))
881        .collect::<Parsed<Vec<_>>>()?;
882    let mut expander = Expander {
883        patterns: &trees,
884        limits,
885        events: 0,
886        calls: Vec::new(),
887    };
888    for (name, pos, nodes) in &channel_trees {
889        let mut writer = score.channel(name).map_err(|e| pos.wrap(e))?;
890        let mut state = TextState {
891            octave: 4,
892            length: Length::QUARTER,
893            last_pitch: None,
894            tie: None,
895        };
896        expander.run(nodes, &mut writer, &mut state, 0)?;
897        if let Some(tie) = state.tie {
898            return Err(tie.syntax("'&' must be followed by a note"));
899        }
900    }
901    Ok(score)
902}