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