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::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_through,
40};
41
42pub use sva_engine::{Handle, QuietTail, Stream, Until};
43
44pub use sva_engine::{Answer, Extent, Label, Output, Representation};
45pub use sva_engine::{
46 Backend, Cache, CachePolicy, CacheStats, DEFAULT_STORE_BYTES, NoStore, Persisted, PrunePolicy,
47 Store, Through,
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 cache: Option<&'a Cache>,
95 pub cache_policy: Option<CachePolicy>,
96 pub asked: &'a [Asked],
97 pub flop_budget: Option<u128>,
99 pub volatile: &'a [String],
100}
101
102impl<'a> Job<'a> {
103 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
104 Job {
105 source,
106 target,
107 until: None,
108 rate: None,
109 bits: None,
110 cache: None,
111 cache_policy: None,
112 asked: &[],
113 flop_budget: None,
114 volatile: &[],
115 }
116 }
117}
118
119fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
120 settle_reaching(job, &[])
121}
122
123fn settle_reaching(job: &Job, also: &[String]) -> Result<(Graph, RenderConfig), CliError> {
124 let Target { expr, interval } = target(job.target)?;
125 let mut roots = roots_of(job.source, &expr)?;
126 roots.extend_from_slice(also);
127 let mut graph = settled(sva_ast::load_reaching(
128 job.source,
129 &roots.iter().map(String::as_str).collect::<Vec<_>>(),
130 ))?;
131 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
132 CliError::BadProbe(format!(
133 "`{}` does not parse as an expression: {}",
134 job.target, d.message
135 ))
136 })?;
137 define_probe(&mut graph, parsed)?;
138 let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
139 let per_bar = graph.seconds_per_bar();
140 let range = match interval {
141 None => Range::default(),
142 Some((start, end)) => Range {
143 start: start.sample(rate, per_bar)?,
144 end: end.sample(rate, per_bar)?,
145 },
146 };
147 if let Range {
148 start: Some(start),
149 end: Some(end),
150 } = range
151 && end <= start
152 {
153 return Err(CliError::Usage(format!(
154 "`{}` reads an interval from sample {start} to {end}, which holds none",
155 job.target
156 )));
157 }
158 let until = match job.until {
159 Some(text) => Some(until(text, rate, per_bar)?),
160 None => None,
161 };
162 let mut config = RenderConfig {
163 range,
164 until,
165 ..RenderConfig::at(rate)
166 };
167 if let Some(budget) = job.flop_budget {
168 config.flop_budget = budget;
169 }
170 if let Some(bits) = job.bits {
171 config.profile.precision_bits = precision(bits)?;
172 }
173 config.volatile = job.volatile.to_vec();
174 config.cache_policy = job.cache_policy;
175 config.asks = job
176 .asked
177 .iter()
178 .map(|asked| Ask {
179 node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
180 representation: asked.representation,
181 })
182 .collect();
183 Ok((graph, config))
184}
185
186fn instance_call(text: &str) -> Option<(&str, &str)> {
188 let (path, rest) = text.split_once('(')?;
189 let binds = rest.strip_suffix(')')?.trim();
190 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
191}
192
193fn all_named(binds: &str) -> bool {
195 let mut depth = 0i32;
196 let mut named = false;
197 for c in binds.chars() {
198 match c {
199 '(' => depth += 1,
200 ')' if depth == 0 => return false,
201 ')' => depth -= 1,
202 '=' if depth == 0 => named = true,
203 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
204 _ => {}
205 }
206 }
207 named && depth == 0
208}
209
210fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
212 let (path, binds) = instance_call(text)?;
213 if !graph.defines(path) {
214 return None;
215 }
216 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
217}
218
219pub fn execute(job: Job) -> Result<Rendered, CliError> {
220 let (graph, config) = settle(&job)?;
221 let render = render(&graph, PROBE, config, job.cache);
222 rendered(&job, graph, render)
223}
224
225pub async fn execute_through<B: Backend>(
227 job: Job<'_>,
228 store: &Store<B>,
229) -> Result<Rendered, CliError> {
230 let (graph, config) = settle(&job)?;
231 let render = render_through(&graph, PROBE, config, store).await;
232 rendered(&job, graph, render)
233}
234
235pub struct Warmed {
236 pub stats: CacheStats,
237 pub readings: Option<Rendered>,
238}
239
240pub async fn warm<B: Backend>(job: Job<'_>, store: &Store<B>) -> Result<Warmed, CliError> {
242 if !job.asked.is_empty() {
243 let mut read = execute_through(job, store).await?;
244 let stats = read.render.cache_stats.take();
245 let stats = stats.expect("a render through a store reports on it");
246 return Ok(Warmed {
247 stats,
248 readings: Some(read),
249 });
250 }
251 let (graph, config) = settle(&job)?;
252 let stats = sva_engine::warm(&graph, PROBE, config, store).await;
253 let stats = stats.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
254 Ok(Warmed {
255 stats,
256 readings: None,
257 })
258}
259
260fn rendered(
261 job: &Job,
262 graph: Graph,
263 render: Result<sva_engine::Render, EngineError>,
264) -> Result<Rendered, CliError> {
265 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
266 Ok(Rendered {
267 config: render.config.clone(),
268 expression: job.target.to_string(),
269 target: PROBE.to_string(),
270 render,
271 graph,
272 })
273}
274
275pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
276 let (graph, config) = settle(job)?;
277 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
278}
279
280pub fn types(job: &Job) -> Result<(), CliError> {
282 let (graph, config) = settle(job)?;
283 sva_engine::types_at(&graph, PROBE, config.rate)
284 .map(|_| ())
285 .map_err(|e| CliError::Engine(as_written(e, job.target)))
286}
287
288pub fn quiet_tails(job: &Job) -> Result<Vec<QuietTail>, CliError> {
290 let (graph, config) = settle(job)?;
291 let found = sva_engine::quiet_tails(&graph, PROBE, config)
292 .map_err(|e| CliError::Engine(as_written(e, job.target)))?;
293 Ok(found.into_iter().filter(|t| t.file != PROBE).collect())
294}
295
296pub async fn stream(job: &Job<'_>, block: usize, store: &impl Through) -> Result<Stream, CliError> {
297 let (graph, config) = settle(job)?;
298 let target = graph
299 .expr(PROBE)
300 .cloned()
301 .expect("the target was defined as the probe");
302 let config = StreamConfig {
303 block,
304 render: config,
305 };
306 Stream::open(&graph, &target, config, job.cache, store)
307 .await
308 .map_err(|e| CliError::Engine(as_written(e, job.target)))
309}
310
311pub async fn edit(
314 stream: &RefCell<Stream>,
315 source: &dyn Source,
316 target: &str,
317 store: &impl Through,
318) -> Result<(), CliError> {
319 let build = |s: &Stream| {
320 let (graph, expr) = streamed(s, source, target)?;
321 Ok(Change::Target(graph, expr))
322 };
323 changed(stream, build, Some(target), store)
324 .await
325 .map(|_| ())
326}
327
328pub async fn add(
330 stream: &RefCell<Stream>,
331 source: &dyn Source,
332 term: &str,
333 store: &impl Through,
334) -> Result<Handle, CliError> {
335 let build = |s: &Stream| {
336 let (graph, expr) = streamed(s, source, term)?;
337 Ok(Change::Add(graph, expr))
338 };
339 match changed(stream, build, Some(term), store).await? {
340 Changed::Added(handle) => Ok(handle),
341 _ => unreachable!("an add answers its handle"),
342 }
343}
344
345pub async fn replace(
347 stream: &RefCell<Stream>,
348 source: &dyn Source,
349 (handle, term): (Handle, &str),
350 store: &impl Through,
351) -> Result<bool, CliError> {
352 let build = |s: &Stream| {
353 let (graph, expr) = streamed(s, source, term)?;
354 Ok(Change::Replace(handle, graph, expr))
355 };
356 Ok(changed(stream, build, Some(term), store).await? == Changed::Held(true))
357}
358
359pub async fn remove(
361 stream: &RefCell<Stream>,
362 handle: Handle,
363 store: &impl Through,
364) -> Result<bool, CliError> {
365 let build = |_: &Stream| Ok(Change::Remove(handle));
366 Ok(changed(stream, build, None, store).await? == Changed::Held(true))
367}
368
369async fn changed(
370 stream: &RefCell<Stream>,
371 build: impl FnMut(&Stream) -> Result<Change, CliError>,
372 text: Option<&str>,
373 store: &impl Through,
374) -> Result<Changed, CliError> {
375 let changed = sva_engine::change(stream, build, store).await;
376 changed.map_err(|e| match (e, text) {
377 (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
378 (e, _) => e,
379 })
380}
381
382fn streamed(
384 stream: &Stream,
385 source: &dyn Source,
386 text: &str,
387) -> Result<(Graph, sva_ast::Expr), CliError> {
388 let playing: Vec<String> = stream
389 .exprs()
390 .flat_map(|e| sva_ast::reads_of(PROBE, e))
391 .collect();
392 let (graph, config) = settle_reaching(&Job::over(source, text), &playing)?;
393 if config.range != Range::default() {
394 return Err(CliError::Usage(format!(
395 "`{text}` reads an interval, and a stream keeps its own"
396 )));
397 }
398 let expr = graph
399 .expr(PROBE)
400 .cloned()
401 .expect("the target was defined as the probe");
402 Ok((graph, expr))
403}
404
405fn precision(bits: i32) -> Result<i32, CliError> {
407 match (2..=52).contains(&bits) {
408 true => Ok(bits),
409 false => Err(CliError::Usage(format!(
410 "`--bits` takes a precision from 2 to 52 bits, not {bits}"
411 ))),
412 }
413}
414
415fn as_written(refused: EngineError, target: &str) -> EngineError {
418 match refused {
419 EngineError::Refused(mut d) => {
420 if d.location.node == PROBE {
421 d.location.node = target.to_string();
422 }
423 d.message = d
424 .message
425 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
426 EngineError::Refused(d)
427 }
428 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
429 node: target.to_string(),
430 span,
431 fault,
432 },
433 other => other,
434 }
435}
436
437pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
438 execute(Job::over(&Dir::at(dir), expression))
439}
440
441pub fn cwd() -> Result<std::path::PathBuf, CliError> {
442 std::env::current_dir()
443 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
444}
445
446pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
447 settled(sva_ast::load(source))
448}
449
450pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
453 let expr = match instance_read(graph, text) {
454 Some(read) => read,
455 None if names_a_missing_node(text) => {
456 return Err(CliError::NotFound(format!(
457 "`{text}` is not a node this composition defines"
458 )));
459 }
460 None => sva_ast::parse_expr(text).map_err(|d| {
461 CliError::BadProbe(format!(
462 "`{text}` is not a node in this composition, and does not parse as an \
463 expression: {}",
464 d.message
465 ))
466 })?,
467 };
468 define_probe(graph, expr)
469}
470
471fn names_a_missing_node(text: &str) -> bool {
474 match instance_call(text) {
475 Some((path, _)) => sva_ast::names_a_node(path),
476 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
477 }
478}
479
480fn reads_as_a_value(text: &str) -> bool {
481 match sva_ast::parse_expr(text) {
482 Ok(sva_ast::Expr::Lit(_)) => true,
483 Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
484 _ => false,
485 }
486}
487
488fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
490 let defined = graph
491 .define_arranged(PROBE, expr)
492 .map_err(|r| CliError::Refusals(vec![r]))?;
493 if !defined {
494 return Err(CliError::BadProbe(format!(
495 "this composition already has a node named `{PROBE}`"
496 )));
497 }
498 tempo::refuse_unresolved_bars(graph)
499}
500
501pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
502 let mut graph = loaded.map_err(CliError::Refusals)?;
503 tempo::resolve(&mut graph)?;
504 graph.desugar_arrangement().map_err(CliError::Refusals)?;
505 Ok(graph)
506}
507
508pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
510
511pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
514 let mut roots: Vec<String> = RESERVED_VARIABLES
515 .iter()
516 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
517 .collect();
518 let held = |path: &str| -> Result<bool, CliError> {
519 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
520 };
521 match target {
522 target if held(target)? => {
523 roots.push(target.to_string());
524 }
525 text => match instance_call(text).map(|(path, _)| path) {
526 Some(path) if held(path)? => {
527 roots.push(path.to_string());
528 }
529 _ => {
530 if let Ok(expr) = sva_ast::parse_expr(text) {
531 roots.extend(sva_ast::reads_of(PROBE, &expr));
532 }
533 }
534 },
535 }
536 Ok(roots)
537}