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(
153            "target.bad_interval",
154            format!("`{written}` starts its interval at inf; start it at a time, as `[0, inf)`"),
155        ));
156    }
157    let expr = format!(
158        "{}t{}",
159        &text[..tokens[at].span.start],
160        &text[tokens[close].span.end..]
161    );
162    Ok(Target {
163        expr,
164        interval: Some((start, end)),
165    })
166}
167
168/// The index of the `)` closing the call the second token opens, the interval's own end
169/// at `interval` aside.
170fn closing(tokens: &[Token], interval: usize) -> Option<usize> {
171    let mut depth = 0i32;
172    for (at, token) in tokens.iter().enumerate().skip(1) {
173        match token.kind {
174            TokenKind::LParen => depth += 1,
175            TokenKind::RParen if at != interval => {
176                depth -= 1;
177                if depth == 0 {
178                    return Some(at);
179                }
180            }
181            _ => {}
182        }
183    }
184    None
185}
186
187fn position(tokens: &[Token], from: usize, found: impl Fn(&TokenKind) -> bool) -> Option<usize> {
188    (from + 1..tokens.len()).find(|at| found(&tokens[*at].kind))
189}
190
191/// `inf`, or one literal of time, a minus before it allowed; `0` needs no unit.
192fn edge(text: &str, written: &[Token]) -> Result<Edge, CliError> {
193    let (sign, rest) = match written {
194        [
195            Token {
196                kind: TokenKind::Minus,
197                ..
198            },
199            rest @ ..,
200        ] => (-1.0, rest),
201        rest => (1.0, rest),
202    };
203    Ok(match rest {
204        [
205            Token {
206                kind: TokenKind::Ident(inf),
207                ..
208            },
209        ] if inf == "inf" && sign > 0.0 => Edge::Inf,
210        [
211            Token {
212                kind: TokenKind::Num(n),
213                ..
214            },
215        ] if *n == 0.0 => Edge::Secs(0.0),
216        [
217            Token {
218                kind: TokenKind::Time(n, sva_ast::SpanUnit::Seconds),
219                ..
220            },
221        ] => Edge::Secs(sign * n),
222        [
223            Token {
224                kind: TokenKind::Time(n, sva_ast::SpanUnit::Bars),
225                ..
226            },
227        ] => Edge::Bars(sign * n),
228        [
229            Token {
230                kind: TokenKind::Samples(n),
231                ..
232            },
233        ] => Edge::Samples(sign * n),
234        _ => {
235            let spelled = written
236                .first()
237                .zip(written.last())
238                .map_or("", |(a, b)| &text[a.span.start..b.span.end]);
239            return Err(CliError::usage(
240                "target.bad_interval",
241                format!(
242                    "`{spelled}` is no interval edge: write a time with its unit, as `2s`, \
243                 `500ms`, `2b` or `88200sp`, or `inf`"
244                ),
245            ));
246        }
247    })
248}
249
250fn misplaced(text: &str) -> CliError {
251    CliError::usage(
252        "target.bad_interval",
253        format!(
254            "`{text}` holds an interval outside its own ref's argument; write one ref read over \
255         it, as `@a([0, 2b], vel=0.5)`, or sample that one read, as \
256         `sample(@a([0, 2b], vel=0.5))`"
257        ),
258    )
259}
260
261fn unparsed(text: &str, why: &str) -> CliError {
262    CliError::BadProbe(format!("`{text}` does not parse as a target: {why}"))
263}