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, counters_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::cell::RefCell;
34use std::num::NonZeroUsize;
35use std::path::Path;
36
37use sva_ast::{Dir, Graph, Refusal, Source};
38use sva_engine::{
39 Ask, Change, Changed, DEFAULT_SAMPLE_RATE, EngineError, Range, RenderConfig, StreamConfig,
40 render, render_in,
41};
42
43pub use sva_engine::{Abandon, Handle, Never, Out, Placed, Session, Stream, Until};
44
45pub use sva_engine::{Answer, Extent, Label, Output, Representation};
46pub use sva_engine::{
47 Backend, CacheStats, Counters, DEFAULT_CACHE_BYTES, DEFAULT_STORE_BYTES, FETCH_READS, Nothing,
48 Persisted, Store, Tier,
49};
50
51pub const ROOT: &str = "master";
52pub const PROBE: &str = "probe";
53
54pub struct Rendered {
55 pub config: RenderConfig,
56 pub expression: String,
57 pub target: String,
58 pub render: sva_engine::Render,
59 pub graph: Graph,
61}
62
63impl Rendered {
64 pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
65 let written = |e| CliError::Engine(as_written(e, &self.expression));
66 let id = self.render.node(node).map_err(written)?;
67 let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
68 self.attribute(&mut answer)?;
69 Ok(answer)
70 }
71
72 fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
74 let Output::Alias(alias) = &mut answer.value else {
75 return Ok(());
76 };
77 alias.instances = self.render.tys.paths().count();
78 Ok(())
79 }
80
81 pub fn label(&self) -> Option<&sva_engine::Label> {
82 self.render.labels.get(&self.render.root)
83 }
84}
85
86pub struct Job<'a> {
89 pub source: &'a dyn Source,
90 pub target: &'a str,
91 pub until: Option<&'a str>,
93 pub rate: Option<u32>,
94 pub bits: Option<i32>,
95 pub asked: &'a [Asked],
96 pub volatile: &'a [String],
97 pub out: Out,
98 pub abandon: &'a dyn Abandon,
99 pub threads: Option<NonZeroUsize>,
101}
102
103impl<'a> Job<'a> {
104 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
105 Job {
106 source,
107 target,
108 until: None,
109 rate: None,
110 bits: None,
111 asked: &[],
112 volatile: &[],
113 out: Out::Kept,
114 abandon: &Never,
115 threads: None,
116 }
117 }
118}
119
120fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
121 let Target { expr, interval } = target(job.target)?;
122 let roots = roots_of(job.source, &expr)?;
123 let mut graph = settled(sva_ast::load_reaching(
124 job.source,
125 &roots.iter().map(String::as_str).collect::<Vec<_>>(),
126 ))?;
127 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
128 CliError::BadProbe(format!(
129 "`{}` does not parse as an expression: {}",
130 job.target, d.message
131 ))
132 })?;
133 define_probe(&mut graph, parsed)?;
134 let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
135 let per_bar = graph.seconds_per_bar();
136 let range = match interval {
137 None => Range::default(),
138 Some((start, end)) => Range {
139 start: start.sample(rate, per_bar)?,
140 end: end.sample(rate, per_bar)?,
141 },
142 };
143 if let Range {
144 start: Some(start),
145 end: Some(end),
146 } = range
147 && end <= start
148 {
149 return Err(CliError::Usage(format!(
150 "`{}` reads an interval from sample {start} to {end}, which holds none",
151 job.target
152 )));
153 }
154 let until = match job.until {
155 Some(text) => Some(until(text, rate, per_bar)?),
156 None => None,
157 };
158 let mut config = RenderConfig {
159 range,
160 until,
161 ..RenderConfig::at(rate)
162 };
163 if let Some(bits) = job.bits {
164 config.profile.precision_bits = precision(bits)?;
165 }
166 config.volatile = job.volatile.to_vec();
167 config.out = job.out;
168 if let Some(threads) = job.threads {
169 config.threads = threads;
170 }
171 config.asks = job
172 .asked
173 .iter()
174 .map(|asked| Ask {
175 node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
176 representation: asked.representation,
177 })
178 .collect();
179 Ok((graph, config))
180}
181
182fn instance_call(text: &str) -> Option<(&str, &str)> {
184 let (path, rest) = text.split_once('(')?;
185 let binds = rest.strip_suffix(')')?.trim();
186 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
187}
188
189fn all_named(binds: &str) -> bool {
191 let mut depth = 0i32;
192 let mut named = false;
193 for c in binds.chars() {
194 match c {
195 '(' => depth += 1,
196 ')' if depth == 0 => return false,
197 ')' => depth -= 1,
198 '=' if depth == 0 => named = true,
199 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
200 _ => {}
201 }
202 }
203 named && depth == 0
204}
205
206fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
208 let (path, binds) = instance_call(text)?;
209 if !graph.defines(path) {
210 return None;
211 }
212 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
213}
214
215pub fn execute(job: Job, tier: &Tier) -> Result<Rendered, CliError> {
216 let (graph, config) = settle(&job)?;
217 let render = render(&graph, PROBE, config, tier);
218 rendered(&job, graph, render)
219}
220
221pub async fn execute_over<B: Backend>(
224 job: Job<'_>,
225 tier: &Tier<B>,
226 session: &mut Session,
227) -> Result<Rendered, CliError> {
228 let (graph, config) = settle(&job)?;
229 let render = render_in(session, &graph, PROBE, config, tier, job.abandon).await;
230 rendered(&job, graph, render)
231}
232
233fn rendered(
234 job: &Job,
235 graph: Graph,
236 render: Result<sva_engine::Render, EngineError>,
237) -> Result<Rendered, CliError> {
238 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
239 Ok(Rendered {
240 config: render.config.clone(),
241 expression: job.target.to_string(),
242 target: PROBE.to_string(),
243 render,
244 graph,
245 })
246}
247
248pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
249 let (graph, config) = settle(job)?;
250 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
251}
252
253pub fn types(job: &Job) -> Result<(), CliError> {
255 let (graph, config) = settle(job)?;
256 sva_engine::types_at(&graph, PROBE, config.rate)
257 .map(|_| ())
258 .map_err(|e| CliError::Engine(as_written(e, job.target)))
259}
260
261pub async fn stream<B: Backend>(
263 job: &Job<'_>,
264 (block, channels): (usize, Option<usize>),
265 tier: &Tier<B>,
266) -> Result<Stream, CliError> {
267 let (graph, config) = settle(job)?;
268 let target = graph
269 .expr(PROBE)
270 .cloned()
271 .expect("the target was defined as the probe");
272 let config = StreamConfig {
273 block,
274 channels,
275 render: config,
276 };
277 Stream::open(&graph, &target, config, tier)
278 .await
279 .map_err(|e| CliError::Engine(as_written(e, job.target)))
280}
281
282pub async fn edit<B: Backend>(
285 stream: &RefCell<Stream>,
286 source: &dyn Source,
287 target: &str,
288 tier: &Tier<B>,
289) -> Result<(), CliError> {
290 let build = |s: &Stream| {
291 let (graph, expr) = streamed(s, source, target)?;
292 Ok(Change::Target(graph, expr))
293 };
294 changed(stream, build, Some(target), tier).await.map(|_| ())
295}
296
297pub async fn add<B: Backend>(
299 stream: &RefCell<Stream>,
300 source: &dyn Source,
301 (term, at): (&str, Placed),
302 tier: &Tier<B>,
303) -> Result<Handle, CliError> {
304 let build = |s: &Stream| {
305 let (graph, expr) = streamed(s, source, term)?;
306 Ok(Change::Add(graph, expr, at))
307 };
308 match changed(stream, build, Some(term), tier).await? {
309 Changed::Added(handle) => Ok(handle),
310 _ => unreachable!("an add answers its handle"),
311 }
312}
313
314pub async fn replace<B: Backend>(
316 stream: &RefCell<Stream>,
317 source: &dyn Source,
318 (handle, term, at): (Handle, &str, Placed),
319 tier: &Tier<B>,
320) -> Result<bool, CliError> {
321 let build = |s: &Stream| {
322 let (graph, expr) = streamed(s, source, term)?;
323 Ok(Change::Replace(handle, graph, expr, at))
324 };
325 Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
326}
327
328pub async fn remove<B: Backend>(
330 stream: &RefCell<Stream>,
331 handle: Handle,
332 tier: &Tier<B>,
333) -> Result<bool, CliError> {
334 let build = |_: &Stream| Ok(Change::Remove(handle));
335 Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
336}
337
338async fn changed<B: Backend>(
339 stream: &RefCell<Stream>,
340 build: impl FnMut(&Stream) -> Result<Change, CliError>,
341 text: Option<&str>,
342 tier: &Tier<B>,
343) -> Result<Changed, CliError> {
344 let changed = sva_engine::change(stream, build, tier).await;
345 changed.map_err(|e| match (e, text) {
346 (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
347 (e, _) => e,
348 })
349}
350
351pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
352 sva_engine::fetch(stream, tier).await;
353}
354
355fn streamed(
358 stream: &Stream,
359 source: &dyn Source,
360 text: &str,
361) -> Result<(Graph, sva_ast::Expr), CliError> {
362 let Target { expr, interval } = target(text)?;
363 if interval.is_some() {
364 return Err(CliError::Usage(format!(
365 "`{text}` reads an interval, and a stream keeps its own"
366 )));
367 }
368 let held = stream.graph();
369 let roots = roots_of(source, &expr)?;
370 let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
371 let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
372 if let Some(per_bar) = held.per_bar() {
373 delta.resolve_bar_spans(per_bar);
374 }
375 delta
376 .desugar_arrangement_beside(held)
377 .map_err(CliError::Refusals)?;
378 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
379 CliError::BadProbe(format!(
380 "`{text}` does not parse as an expression: {}",
381 d.message
382 ))
383 })?;
384 let defined = delta
385 .define_arranged_beside(held, PROBE, parsed)
386 .map_err(|r| CliError::Refusals(vec![r]))?;
387 if !defined {
388 return Err(CliError::BadProbe(format!(
389 "this composition already has a node named `{PROBE}`"
390 )));
391 }
392 tempo::refuse_unresolved_bars(&delta)?;
393 let expr = delta
394 .expr(PROBE)
395 .cloned()
396 .expect("the target was defined as the probe");
397 Ok((delta, expr))
398}
399
400fn precision(bits: i32) -> Result<i32, CliError> {
402 match (2..=52).contains(&bits) {
403 true => Ok(bits),
404 false => Err(CliError::Usage(format!(
405 "`--bits` takes a precision from 2 to 52 bits, not {bits}"
406 ))),
407 }
408}
409
410fn as_written(refused: EngineError, target: &str) -> EngineError {
413 match refused {
414 EngineError::Refused(mut d) => {
415 if d.location.node == PROBE {
416 d.location.node = target.to_string();
417 }
418 d.message = d
419 .message
420 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
421 EngineError::Refused(d)
422 }
423 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
424 node: target.to_string(),
425 span,
426 fault,
427 },
428 other => other,
429 }
430}
431
432pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
433 execute(Job::over(&Dir::at(dir), expression), &Tier::default())
434}
435
436pub fn cwd() -> Result<std::path::PathBuf, CliError> {
437 std::env::current_dir()
438 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
439}
440
441pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
442 settled(sva_ast::load(source))
443}
444
445pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
448 let expr = match instance_read(graph, text) {
449 Some(read) => read,
450 None if names_a_missing_node(text) => {
451 return Err(CliError::NotFound(format!(
452 "`{text}` is not a node this composition defines"
453 )));
454 }
455 None => sva_ast::parse_expr(text).map_err(|d| {
456 CliError::BadProbe(format!(
457 "`{text}` is not a node in this composition, and does not parse as an \
458 expression: {}",
459 d.message
460 ))
461 })?,
462 };
463 define_probe(graph, expr)
464}
465
466fn names_a_missing_node(text: &str) -> bool {
469 match instance_call(text) {
470 Some((path, _)) => sva_ast::names_a_node(path),
471 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
472 }
473}
474
475fn reads_as_a_value(text: &str) -> bool {
476 match sva_ast::parse_expr(text) {
477 Ok(sva_ast::Expr::Lit(_)) => true,
478 Ok(sva_ast::Expr::Var(name)) => sva_ast::is_reserved(&name),
479 _ => false,
480 }
481}
482
483fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
485 let defined = graph
486 .define_arranged(PROBE, expr)
487 .map_err(|r| CliError::Refusals(vec![r]))?;
488 if !defined {
489 return Err(CliError::BadProbe(format!(
490 "this composition already has a node named `{PROBE}`"
491 )));
492 }
493 tempo::refuse_unresolved_bars(graph)
494}
495
496pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
497 let mut graph = loaded.map_err(CliError::Refusals)?;
498 tempo::resolve(&mut graph)?;
499 graph.desugar_arrangement().map_err(CliError::Refusals)?;
500 Ok(graph)
501}
502
503pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
505
506pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
509 let mut roots: Vec<String> = RESERVED_VARIABLES
510 .iter()
511 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
512 .collect();
513 let held = |path: &str| -> Result<bool, CliError> {
514 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
515 };
516 match target {
517 target if held(target)? => {
518 roots.push(target.to_string());
519 }
520 text => match instance_call(text).map(|(path, _)| path) {
521 Some(path) if held(path)? => {
522 roots.push(path.to_string());
523 }
524 _ => {
525 if let Ok(expr) = sva_ast::parse_expr(text) {
526 roots.extend(sva_ast::reads_of(PROBE, &expr));
527 }
528 }
529 },
530 }
531 Ok(roots)
532}