1mod answer;
4mod builtins;
5mod cli_error;
6mod duration;
7pub mod json;
8mod lint_code;
9mod outline;
10mod output;
11mod query;
12mod tempo;
13
14pub use answer::{
15 CacheReport, Report, SAMPLE_LIMIT, answer_json, label_json, query_data, stats_json, value_json,
16 work_json,
17};
18pub use builtins::{Builtins, Callable, Crossing, builtins, builtins_data};
19pub use cli_error::{CliError, LintViolation, lint_diagnostic};
20pub use lint_code::LintCode;
21pub use outline::outline_data;
22pub use output::{Diagnostic, Severity, diagnostics_json, error_envelope, success_envelope};
23pub use query::{
24 Asked, DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PEAKS, DEFAULT_OVERSAMPLE, DEFAULT_SILENT_BITS,
25 DEFAULT_SILENT_MAX_SECS, REPRESENTATIONS, RETIRED, Shaping, WindowEdge, is_wav,
26 representation_for, retired, silence, silent_edge, window_edge, window_for,
27};
28pub use tempo::refuse_unresolved_bars;
29
30use std::path::Path;
31
32use sva_ast::{Dir, Graph, Refusal, Source, SpanUnit};
33use sva_engine::{
34 Ask, BindingFault, Cache, DEFAULT_SAMPLE_RATE, EngineError, PSYCHOACOUSTIC_V1, Render,
35 RenderConfig, StreamConfig, render_until_silent, render_with_slots,
36};
37
38pub use sva_engine::{Checkpoint, Silent, Stream};
39
40pub use sva_engine::{Answer, Horizon, Label, Output, Representation};
41pub use sva_engine::{DiskCache, MemoryCache, Slots};
42
43pub const ROOT: &str = "master";
44pub const PROBE: &str = "probe";
45
46pub const DEFAULT_SECONDS: f64 = 1.0;
47
48pub struct Rendered {
49 pub config: RenderConfig,
50 pub target: String,
51 pub render: Render,
52 pub graph: Graph,
54}
55
56impl Rendered {
57 pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
58 let id = self.render.node(node).map_err(CliError::Engine)?;
59 let mut answer =
60 sva_engine::answer(&self.render, id, representation).map_err(CliError::Engine)?;
61 self.attribute(&mut answer)?;
62 Ok(answer)
63 }
64
65 fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
68 let Output::Alias(alias) = &mut answer.value else {
69 return Ok(());
70 };
71 alias.rate_dependent = sva_engine::rate_dependent(&self.graph, &self.target)
72 .map_err(CliError::Engine)?
73 .len();
74 alias.instances = self.render.tys.paths().count();
75 Ok(())
76 }
77
78 pub fn label(&self) -> Option<&sva_engine::Label> {
79 self.render.labels.get(&self.render.root)
80 }
81}
82
83pub struct Job<'a> {
85 pub source: &'a dyn Source,
86 pub target: Option<&'a str>,
87 pub from: Option<WindowEdge>,
88 pub until: Option<WindowEdge>,
89 pub sample_rate: Option<u32>,
90 pub cache: Option<&'a dyn Cache>,
91 pub reaching: bool,
93 pub reading: Option<&'a str>,
95 pub representations: Vec<Representation>,
96 pub flop_budget: Option<u128>,
98 pub volatile: &'a [String],
99 pub slots: Option<&'a Slots>,
100 pub silent: Option<Silent>,
102}
103
104impl<'a> Job<'a> {
105 pub fn over(source: &'a dyn Source) -> Job<'a> {
106 Job {
107 source,
108 target: None,
109 from: None,
110 until: None,
111 sample_rate: None,
112 cache: None,
113 reaching: false,
114 reading: None,
115 representations: Vec::new(),
116 flop_budget: None,
117 volatile: &[],
118 slots: None,
119 silent: None,
120 }
121 }
122}
123
124fn settle(job: &Job, asked: Option<&str>) -> Result<(Graph, String, RenderConfig), CliError> {
125 let mut graph = match job.reaching {
126 false => prepared(job.source)?,
127 true => settled(sva_ast::load_reaching(
128 job.source,
129 &roots_of(job.source, asked)?
130 .iter()
131 .map(String::as_str)
132 .collect::<Vec<_>>(),
133 ))?,
134 };
135 let target = match asked {
136 None => ROOT.to_string(),
137 Some(name) if graph.defines(name) => name.to_string(),
138 Some(text) => {
139 define_probe_for(&mut graph, text)?;
140 PROBE.to_string()
141 }
142 };
143 let until = match job.silent {
144 Some(silent) => Some(WindowEdge::Secs(silent.max_secs)),
145 None => job.until,
146 };
147 let mut config = config_for(&graph, &target, job.from, until, job.sample_rate)?;
148 if let Some(budget) = job.flop_budget {
149 config.flop_budget = budget;
150 }
151 config.volatile = job.volatile.to_vec();
152 let node = job.reading.unwrap_or(&target);
153 config.asks = job
154 .representations
155 .iter()
156 .map(|representation| Ask {
157 node: node.to_string(),
158 representation: *representation,
159 })
160 .collect();
161 Ok((graph, target, config))
162}
163
164fn instance_call(text: &str) -> Option<(&str, &str)> {
166 let (path, rest) = text.split_once('(')?;
167 let binds = rest.strip_suffix(')')?.trim();
168 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
169}
170
171fn all_named(binds: &str) -> bool {
173 let mut depth = 0i32;
174 let mut named = false;
175 for c in binds.chars() {
176 match c {
177 '(' => depth += 1,
178 ')' if depth == 0 => return false,
179 ')' => depth -= 1,
180 '=' if depth == 0 => named = true,
181 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
182 _ => {}
183 }
184 }
185 named && depth == 0
186}
187
188fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
190 let (path, binds) = instance_call(text)?;
191 if !graph.defines(path) {
192 return None;
193 }
194 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
195}
196
197pub fn execute(job: Job) -> Result<Rendered, CliError> {
198 let cache = job.cache;
199 let rendered = rendered(job);
200 if let Some(store) = cache {
201 store.sweep();
202 }
203 rendered
204}
205
206fn rendered(job: Job) -> Result<Rendered, CliError> {
207 let (graph, target, config) = settle(&job, job.target)?;
208 let refused = match rendering(&job, &graph, &target, config) {
209 Ok(render) => {
210 return Ok(Rendered {
211 config: render.config.clone(),
212 render,
213 target,
214 graph,
215 });
216 }
217 Err(refused) => refused,
218 };
219 match instances_behind(job.source, &target, &refused) {
220 Some(held) if held.len() == 1 => at_instance(&job, &held[0]),
221 Some(held) => Err(CliError::Engine(EngineError::AmbiguousNode(target, held))),
222 None => Err(CliError::Engine(refused)),
223 }
224}
225
226pub fn instances_behind(
227 source: &dyn Source,
228 target: &str,
229 refused: &EngineError,
230) -> Option<Vec<String>> {
231 unbound(refused)
232 .then(|| instances_of(source, target))
233 .flatten()
234}
235
236fn unbound(refused: &EngineError) -> bool {
237 matches!(
238 refused,
239 EngineError::Binding {
240 fault: BindingFault::Unbound(..),
241 ..
242 }
243 )
244}
245
246fn instances_of(source: &dyn Source, target: &str) -> Option<Vec<String>> {
248 let graph = prepared(source).ok()?;
249 let (instances, _) = sva_engine::instantiate::from_roots(&graph, &[ROOT.to_string()]).ok()?;
250 let held: Vec<String> = instances.instances_of(target).collect();
251 (!held.is_empty()).then_some(held)
252}
253
254fn at_instance(job: &Job, instance: &str) -> Result<Rendered, CliError> {
256 let (graph, target, config) = settle(job, Some(instance))?;
257 let render = rendering(job, &graph, &target, config).map_err(CliError::Engine)?;
258 Ok(Rendered {
259 config: render.config.clone(),
260 render,
261 target,
262 graph,
263 })
264}
265
266fn rendering(
268 job: &Job,
269 graph: &Graph,
270 target: &str,
271 config: RenderConfig,
272) -> Result<Render, EngineError> {
273 match job.silent {
274 Some(silent) => render_until_silent(graph, target, config, silent, job.cache, job.slots),
275 None => render_with_slots(graph, target, config, job.cache, job.slots),
276 }
277}
278
279pub fn stream(job: &Job, block: usize, bindings: &[(String, f64)]) -> Result<Stream, CliError> {
281 let (graph, target, config) = settle(job, job.target)?;
282 let config = StreamConfig {
283 rate: config.rate,
284 block,
285 silent: job.silent,
286 };
287 Stream::open(&graph, &target, bindings, config).map_err(CliError::Engine)
288}
289
290pub fn run(dir: &Path) -> Result<Rendered, CliError> {
291 execute(Job::over(&Dir::at(dir)))
292}
293
294pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
295 execute(Job {
296 target: Some(expression),
297 ..Job::over(&Dir::at(dir))
298 })
299}
300
301pub fn cwd() -> Result<std::path::PathBuf, CliError> {
302 std::env::current_dir()
303 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
304}
305
306pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
307 settled(sva_ast::load(source))
308}
309
310pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
313 let expr = match instance_read(graph, text) {
314 Some(read) => read,
315 None if names_a_missing_node(text) => {
316 return Err(CliError::NotFound(format!(
317 "`{text}` is not a node this composition defines"
318 )));
319 }
320 None => sva_ast::parse_expr(text).map_err(|d| {
321 CliError::BadProbe(format!(
322 "`{text}` is not a node in this composition, and does not parse as an \
323 expression: {}",
324 d.message
325 ))
326 })?,
327 };
328 define_probe(graph, expr)
329}
330
331fn names_a_missing_node(text: &str) -> bool {
334 match instance_call(text) {
335 Some((path, _)) => sva_ast::names_a_node(path),
336 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
337 }
338}
339
340fn reads_as_a_value(text: &str) -> bool {
341 match sva_ast::parse_expr(text) {
342 Ok(sva_ast::Expr::Lit(_)) => true,
343 Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
344 _ => false,
345 }
346}
347
348fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
350 let defined = graph
351 .define_arranged(PROBE, expr)
352 .map_err(|r| CliError::Refusals(vec![r]))?;
353 if !defined {
354 return Err(CliError::BadProbe(format!(
355 "this composition already has a node named `{PROBE}`"
356 )));
357 }
358 tempo::refuse_unresolved_bars(graph)
359}
360
361pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
362 let mut graph = loaded.map_err(CliError::Refusals)?;
363 tempo::resolve(&mut graph)?;
364 graph.desugar_arrangement().map_err(CliError::Refusals)?;
365 Ok(graph)
366}
367
368pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
370
371pub fn roots_of(source: &dyn Source, target: Option<&str>) -> Result<Vec<String>, CliError> {
373 let mut roots: Vec<String> = RESERVED_VARIABLES
374 .iter()
375 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
376 .collect();
377 match target {
378 None => roots.push(ROOT.to_string()),
379 Some(target) if source.get(target).map_err(CliError::Io)?.is_some() => {
380 roots.push(target.to_string());
381 }
382 Some(text) => match instance_call(text).map(|(path, _)| path) {
383 Some(path) if source.get(path).map_err(CliError::Io)?.is_some() => {
384 roots.push(path.to_string());
385 }
386 _ => {
387 if let Ok(expr) = sva_ast::parse_expr(text) {
388 roots.extend(sva_ast::reads_of(PROBE, &expr));
389 }
390 }
391 },
392 }
393 Ok(roots)
394}
395
396pub fn config_for(
399 graph: &Graph,
400 root: &str,
401 from: Option<WindowEdge>,
402 until: Option<WindowEdge>,
403 sample_rate: Option<u32>,
404) -> Result<RenderConfig, CliError> {
405 let declared = duration::widest_crop(graph, root);
406 let length = match graph.span(root) {
407 Some(span) if span.unit == SpanUnit::Seconds => Some(span.amount),
408 Some(span) => {
409 return Err(CliError::BadTempo(format!(
410 "`{root}`'s span is still {} bars, unresolved; give bpm/meter, or a seconds span",
411 span.amount
412 )));
413 }
414 None => declared.end,
415 };
416 let asked = window_for(from, until, graph.seconds_per_bar())?;
417 let end = match asked.end_secs.is_finite() {
418 true => asked.end_secs,
419 false => length.unwrap_or(DEFAULT_SECONDS),
420 };
421 let stated = !matches!(from, None | Some(WindowEdge::End));
422 let start = match stated {
423 true => asked.start_secs,
424 false => declared.start.unwrap_or(0.0).min(0.0),
425 };
426 if end <= start {
427 return Err(CliError::Usage(format!(
428 "a reading runs from {start}s to {end}s, which is no window; give --to past --from"
429 )));
430 }
431 Ok(RenderConfig {
432 rate: sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
433 horizon: Horizon::secs(start, end),
434 profile: PSYCHOACOUSTIC_V1,
435 asks: Vec::new(),
436 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
437 volatile: Vec::new(),
438 })
439}