1mod answer;
4mod builtins;
5mod cli_error;
6pub mod encode;
7pub mod json;
8mod lint_code;
9mod outline;
10mod output;
11mod query;
12mod target;
13mod tempo;
14mod until;
15
16pub use answer::{
17 Printed, Report, SAMPLE_LIMIT, answer_json, label_json, query_data, stats_json,
18 stream_stats_json, value_json, work_json,
19};
20pub use builtins::{Builtins, Callable, Crossing, builtins, builtins_data};
21pub use cli_error::{CliError, LintViolation, lint_diagnostic};
22pub use lint_code::LintCode;
23pub use outline::outline_data;
24pub use output::{Diagnostic, Severity, diagnostics_json, error_envelope, success_envelope};
25pub use query::{
26 Asked, Call, DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PEAKS, DEFAULT_OVERSAMPLE, REPRESENTATIONS,
27 RETIRED, asked, call, calls, is_wav, retired, seconds, wav_path,
28};
29pub use target::{Edge, Target, target};
30pub use tempo::{Tempo, refuse_unresolved_bars, resolved as tempo};
31pub use until::until;
32
33use std::path::Path;
34
35use sva_ast::{Dir, Graph, Refusal, Source};
36use sva_engine::{
37 Ask, DEFAULT_SAMPLE_RATE, EngineError, Range, RenderConfig, StreamConfig, render,
38 render_through,
39};
40
41pub use sva_engine::{Handle, QuietTail, Stream, Until};
42
43pub use sva_engine::{Answer, Extent, Label, Output, Representation};
44pub use sva_engine::{
45 Backend, Cache, CachePolicy, DEFAULT_STORE_BYTES, NoStore, Persisted, PrunePolicy, Store,
46 Through,
47};
48
49pub const ROOT: &str = "master";
50pub const PROBE: &str = "probe";
51
52pub struct Rendered {
53 pub config: RenderConfig,
54 pub expression: String,
55 pub target: String,
56 pub render: sva_engine::Render,
57 pub graph: Graph,
59}
60
61impl Rendered {
62 pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
63 let written = |e| CliError::Engine(as_written(e, &self.expression));
64 let id = self.render.node(node).map_err(written)?;
65 let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
66 self.attribute(&mut answer)?;
67 Ok(answer)
68 }
69
70 fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
72 let Output::Alias(alias) = &mut answer.value else {
73 return Ok(());
74 };
75 alias.instances = self.render.tys.paths().count();
76 Ok(())
77 }
78
79 pub fn label(&self) -> Option<&sva_engine::Label> {
80 self.render.labels.get(&self.render.root)
81 }
82}
83
84pub struct Job<'a> {
87 pub source: &'a dyn Source,
88 pub target: &'a str,
89 pub until: Option<&'a str>,
91 pub rate: Option<u32>,
92 pub bits: Option<i32>,
93 pub cache: Option<&'a Cache>,
94 pub cache_policy: Option<CachePolicy>,
95 pub asked: &'a [Asked],
96 pub flop_budget: Option<u128>,
98 pub volatile: &'a [String],
99}
100
101impl<'a> Job<'a> {
102 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
103 Job {
104 source,
105 target,
106 until: None,
107 rate: None,
108 bits: None,
109 cache: None,
110 cache_policy: None,
111 asked: &[],
112 flop_budget: None,
113 volatile: &[],
114 }
115 }
116}
117
118fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
119 settle_reaching(job, &[])
120}
121
122fn settle_reaching(job: &Job, also: &[String]) -> Result<(Graph, RenderConfig), CliError> {
123 let Target { expr, interval } = target(job.target)?;
124 let mut roots = roots_of(job.source, &expr)?;
125 roots.extend_from_slice(also);
126 let mut graph = settled(sva_ast::load_reaching(
127 job.source,
128 &roots.iter().map(String::as_str).collect::<Vec<_>>(),
129 ))?;
130 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
131 CliError::BadProbe(format!(
132 "`{}` does not parse as an expression: {}",
133 job.target, d.message
134 ))
135 })?;
136 define_probe(&mut graph, parsed)?;
137 let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
138 let per_bar = graph.seconds_per_bar();
139 let range = match interval {
140 None => Range::default(),
141 Some((start, end)) => Range {
142 start: start.sample(rate, per_bar)?,
143 end: end.sample(rate, per_bar)?,
144 },
145 };
146 if let Range {
147 start: Some(start),
148 end: Some(end),
149 } = range
150 && end <= start
151 {
152 return Err(CliError::Usage(format!(
153 "`{}` reads an interval from sample {start} to {end}, which holds none",
154 job.target
155 )));
156 }
157 let until = match job.until {
158 Some(text) => Some(until(text, rate, per_bar)?),
159 None => None,
160 };
161 let mut config = RenderConfig {
162 range,
163 until,
164 ..RenderConfig::at(rate)
165 };
166 if let Some(budget) = job.flop_budget {
167 config.flop_budget = budget;
168 }
169 if let Some(bits) = job.bits {
170 config.profile.precision_bits = precision(bits)?;
171 }
172 config.volatile = job.volatile.to_vec();
173 config.cache_policy = job.cache_policy;
174 config.asks = job
175 .asked
176 .iter()
177 .map(|asked| Ask {
178 node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
179 representation: asked.representation,
180 })
181 .collect();
182 Ok((graph, config))
183}
184
185fn instance_call(text: &str) -> Option<(&str, &str)> {
187 let (path, rest) = text.split_once('(')?;
188 let binds = rest.strip_suffix(')')?.trim();
189 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
190}
191
192fn all_named(binds: &str) -> bool {
194 let mut depth = 0i32;
195 let mut named = false;
196 for c in binds.chars() {
197 match c {
198 '(' => depth += 1,
199 ')' if depth == 0 => return false,
200 ')' => depth -= 1,
201 '=' if depth == 0 => named = true,
202 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
203 _ => {}
204 }
205 }
206 named && depth == 0
207}
208
209fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
211 let (path, binds) = instance_call(text)?;
212 if !graph.defines(path) {
213 return None;
214 }
215 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
216}
217
218pub fn execute(job: Job) -> Result<Rendered, CliError> {
219 let (graph, config) = settle(&job)?;
220 let render = render(&graph, PROBE, config, job.cache);
221 rendered(&job, graph, render)
222}
223
224pub async fn execute_through<B: Backend>(
226 job: Job<'_>,
227 store: &Store<B>,
228) -> Result<Rendered, CliError> {
229 let (graph, config) = settle(&job)?;
230 let render = render_through(&graph, PROBE, config, store).await;
231 rendered(&job, graph, render)
232}
233
234fn rendered(
235 job: &Job,
236 graph: Graph,
237 render: Result<sva_engine::Render, EngineError>,
238) -> Result<Rendered, CliError> {
239 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
240 Ok(Rendered {
241 config: render.config.clone(),
242 expression: job.target.to_string(),
243 target: PROBE.to_string(),
244 render,
245 graph,
246 })
247}
248
249pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
250 let (graph, config) = settle(job)?;
251 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
252}
253
254pub fn types(job: &Job) -> Result<(), CliError> {
256 let (graph, config) = settle(job)?;
257 sva_engine::types_at(&graph, PROBE, config.rate)
258 .map(|_| ())
259 .map_err(|e| CliError::Engine(as_written(e, job.target)))
260}
261
262pub fn quiet_tails(job: &Job) -> Result<Vec<QuietTail>, CliError> {
264 let (graph, config) = settle(job)?;
265 let found = sva_engine::quiet_tails(&graph, PROBE, config)
266 .map_err(|e| CliError::Engine(as_written(e, job.target)))?;
267 Ok(found.into_iter().filter(|t| t.file != PROBE).collect())
268}
269
270pub async fn stream(job: &Job<'_>, block: usize, store: &impl Through) -> Result<Stream, CliError> {
271 let (graph, config) = settle(job)?;
272 let target = graph
273 .expr(PROBE)
274 .cloned()
275 .expect("the target was defined as the probe");
276 let config = StreamConfig {
277 block,
278 render: config,
279 };
280 Stream::open(&graph, &target, config, job.cache, store)
281 .await
282 .map_err(|e| CliError::Engine(as_written(e, job.target)))
283}
284
285pub async fn edit(
288 stream: &mut Stream,
289 source: &dyn Source,
290 target: &str,
291 store: &impl Through,
292) -> Result<(), CliError> {
293 let (graph, expr) = streamed(stream, source, target)?;
294 stream
295 .edit(&graph, &expr, store)
296 .await
297 .map_err(|e| CliError::Engine(as_written(e, target)))
298}
299
300pub async fn add(
302 stream: &mut Stream,
303 source: &dyn Source,
304 term: &str,
305 store: &impl Through,
306) -> Result<Handle, CliError> {
307 let (graph, expr) = streamed(stream, source, term)?;
308 stream
309 .add(&graph, &expr, store)
310 .await
311 .map_err(|e| CliError::Engine(as_written(e, term)))
312}
313
314pub async fn replace(
316 stream: &mut Stream,
317 source: &dyn Source,
318 (handle, term): (Handle, &str),
319 store: &impl Through,
320) -> Result<bool, CliError> {
321 let (graph, expr) = streamed(stream, source, term)?;
322 stream
323 .replace(&graph, (handle, &expr), store)
324 .await
325 .map_err(|e| CliError::Engine(as_written(e, term)))
326}
327
328fn streamed(
330 stream: &Stream,
331 source: &dyn Source,
332 text: &str,
333) -> Result<(Graph, sva_ast::Expr), CliError> {
334 let playing: Vec<String> = stream
335 .exprs()
336 .flat_map(|e| sva_ast::reads_of(PROBE, e))
337 .collect();
338 let (graph, config) = settle_reaching(&Job::over(source, text), &playing)?;
339 if config.range != Range::default() {
340 return Err(CliError::Usage(format!(
341 "`{text}` reads an interval, and a stream keeps its own"
342 )));
343 }
344 let expr = graph
345 .expr(PROBE)
346 .cloned()
347 .expect("the target was defined as the probe");
348 Ok((graph, expr))
349}
350
351fn precision(bits: i32) -> Result<i32, CliError> {
353 match (2..=52).contains(&bits) {
354 true => Ok(bits),
355 false => Err(CliError::Usage(format!(
356 "`--bits` takes a precision from 2 to 52 bits, not {bits}"
357 ))),
358 }
359}
360
361fn as_written(refused: EngineError, target: &str) -> EngineError {
364 match refused {
365 EngineError::Refused(mut d) => {
366 if d.location.node == PROBE {
367 d.location.node = target.to_string();
368 }
369 d.message = d
370 .message
371 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
372 EngineError::Refused(d)
373 }
374 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
375 node: target.to_string(),
376 span,
377 fault,
378 },
379 other => other,
380 }
381}
382
383pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
384 execute(Job::over(&Dir::at(dir), expression))
385}
386
387pub fn cwd() -> Result<std::path::PathBuf, CliError> {
388 std::env::current_dir()
389 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
390}
391
392pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
393 settled(sva_ast::load(source))
394}
395
396pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
399 let expr = match instance_read(graph, text) {
400 Some(read) => read,
401 None if names_a_missing_node(text) => {
402 return Err(CliError::NotFound(format!(
403 "`{text}` is not a node this composition defines"
404 )));
405 }
406 None => sva_ast::parse_expr(text).map_err(|d| {
407 CliError::BadProbe(format!(
408 "`{text}` is not a node in this composition, and does not parse as an \
409 expression: {}",
410 d.message
411 ))
412 })?,
413 };
414 define_probe(graph, expr)
415}
416
417fn names_a_missing_node(text: &str) -> bool {
420 match instance_call(text) {
421 Some((path, _)) => sva_ast::names_a_node(path),
422 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
423 }
424}
425
426fn reads_as_a_value(text: &str) -> bool {
427 match sva_ast::parse_expr(text) {
428 Ok(sva_ast::Expr::Lit(_)) => true,
429 Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
430 _ => false,
431 }
432}
433
434fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
436 let defined = graph
437 .define_arranged(PROBE, expr)
438 .map_err(|r| CliError::Refusals(vec![r]))?;
439 if !defined {
440 return Err(CliError::BadProbe(format!(
441 "this composition already has a node named `{PROBE}`"
442 )));
443 }
444 tempo::refuse_unresolved_bars(graph)
445}
446
447pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
448 let mut graph = loaded.map_err(CliError::Refusals)?;
449 tempo::resolve(&mut graph)?;
450 graph.desugar_arrangement().map_err(CliError::Refusals)?;
451 Ok(graph)
452}
453
454pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
456
457pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
460 let mut roots: Vec<String> = RESERVED_VARIABLES
461 .iter()
462 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
463 .collect();
464 let held = |path: &str| -> Result<bool, CliError> {
465 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
466 };
467 match target {
468 target if held(target)? => {
469 roots.push(target.to_string());
470 }
471 text => match instance_call(text).map(|(path, _)| path) {
472 Some(path) if held(path)? => {
473 roots.push(path.to_string());
474 }
475 _ => {
476 if let Ok(expr) = sva_ast::parse_expr(text) {
477 roots.extend(sva_ast::reads_of(PROBE, &expr));
478 }
479 }
480 },
481 }
482 Ok(roots)
483}