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, engine_stats_json,
18 label_json, query_data, 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, Kind, PSYCHOACOUSTIC_V1, Phase, Range,
40 RenderConfig, StreamConfig, render, render_called,
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, EngineStats,
48 FETCH_READS, Nothing, 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 call = tier.call(Kind::Render, job.target, Phase::Parsing);
232 let (graph, config) = settle(&job)?;
233 let hooks = (job.abandon, job.hear);
234 let render = render_called(session, &graph, PROBE, config, tier, hooks, call).await;
235 rendered(&job, graph, render)
236}
237
238fn rendered(
239 job: &Job,
240 graph: Graph,
241 render: Result<sva_engine::Render, EngineError>,
242) -> Result<Rendered, CliError> {
243 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
244 Ok(Rendered {
245 config: render.config.clone(),
246 expression: job.target.to_string(),
247 target: PROBE.to_string(),
248 render,
249 graph,
250 })
251}
252
253pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
254 let (graph, config) = settle(job)?;
255 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
256}
257
258pub fn types(job: &Job) -> Result<(), CliError> {
260 let (graph, config) = settle(job)?;
261 sva_engine::types_at(&graph, PROBE, config.rate)
262 .map(|_| ())
263 .map_err(|e| CliError::Engine(as_written(e, job.target)))
264}
265
266pub async fn stream<B: Backend>(
268 job: &Job<'_>,
269 (block, channels): (usize, Option<usize>),
270 tier: &Tier<B>,
271) -> Result<Stream, CliError> {
272 let call = tier.call(Kind::Stream, job.target, Phase::Parsing);
273 let (graph, config) = settle(job)?;
274 let target = graph
275 .expr(PROBE)
276 .cloned()
277 .expect("the target was defined as the probe");
278 let config = StreamConfig {
279 block,
280 channels,
281 render: config,
282 };
283 Stream::open_called(&graph, &target, config, tier, call)
284 .await
285 .map_err(|e| CliError::Engine(as_written(e, job.target)))
286}
287
288pub async fn edit<B: Backend>(
291 stream: &RefCell<Stream>,
292 source: &dyn Source,
293 target: &str,
294 tier: &Tier<B>,
295) -> Result<(), CliError> {
296 let build = |s: &Stream| {
297 let (graph, expr) = streamed(s, source, target)?;
298 Ok(Change::Target(graph, expr))
299 };
300 changed(stream, build, Some(target), tier).await.map(|_| ())
301}
302
303pub async fn add<B: Backend>(
305 stream: &RefCell<Stream>,
306 source: &dyn Source,
307 (term, at): (&str, Placed),
308 tier: &Tier<B>,
309) -> Result<Handle, CliError> {
310 let build = |s: &Stream| {
311 let (graph, expr) = streamed(s, source, term)?;
312 Ok(Change::Add(graph, expr, at))
313 };
314 match changed(stream, build, Some(term), tier).await? {
315 Changed::Added(handle) => Ok(handle),
316 _ => unreachable!("an add answers its handle"),
317 }
318}
319
320pub async fn replace<B: Backend>(
322 stream: &RefCell<Stream>,
323 source: &dyn Source,
324 (handle, term, at): (Handle, &str, Placed),
325 tier: &Tier<B>,
326) -> Result<bool, CliError> {
327 let build = |s: &Stream| {
328 let (graph, expr) = streamed(s, source, term)?;
329 Ok(Change::Replace(handle, graph, expr, at))
330 };
331 Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
332}
333
334pub async fn remove<B: Backend>(
336 stream: &RefCell<Stream>,
337 handle: Handle,
338 tier: &Tier<B>,
339) -> Result<bool, CliError> {
340 let build = |_: &Stream| Ok(Change::Remove(handle));
341 Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
342}
343
344async fn changed<B: Backend>(
345 stream: &RefCell<Stream>,
346 build: impl FnMut(&Stream) -> Result<Change, CliError>,
347 text: Option<&str>,
348 tier: &Tier<B>,
349) -> Result<Changed, CliError> {
350 let changed = sva_engine::change(stream, build, tier).await;
351 changed.map_err(|e| match (e, text) {
352 (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
353 (e, _) => e,
354 })
355}
356
357pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
358 sva_engine::fetch(stream, tier).await;
359}
360
361fn streamed(
364 stream: &Stream,
365 source: &dyn Source,
366 text: &str,
367) -> Result<(Graph, sva_ast::Expr), CliError> {
368 let Target { expr, interval } = target(text)?;
369 if interval.is_some() {
370 return Err(CliError::usage(
371 "stream.interval",
372 format!("`{text}` reads an interval, and a stream keeps its own"),
373 ));
374 }
375 let held = stream.graph();
376 let roots = roots_of(source, &expr)?;
377 let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
378 let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
379 if let Some(per_bar) = held.per_bar() {
380 delta.resolve_bar_spans(per_bar);
381 }
382 delta
383 .desugar_arrangement_beside(held)
384 .map_err(CliError::Refusals)?;
385 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
386 CliError::BadProbe(format!(
387 "`{text}` does not parse as an expression: {}",
388 d.message
389 ))
390 })?;
391 let defined = delta
392 .define_arranged_beside(held, PROBE, parsed)
393 .map_err(|r| CliError::Refusals(vec![r]))?;
394 if !defined {
395 return Err(CliError::BadProbe(format!(
396 "this composition already has a node named `{PROBE}`"
397 )));
398 }
399 tempo::refuse_unresolved_bars(&delta)?;
400 let expr = delta
401 .expr(PROBE)
402 .cloned()
403 .expect("the target was defined as the probe");
404 Ok((delta, expr))
405}
406
407pub fn precision_bits(bits: Option<i32>) -> Result<i32, CliError> {
409 bits.map_or(Ok(PSYCHOACOUSTIC_V1.precision_bits), precision)
410}
411
412fn precision(bits: i32) -> Result<i32, CliError> {
414 match (2..=52).contains(&bits) {
415 true => Ok(bits),
416 false => Err(CliError::usage(
417 "cli.bad_value",
418 format!("`--bits` takes a precision from 2 to 52 bits, not {bits}"),
419 )),
420 }
421}
422
423fn as_written(refused: EngineError, target: &str) -> EngineError {
426 match refused {
427 EngineError::Refused(mut d) => {
428 if d.location.node == PROBE {
429 d.location.node = target.to_string();
430 }
431 d.message = d
432 .message
433 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
434 EngineError::Refused(d)
435 }
436 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
437 node: target.to_string(),
438 span,
439 fault,
440 },
441 other => other,
442 }
443}
444
445pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
446 execute(Job::over(&Dir::at(dir), expression), &Tier::default())
447}
448
449pub fn cwd() -> Result<std::path::PathBuf, CliError> {
450 std::env::current_dir()
451 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
452}
453
454pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
455 settled(sva_ast::load(source))
456}
457
458pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
461 let expr = match instance_read(graph, text) {
462 Some(read) => read,
463 None if names_a_missing_node(text) => {
464 return Err(CliError::NotFound(format!(
465 "`{text}` is not a node this composition defines"
466 )));
467 }
468 None => sva_ast::parse_expr(text).map_err(|d| {
469 CliError::BadProbe(format!(
470 "`{text}` is not a node in this composition, and does not parse as an \
471 expression: {}",
472 d.message
473 ))
474 })?,
475 };
476 define_probe(graph, expr)
477}
478
479fn names_a_missing_node(text: &str) -> bool {
482 match instance_call(text) {
483 Some((path, _)) => sva_ast::names_a_node(path),
484 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
485 }
486}
487
488fn reads_as_a_value(text: &str) -> bool {
489 match sva_ast::parse_expr(text) {
490 Ok(sva_ast::Expr::Lit(_)) => true,
491 Ok(sva_ast::Expr::Var(name)) => sva_ast::is_reserved(&name),
492 _ => false,
493 }
494}
495
496fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
498 let defined = graph
499 .define_arranged(PROBE, expr)
500 .map_err(|r| CliError::Refusals(vec![r]))?;
501 if !defined {
502 return Err(CliError::BadProbe(format!(
503 "this composition already has a node named `{PROBE}`"
504 )));
505 }
506 tempo::refuse_unresolved_bars(graph)
507}
508
509pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
510 let mut graph = loaded.map_err(CliError::Refusals)?;
511 tempo::resolve(&mut graph)?;
512 graph.desugar_arrangement().map_err(CliError::Refusals)?;
513 Ok(graph)
514}
515
516pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
518
519pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
522 let mut roots: Vec<String> = RESERVED_VARIABLES
523 .iter()
524 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
525 .collect();
526 let held = |path: &str| -> Result<bool, CliError> {
527 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
528 };
529 match target {
530 target if held(target)? => {
531 roots.push(target.to_string());
532 }
533 text => match instance_call(text).map(|(path, _)| path) {
534 Some(path) if held(path)? => {
535 roots.push(path.to_string());
536 }
537 _ => {
538 if let Ok(expr) = sva_ast::parse_expr(text) {
539 roots.extend(sva_ast::reads_of(PROBE, &expr));
540 }
541 }
542 },
543 }
544 Ok(roots)
545}