Skip to main content

sva_core/
until.rs

1// Concern: parses a stop condition into the engine's Until | Non-concern: where it holds (sva-engine), the expression grammar | IO: (text, rate, tempo) -> Until or CliError
2
3use sva_ast::{LogUnit, SpanUnit, Token, TokenKind, tokenize};
4use sva_engine::{Cmp, Term, Until};
5
6use crate::cli_error::CliError;
7
8/// `t` in seconds is compared with times, a level with levels; `and` binds tighter than `or`.
9pub fn until(text: &str, rate: u32, seconds_per_bar: Option<f64>) -> Result<Until, CliError> {
10    let tokens = tokenize(text).map_err(|d| refused(text, &d.message))?;
11    let mut reading = Reading {
12        text,
13        tokens: &tokens,
14        at: 0,
15        rate,
16        seconds_per_bar,
17    };
18    let held = reading.either()?;
19    if reading.at != tokens.len() {
20        return Err(reading.wrong("the condition ended"));
21    }
22    held.checked().map_err(|why| refused(text, &why))?;
23    Ok(held)
24}
25
26struct Reading<'a> {
27    text: &'a str,
28    tokens: &'a [Token],
29    at: usize,
30    rate: u32,
31    seconds_per_bar: Option<f64>,
32}
33
34impl Reading<'_> {
35    fn peek(&self) -> Option<&TokenKind> {
36        self.tokens.get(self.at).map(|t| &t.kind)
37    }
38
39    fn word(&self, word: &str) -> bool {
40        matches!(self.peek(), Some(TokenKind::Ident(w)) if w == word)
41    }
42
43    fn take(&mut self, kind: &TokenKind) -> Result<(), CliError> {
44        match self.peek() {
45            Some(k) if k == kind => {
46                self.at += 1;
47                Ok(())
48            }
49            _ => Err(self.wrong(&format!("`{}`", spelled(kind)))),
50        }
51    }
52
53    fn either(&mut self) -> Result<Until, CliError> {
54        let mut held = self.both()?;
55        while self.word("or") {
56            self.at += 1;
57            held = Until::Any(Box::new(held), Box::new(self.both()?));
58        }
59        Ok(held)
60    }
61
62    fn both(&mut self) -> Result<Until, CliError> {
63        let mut held = self.one()?;
64        while self.word("and") {
65            self.at += 1;
66            held = Until::All(Box::new(held), Box::new(self.one()?));
67        }
68        Ok(held)
69    }
70
71    fn one(&mut self) -> Result<Until, CliError> {
72        if self.peek() == Some(&TokenKind::LParen) {
73            self.at += 1;
74            let held = self.either()?;
75            self.take(&TokenKind::RParen)?;
76            return Ok(held);
77        }
78        let from = self.at;
79        let left = self.term()?;
80        let left_bare = self.bare_number(from);
81        let cmp = match self.peek() {
82            Some(TokenKind::Lt) => Cmp::Lt,
83            Some(TokenKind::Le) => Cmp::Le,
84            Some(TokenKind::Gt) => Cmp::Gt,
85            Some(TokenKind::Ge) => Cmp::Ge,
86            _ => return Err(self.wrong("a comparison, `<`, `<=`, `>` or `>=`")),
87        };
88        self.at += 1;
89        let from = self.at;
90        let right = self.term()?;
91        let bare = match (level(&left), level(&right)) {
92            (true, false) => self.bare_number(from),
93            (false, true) => left_bare,
94            _ => None,
95        };
96        if let Some((start, end)) = bare {
97            return Err(refused(
98                self.text,
99                &format!(
100                    "`{}` (at bytes {start}..{end}) is compared with a level and has no level \
101                     unit; write it in `db`",
102                    &self.text[start..end]
103                ),
104            ));
105        }
106        Ok(Until::Holds(left, cmp, right))
107    }
108
109    /// The bytes of the number `term` read from token `from` on, where it was written
110    /// with no unit; `0` is zero in every unit.
111    fn bare_number(&self, from: usize) -> Option<(usize, usize)> {
112        let read = &self.tokens[from..self.at];
113        let unitless = read
114            .iter()
115            .any(|t| matches!(t.kind, TokenKind::Num(n) if n != 0.0));
116        match (unitless, read.first(), read.last()) {
117            (true, Some(first), Some(last)) => Some((first.span.start, last.span.end)),
118            _ => None,
119        }
120    }
121
122    fn term(&mut self) -> Result<Term, CliError> {
123        if self.word("t") {
124            self.at += 1;
125            return Ok(Term::Time);
126        }
127        if self.word("envelope") {
128            self.at += 1;
129            self.take(&TokenKind::LParen)?;
130            if !self.word("t") {
131                return Err(self.wrong("`t`"));
132            }
133            self.at += 1;
134            self.take(&TokenKind::RParen)?;
135            return Ok(Term::Envelope);
136        }
137        self.number().map(Term::Number)
138    }
139
140    /// A time in seconds or a level in `db`, a minus before it allowed; `0` needs no unit.
141    fn number(&mut self) -> Result<f64, CliError> {
142        let sign = match self.peek() {
143            Some(TokenKind::Minus) => {
144                self.at += 1;
145                -1.0
146            }
147            _ => 1.0,
148        };
149        let value = match self.peek() {
150            Some(TokenKind::Num(n)) => sign * n,
151            Some(TokenKind::Time(n, SpanUnit::Seconds)) => sign * n,
152            Some(TokenKind::Samples(n)) => sign * n / f64::from(self.rate),
153            Some(TokenKind::Time(n, SpanUnit::Bars)) => match self.seconds_per_bar {
154                Some(per) => sign * n * per,
155                None => {
156                    return Err(CliError::BadTempo(format!(
157                        "`{}` is written in bars and nothing here declares a tempo; state \
158                         `variables/bpm` and `variables/meter`, or write seconds",
159                        self.text
160                    )));
161                }
162            },
163            Some(TokenKind::Log(n, LogUnit::Decibels)) => LogUnit::Decibels.resolve(sign * n),
164            _ => return Err(self.wrong("a number, a time or a level")),
165        };
166        self.at += 1;
167        Ok(value)
168    }
169
170    fn wrong(&self, wanted: &str) -> CliError {
171        let found = self.tokens.get(self.at).map_or("the end".to_string(), |t| {
172            format!("`{}`", &self.text[t.span.start..t.span.end])
173        });
174        refused(self.text, &format!("expected {wanted}, found {found}"))
175    }
176}
177
178fn level(term: &Term) -> bool {
179    matches!(term, Term::Envelope)
180}
181
182fn spelled(kind: &TokenKind) -> &'static str {
183    match kind {
184        TokenKind::LParen => "(",
185        TokenKind::RParen => ")",
186        _ => "?",
187    }
188}
189
190fn refused(text: &str, why: &str) -> CliError {
191    CliError::Usage(format!(
192        "`{text}` is no condition: {why}. write comparisons over `t` and `envelope(t)`, \
193         joined by `and`/`or`, as `envelope(t) < -60db and t > 1s`"
194    ))
195}