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)`, or of the one
46/// ref it samples, `sample(@a([0, 2b]))`: nowhere else.
47pub fn target(text: &str) -> Result<Target, CliError> {
48    let tokens = tokenize(text).map_err(|d| unparsed(text, &d.message))?;
49    if let Some(inner) = sampled(text, &tokens) {
50        let read = own_read(inner, text)?;
51        if read.interval.is_some() {
52            return Ok(Target {
53                expr: format!("sample({})", read.expr),
54                interval: read.interval,
55            });
56        }
57    }
58    own_read(text, text)
59}
60
61/// What a target that is one `sample(@...)` call holds.
62fn sampled<'t>(text: &'t str, tokens: &[Token]) -> Option<&'t str> {
63    match tokens {
64        [
65            Token {
66                kind: TokenKind::Ident(name),
67                ..
68            },
69            open @ Token {
70                kind: TokenKind::LParen,
71                ..
72            },
73            Token {
74                kind: TokenKind::Ref(_),
75                ..
76            },
77            ..,
78            close @ Token {
79                kind: TokenKind::RParen,
80                ..
81            },
82        ] if name == "sample" => Some(&text[open.span.end..close.span.start]),
83        _ => None,
84    }
85}
86
87/// `@a([0, 2b], vel=0.5)`, or no interval at all. A `[` right after a ref or `self` reads an
88/// index, which the expression grammar owns.
89fn own_read(text: &str, written: &str) -> Result<Target, CliError> {
90    let tokens = tokenize(text).map_err(|d| unparsed(written, &d.message))?;
91    let indexes = |at: usize| {
92        at > 0
93            && match &tokens[at - 1].kind {
94                TokenKind::Ref(_) => true,
95                TokenKind::Ident(name) => name == "self",
96                _ => false,
97            }
98    };
99    let opens: Vec<usize> = tokens
100        .iter()
101        .enumerate()
102        .filter(|(at, t)| t.kind == TokenKind::LBracket && !indexes(*at))
103        .map(|(at, _)| at)
104        .collect();
105    let at = match opens.as_slice() {
106        [] => {
107            return Ok(Target {
108                expr: text.to_string(),
109                interval: None,
110            });
111        }
112        [at] => *at,
113        _ => return Err(misplaced(written)),
114    };
115    let own_ref = matches!(
116        tokens.as_slice(),
117        [
118            Token {
119                kind: TokenKind::Ref(_),
120                ..
121            },
122            Token {
123                kind: TokenKind::LParen,
124                ..
125            },
126            ..
127        ]
128    );
129    if at != 2 || !own_ref {
130        return Err(misplaced(written));
131    }
132    let comma =
133        position(&tokens, at, |k| *k == TokenKind::Comma).ok_or_else(|| misplaced(written))?;
134    let close = position(&tokens, comma, |k| {
135        matches!(k, TokenKind::RBracket | TokenKind::RParen)
136    })
137    .ok_or_else(|| misplaced(written))?;
138    if closing(&tokens, close) != Some(tokens.len() - 1)
139        || !matches!(
140            tokens.get(close + 1).map(|t| &t.kind),
141            Some(TokenKind::Comma | TokenKind::RParen)
142        )
143    {
144        return Err(misplaced(written));
145    }
146    let start = edge(text, &tokens[at + 1..comma])?;
147    let end = match &tokens[comma + 1..close] {
148        [] => Edge::Inf,
149        ends => edge(text, ends)?,
150    };
151    if start == Edge::Inf {
152        return Err(CliError::Usage(format!(
153            "`{written}` starts its interval at inf; start it at a time, as `[0, inf)`"
154        )));
155    }
156    let expr = format!(
157        "{}t{}",
158        &text[..tokens[at].span.start],
159        &text[tokens[close].span.end..]
160    );
161    Ok(Target {
162        expr,
163        interval: Some((start, end)),
164    })
165}
166
167/// The index of the `)` closing the call the second token opens, the interval's own end
168/// at `interval` aside.
169fn closing(tokens: &[Token], interval: usize) -> Option<usize> {
170    let mut depth = 0i32;
171    for (at, token) in tokens.iter().enumerate().skip(1) {
172        match token.kind {
173            TokenKind::LParen => depth += 1,
174            TokenKind::RParen if at != interval => {
175                depth -= 1;
176                if depth == 0 {
177                    return Some(at);
178                }
179            }
180            _ => {}
181        }
182    }
183    None
184}
185
186fn position(tokens: &[Token], from: usize, found: impl Fn(&TokenKind) -> bool) -> Option<usize> {
187    (from + 1..tokens.len()).find(|at| found(&tokens[*at].kind))
188}
189
190/// `inf`, or one literal of time, a minus before it allowed; `0` needs no unit.
191fn edge(text: &str, written: &[Token]) -> Result<Edge, CliError> {
192    let (sign, rest) = match written {
193        [
194            Token {
195                kind: TokenKind::Minus,
196                ..
197            },
198            rest @ ..,
199        ] => (-1.0, rest),
200        rest => (1.0, rest),
201    };
202    Ok(match rest {
203        [
204            Token {
205                kind: TokenKind::Ident(inf),
206                ..
207            },
208        ] if inf == "inf" && sign > 0.0 => Edge::Inf,
209        [
210            Token {
211                kind: TokenKind::Num(n),
212                ..
213            },
214        ] if *n == 0.0 => Edge::Secs(0.0),
215        [
216            Token {
217                kind: TokenKind::Time(n, sva_ast::SpanUnit::Seconds),
218                ..
219            },
220        ] => Edge::Secs(sign * n),
221        [
222            Token {
223                kind: TokenKind::Time(n, sva_ast::SpanUnit::Bars),
224                ..
225            },
226        ] => Edge::Bars(sign * n),
227        [
228            Token {
229                kind: TokenKind::Samples(n),
230                ..
231            },
232        ] => Edge::Samples(sign * n),
233        _ => {
234            let spelled = written
235                .first()
236                .zip(written.last())
237                .map_or("", |(a, b)| &text[a.span.start..b.span.end]);
238            return Err(CliError::Usage(format!(
239                "`{spelled}` is no interval edge: write a time with its unit, as `2s`, \
240                 `500ms`, `2b` or `88200sp`, or `inf`"
241            )));
242        }
243    })
244}
245
246fn misplaced(text: &str) -> CliError {
247    CliError::Usage(format!(
248        "`{text}` holds an interval outside its own ref's argument; write one ref read over \
249         it, as `@a([0, 2b], vel=0.5)`, or sample that one read, as \
250         `sample(@a([0, 2b], vel=0.5))`"
251    ))
252}
253
254fn unparsed(text: &str, why: &str) -> CliError {
255    CliError::BadProbe(format!("`{text}` does not parse as a target: {why}"))
256}