Skip to main content

sva_core/
target.rs

1// Concern: splits a target into the expression it renders and the interval its ref reads | Non-concern: the expression grammar (sva-ast), where an open interval ends | IO: (text) -> Target
2
3use sva_ast::{Token, TokenKind, tokenize};
4
5use crate::cli_error::CliError;
6
7/// One end of an interval, in the units it was written in; a bare zero is zero in all of them.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum Edge {
10    Secs(f64),
11    Bars(f64),
12    Samples(f64),
13    Inf,
14}
15
16impl Edge {
17    /// The grid sample this edge falls on; `None` where it is no edge at all.
18    pub fn sample(self, rate: u32, seconds_per_bar: Option<f64>) -> Result<Option<i64>, CliError> {
19        let secs = match self {
20            Edge::Inf => return Ok(None),
21            Edge::Samples(n) => return Ok(Some(n.round() as i64)),
22            Edge::Secs(secs) => secs,
23            Edge::Bars(bars) => match seconds_per_bar {
24                Some(per) => bars * per,
25                None => {
26                    return Err(CliError::BadTempo(format!(
27                        "the interval is written in bars and nothing here declares a tempo; \
28                         state `variables/bpm` and `variables/meter`, or write seconds, as \
29                         `{bars}s`"
30                    )));
31                }
32            },
33        };
34        Ok(Some((secs * f64::from(rate)).round() as i64))
35    }
36}
37
38/// The expression with its interval read as `t`, and that interval's two ends.
39#[derive(Clone, Debug, PartialEq)]
40pub struct Target {
41    pub expr: String,
42    pub interval: Option<(Edge, Edge)>,
43}
44
45/// An interval is the argument of the target's own ref, `@a([0, 2b], vel=0.5)`: nowhere else.
46/// A `[` right after a ref or `self` reads an index, which the expression grammar owns.
47pub fn target(text: &str) -> Result<Target, CliError> {
48    let tokens = tokenize(text).map_err(|d| unparsed(text, &d.message))?;
49    let indexes = |at: usize| {
50        at > 0
51            && match &tokens[at - 1].kind {
52                TokenKind::Ref(_) => true,
53                TokenKind::Ident(name) => name == "self",
54                _ => false,
55            }
56    };
57    let opens: Vec<usize> = tokens
58        .iter()
59        .enumerate()
60        .filter(|(at, t)| t.kind == TokenKind::LBracket && !indexes(*at))
61        .map(|(at, _)| at)
62        .collect();
63    let at = match opens.as_slice() {
64        [] => {
65            return Ok(Target {
66                expr: text.to_string(),
67                interval: None,
68            });
69        }
70        [at] => *at,
71        _ => return Err(misplaced(text)),
72    };
73    let own_ref = matches!(
74        tokens.as_slice(),
75        [
76            Token {
77                kind: TokenKind::Ref(_),
78                ..
79            },
80            Token {
81                kind: TokenKind::LParen,
82                ..
83            },
84            ..
85        ]
86    );
87    if at != 2 || !own_ref {
88        return Err(misplaced(text));
89    }
90    let comma = position(&tokens, at, |k| *k == TokenKind::Comma).ok_or_else(|| misplaced(text))?;
91    let close = position(&tokens, comma, |k| {
92        matches!(k, TokenKind::RBracket | TokenKind::RParen)
93    })
94    .ok_or_else(|| misplaced(text))?;
95    if closing(&tokens, close) != Some(tokens.len() - 1)
96        || !matches!(
97            tokens.get(close + 1).map(|t| &t.kind),
98            Some(TokenKind::Comma | TokenKind::RParen)
99        )
100    {
101        return Err(misplaced(text));
102    }
103    let start = edge(text, &tokens[at + 1..comma])?;
104    let end = match &tokens[comma + 1..close] {
105        [] => Edge::Inf,
106        written => edge(text, written)?,
107    };
108    if start == Edge::Inf {
109        return Err(CliError::Usage(format!(
110            "`{text}` starts its interval at inf; start it at a time, as `[0, inf)`"
111        )));
112    }
113    let expr = format!(
114        "{}t{}",
115        &text[..tokens[at].span.start],
116        &text[tokens[close].span.end..]
117    );
118    Ok(Target {
119        expr,
120        interval: Some((start, end)),
121    })
122}
123
124/// The index of the `)` closing the call the second token opens, the interval's own end
125/// at `interval` aside.
126fn closing(tokens: &[Token], interval: usize) -> Option<usize> {
127    let mut depth = 0i32;
128    for (at, token) in tokens.iter().enumerate().skip(1) {
129        match token.kind {
130            TokenKind::LParen => depth += 1,
131            TokenKind::RParen if at != interval => {
132                depth -= 1;
133                if depth == 0 {
134                    return Some(at);
135                }
136            }
137            _ => {}
138        }
139    }
140    None
141}
142
143fn position(tokens: &[Token], from: usize, found: impl Fn(&TokenKind) -> bool) -> Option<usize> {
144    (from + 1..tokens.len()).find(|at| found(&tokens[*at].kind))
145}
146
147/// `inf`, or one literal of time, a minus before it allowed; `0` needs no unit.
148fn edge(text: &str, written: &[Token]) -> Result<Edge, CliError> {
149    let (sign, rest) = match written {
150        [
151            Token {
152                kind: TokenKind::Minus,
153                ..
154            },
155            rest @ ..,
156        ] => (-1.0, rest),
157        rest => (1.0, rest),
158    };
159    Ok(match rest {
160        [
161            Token {
162                kind: TokenKind::Ident(inf),
163                ..
164            },
165        ] if inf == "inf" && sign > 0.0 => Edge::Inf,
166        [
167            Token {
168                kind: TokenKind::Num(n),
169                ..
170            },
171        ] if *n == 0.0 => Edge::Secs(0.0),
172        [
173            Token {
174                kind: TokenKind::Time(n, sva_ast::SpanUnit::Seconds),
175                ..
176            },
177        ] => Edge::Secs(sign * n),
178        [
179            Token {
180                kind: TokenKind::Time(n, sva_ast::SpanUnit::Bars),
181                ..
182            },
183        ] => Edge::Bars(sign * n),
184        [
185            Token {
186                kind: TokenKind::Samples(n),
187                ..
188            },
189        ] => Edge::Samples(sign * n),
190        _ => {
191            let spelled = written
192                .first()
193                .zip(written.last())
194                .map_or("", |(a, b)| &text[a.span.start..b.span.end]);
195            return Err(CliError::Usage(format!(
196                "`{spelled}` is no interval edge: write a time with its unit, as `2s`, \
197                 `500ms`, `2b` or `88200sp`, or `inf`"
198            )));
199        }
200    })
201}
202
203fn misplaced(text: &str) -> CliError {
204    CliError::Usage(format!(
205        "`{text}` holds an interval outside its own ref's argument; write one ref read over \
206         it, as `@a([0, 2b], vel=0.5)`"
207    ))
208}
209
210fn unparsed(text: &str, why: &str) -> CliError {
211    CliError::BadProbe(format!("`{text}` does not parse as a target: {why}"))
212}