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 Played, Printed, Report, SAMPLE_LIMIT, answer_json, counters_json, label_json, query_data,
18 stats_json, 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, Destination, RETIRED, asked, asked_all, call, calls, is_wav, retired, seconds,
27 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, PSYCHOACOUSTIC_V1, Range, RenderConfig,
40 StreamConfig, render, render_in,
41};
42
43pub use sva_engine::{Abandon, Handle, Hear, 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 hear: Option<&'a dyn Hear>,
100 pub threads: Option<NonZeroUsize>,
102}
103
104impl<'a> Job<'a> {
105 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
106 Job {
107 source,
108 target,
109 until: None,
110 rate: None,
111 bits: None,
112 asked: &[],
113 volatile: &[],
114 out: Out::Kept,
115 abandon: &Never,
116 hear: None,
117 threads: None,
118 }
119 }
120}
121
122fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
123 let Target { expr, interval } = target(job.target)?;
124 let roots = roots_of(job.source, &expr)?;
125 let mut graph = settled(sva_ast::load_reaching(
126 job.source,
127 &roots.iter().map(String::as_str).collect::<Vec<_>>(),
128 ))?;
129 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
130 CliError::BadProbe(format!(
131 "`{}` does not parse as an expression: {}",
132 job.target, d.message
133 ))
134 })?;
135 define_probe(&mut graph, parsed)?;
136 let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
137 let per_bar = graph.seconds_per_bar();
138 let range = match interval {
139 None => Range::default(),
140 Some((start, end)) => Range {
141 start: start.sample(rate, per_bar)?,
142 end: end.sample(rate, per_bar)?,
143 },
144 };
145 if let Range {
146 start: Some(start),
147 end: Some(end),
148 } = range
149 && end <= start
150 {
151 return Err(CliError::usage(
152 "render.empty_interval",
153 format!(
154 "`{}` reads an interval from sample {start} to {end}, which holds none",
155 job.target
156 ),
157 ));
158 }
159 let until = match job.until {
160 Some(text) => Some(until(text, rate, per_bar)?),
161 None => None,
162 };
163 let mut config = RenderConfig {
164 range,
165 until,
166 ..RenderConfig::at(rate)
167 };
168 config.profile.precision_bits = precision_bits(job.bits)?;
169 config.volatile = job.volatile.to_vec();
170 config.out = job.out;
171 if let Some(threads) = job.threads {
172 config.threads = threads;
173 }
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, tier: &Tier) -> Result<Rendered, CliError> {
219 let (graph, config) = settle(&job)?;
220 let render = render(&graph, PROBE, config, tier);
221 rendered(&job, graph, render)
222}
223
224pub async fn execute_over<B: Backend>(
227 job: Job<'_>,
228 tier: &Tier<B>,
229 session: &mut Session,
230) -> Result<Rendered, CliError> {
231 let (graph, config) = settle(&job)?;
232 let render = render_in(
233 session,
234 &graph,
235 PROBE,
236 config,
237 tier,
238 (job.abandon, job.hear),
239 )
240 .await;
241 rendered(&job, graph, render)
242}
243
244fn rendered(
245 job: &Job,
246 graph: Graph,
247 render: Result<sva_engine::Render, EngineError>,
248) -> Result<Rendered, CliError> {
249 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
250 Ok(Rendered {
251 config: render.config.clone(),
252 expression: job.target.to_string(),
253 target: PROBE.to_string(),
254 render,
255 graph,
256 })
257}
258
259pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
260 let (graph, config) = settle(job)?;
261 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
262}
263
264pub fn types(job: &Job) -> Result<(), CliError> {
266 let (graph, config) = settle(job)?;
267 sva_engine::types_at(&graph, PROBE, config.rate)
268 .map(|_| ())
269 .map_err(|e| CliError::Engine(as_written(e, job.target)))
270}
271
272pub async fn stream<B: Backend>(
274 job: &Job<'_>,
275 (block, channels): (usize, Option<usize>),
276 tier: &Tier<B>,
277) -> Result<Stream, CliError> {
278 let (graph, config) = settle(job)?;
279 let target = graph
280 .expr(PROBE)
281 .cloned()
282 .expect("the target was defined as the probe");
283 let config = StreamConfig {
284 block,
285 channels,
286 render: config,
287 };
288 Stream::open(&graph, &target, config, tier)
289 .await
290 .map_err(|e| CliError::Engine(as_written(e, job.target)))
291}
292
293pub async fn edit<B: Backend>(
296 stream: &RefCell<Stream>,
297 source: &dyn Source,
298 target: &str,
299 tier: &Tier<B>,
300) -> Result<(), CliError> {
301 let build = |s: &Stream| {
302 let (graph, expr) = streamed(s, source, target)?;
303 Ok(Change::Target(graph, expr))
304 };
305 changed(stream, build, Some(target), tier).await.map(|_| ())
306}
307
308pub async fn add<B: Backend>(
310 stream: &RefCell<Stream>,
311 source: &dyn Source,
312 (term, at): (&str, Placed),
313 tier: &Tier<B>,
314) -> Result<Handle, CliError> {
315 let build = |s: &Stream| {
316 let (graph, expr) = streamed(s, source, term)?;
317 Ok(Change::Add(graph, expr, at))
318 };
319 match changed(stream, build, Some(term), tier).await? {
320 Changed::Added(handle) => Ok(handle),
321 _ => unreachable!("an add answers its handle"),
322 }
323}
324
325pub async fn replace<B: Backend>(
327 stream: &RefCell<Stream>,
328 source: &dyn Source,
329 (handle, term, at): (Handle, &str, Placed),
330 tier: &Tier<B>,
331) -> Result<bool, CliError> {
332 let build = |s: &Stream| {
333 let (graph, expr) = streamed(s, source, term)?;
334 Ok(Change::Replace(handle, graph, expr, at))
335 };
336 Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
337}
338
339pub async fn remove<B: Backend>(
341 stream: &RefCell<Stream>,
342 handle: Handle,
343 tier: &Tier<B>,
344) -> Result<bool, CliError> {
345 let build = |_: &Stream| Ok(Change::Remove(handle));
346 Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
347}
348
349async fn changed<B: Backend>(
350 stream: &RefCell<Stream>,
351 build: impl FnMut(&Stream) -> Result<Change, CliError>,
352 text: Option<&str>,
353 tier: &Tier<B>,
354) -> Result<Changed, CliError> {
355 let changed = sva_engine::change(stream, build, tier).await;
356 changed.map_err(|e| match (e, text) {
357 (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
358 (e, _) => e,
359 })
360}
361
362pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
363 sva_engine::fetch(stream, tier).await;
364}
365
366fn streamed(
369 stream: &Stream,
370 source: &dyn Source,
371 text: &str,
372) -> Result<(Graph, sva_ast::Expr), CliError> {
373 let Target { expr, interval } = target(text)?;
374 if interval.is_some() {
375 return Err(CliError::usage(
376 "stream.interval",
377 format!("`{text}` reads an interval, and a stream keeps its own"),
378 ));
379 }
380 let held = stream.graph();
381 let roots = roots_of(source, &expr)?;
382 let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
383 let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
384 if let Some(per_bar) = held.per_bar() {
385 delta.resolve_bar_spans(per_bar);
386 }
387 delta
388 .desugar_arrangement_beside(held)
389 .map_err(CliError::Refusals)?;
390 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
391 CliError::BadProbe(format!(
392 "`{text}` does not parse as an expression: {}",
393 d.message
394 ))
395 })?;
396 let defined = delta
397 .define_arranged_beside(held, PROBE, parsed)
398 .map_err(|r| CliError::Refusals(vec![r]))?;
399 if !defined {
400 return Err(CliError::BadProbe(format!(
401 "this composition already has a node named `{PROBE}`"
402 )));
403 }
404 tempo::refuse_unresolved_bars(&delta)?;
405 let expr = delta
406 .expr(PROBE)
407 .cloned()
408 .expect("the target was defined as the probe");
409 Ok((delta, expr))
410}
411
412pub fn precision_bits(bits: Option<i32>) -> Result<i32, CliError> {
414 bits.map_or(Ok(PSYCHOACOUSTIC_V1.precision_bits), precision)
415}
416
417fn precision(bits: i32) -> Result<i32, CliError> {
419 match (2..=52).contains(&bits) {
420 true => Ok(bits),
421 false => Err(CliError::usage(
422 "cli.bad_value",
423 format!("`--bits` takes a precision from 2 to 52 bits, not {bits}"),
424 )),
425 }
426}
427
428fn as_written(refused: EngineError, target: &str) -> EngineError {
431 match refused {
432 EngineError::Refused(mut d) => {
433 if d.location.node == PROBE {
434 d.location.node = target.to_string();
435 }
436 d.message = d
437 .message
438 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
439 EngineError::Refused(d)
440 }
441 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
442 node: target.to_string(),
443 span,
444 fault,
445 },
446 other => other,
447 }
448}
449
450pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
451 execute(Job::over(&Dir::at(dir), expression), &Tier::default())
452}
453
454pub fn cwd() -> Result<std::path::PathBuf, CliError> {
455 std::env::current_dir()
456 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
457}
458
459pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
460 settled(sva_ast::load(source))
461}
462
463pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
466 let expr = match instance_read(graph, text) {
467 Some(read) => read,
468 None if names_a_missing_node(text) => {
469 return Err(CliError::NotFound(format!(
470 "`{text}` is not a node this composition defines"
471 )));
472 }
473 None => sva_ast::parse_expr(text).map_err(|d| {
474 CliError::BadProbe(format!(
475 "`{text}` is not a node in this composition, and does not parse as an \
476 expression: {}",
477 d.message
478 ))
479 })?,
480 };
481 define_probe(graph, expr)
482}
483
484fn names_a_missing_node(text: &str) -> bool {
487 match instance_call(text) {
488 Some((path, _)) => sva_ast::names_a_node(path),
489 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
490 }
491}
492
493fn reads_as_a_value(text: &str) -> bool {
494 match sva_ast::parse_expr(text) {
495 Ok(sva_ast::Expr::Lit(_)) => true,
496 Ok(sva_ast::Expr::Var(name)) => sva_ast::is_reserved(&name),
497 _ => false,
498 }
499}
500
501fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
503 let defined = graph
504 .define_arranged(PROBE, expr)
505 .map_err(|r| CliError::Refusals(vec![r]))?;
506 if !defined {
507 return Err(CliError::BadProbe(format!(
508 "this composition already has a node named `{PROBE}`"
509 )));
510 }
511 tempo::refuse_unresolved_bars(graph)
512}
513
514pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
515 let mut graph = loaded.map_err(CliError::Refusals)?;
516 tempo::resolve(&mut graph)?;
517 graph.desugar_arrangement().map_err(CliError::Refusals)?;
518 Ok(graph)
519}
520
521pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
523
524pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
527 let mut roots: Vec<String> = RESERVED_VARIABLES
528 .iter()
529 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
530 .collect();
531 let held = |path: &str| -> Result<bool, CliError> {
532 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
533 };
534 match target {
535 target if held(target)? => {
536 roots.push(target.to_string());
537 }
538 text => match instance_call(text).map(|(path, _)| path) {
539 Some(path) if held(path)? => {
540 roots.push(path.to_string());
541 }
542 _ => {
543 if let Ok(expr) = sva_ast::parse_expr(text) {
544 roots.extend(sva_ast::reads_of(PROBE, &expr));
545 }
546 }
547 },
548 }
549 Ok(roots)
550}