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