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 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
124fn 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
147fn 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}