1use sva_ast::{Token, TokenKind, tokenize};
4
5use crate::cli_error::CliError;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum Edge {
10 Secs(f64),
11 Bars(f64),
12 Samples(f64),
13 Inf,
14}
15
16impl Edge {
17 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#[derive(Clone, Debug, PartialEq)]
40pub struct Target {
41 pub expr: String,
42 pub interval: Option<(Edge, Edge)>,
43}
44
45pub 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
61fn 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
87fn 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
167fn 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
190fn 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}