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