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 Report, SAMPLE_LIMIT, answer_json, label_json, query_data, stats_json, value_json, work_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, DEFAULT_SAMPLE_RATE, EngineError, PSYCHOACOUSTIC_V1, Render, RenderConfig,
34 StreamConfig, render, render_until_silent,
35};
36
37pub use sva_engine::{Checkpoint, Silent, Stream};
38
39pub use sva_engine::{Answer, Horizon, Label, Output, Representation};
40pub use sva_engine::{Cache, CachePolicy, PrunePolicy};
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 Cache>,
90 pub cache_policy: Option<CachePolicy>,
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 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 cache_policy: None,
113 reaching: false,
114 reading: None,
115 representations: Vec::new(),
116 flop_budget: None,
117 volatile: &[],
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 config.cache_policy = job.cache_policy;
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 (graph, target, config) = settle(&job, job.target)?;
199 let refused = match rendering(&job, &graph, &target, config) {
200 Ok(render) => {
201 return Ok(Rendered {
202 config: render.config.clone(),
203 render,
204 target,
205 graph,
206 });
207 }
208 Err(refused) => refused,
209 };
210 match instances_behind(job.source, &target, &refused) {
211 Some(held) if held.len() == 1 => at_instance(&job, &held[0]),
212 Some(held) => Err(CliError::Engine(EngineError::AmbiguousNode(target, held))),
213 None => Err(CliError::Engine(refused)),
214 }
215}
216
217pub fn instances_behind(
218 source: &dyn Source,
219 target: &str,
220 refused: &EngineError,
221) -> Option<Vec<String>> {
222 unbound(refused)
223 .then(|| instances_of(source, target))
224 .flatten()
225}
226
227fn unbound(refused: &EngineError) -> bool {
228 matches!(
229 refused,
230 EngineError::Binding {
231 fault: BindingFault::Unbound(..),
232 ..
233 }
234 )
235}
236
237fn instances_of(source: &dyn Source, target: &str) -> Option<Vec<String>> {
239 let graph = prepared(source).ok()?;
240 let (instances, _) = sva_engine::instantiate::from_roots(&graph, &[ROOT.to_string()]).ok()?;
241 let held: Vec<String> = instances.instances_of(target).collect();
242 (!held.is_empty()).then_some(held)
243}
244
245fn at_instance(job: &Job, instance: &str) -> Result<Rendered, CliError> {
247 let (graph, target, config) = settle(job, Some(instance))?;
248 let render = rendering(job, &graph, &target, config).map_err(CliError::Engine)?;
249 Ok(Rendered {
250 config: render.config.clone(),
251 render,
252 target,
253 graph,
254 })
255}
256
257fn rendering(
259 job: &Job,
260 graph: &Graph,
261 target: &str,
262 config: RenderConfig,
263) -> Result<Render, EngineError> {
264 match job.silent {
265 Some(silent) => render_until_silent(graph, target, config, silent, job.cache),
266 None => render(graph, target, config, job.cache),
267 }
268}
269
270pub fn stream(job: &Job, block: usize, bindings: &[(String, f64)]) -> Result<Stream, CliError> {
272 let (graph, target, config) = settle(job, job.target)?;
273 let config = StreamConfig {
274 rate: config.rate,
275 block,
276 silent: job.silent,
277 };
278 Stream::open(&graph, &target, bindings, config).map_err(CliError::Engine)
279}
280
281pub fn run(dir: &Path) -> Result<Rendered, CliError> {
282 execute(Job::over(&Dir::at(dir)))
283}
284
285pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
286 execute(Job {
287 target: Some(expression),
288 ..Job::over(&Dir::at(dir))
289 })
290}
291
292pub fn cwd() -> Result<std::path::PathBuf, CliError> {
293 std::env::current_dir()
294 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
295}
296
297pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
298 settled(sva_ast::load(source))
299}
300
301pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
304 let expr = match instance_read(graph, text) {
305 Some(read) => read,
306 None if names_a_missing_node(text) => {
307 return Err(CliError::NotFound(format!(
308 "`{text}` is not a node this composition defines"
309 )));
310 }
311 None => sva_ast::parse_expr(text).map_err(|d| {
312 CliError::BadProbe(format!(
313 "`{text}` is not a node in this composition, and does not parse as an \
314 expression: {}",
315 d.message
316 ))
317 })?,
318 };
319 define_probe(graph, expr)
320}
321
322fn names_a_missing_node(text: &str) -> bool {
325 match instance_call(text) {
326 Some((path, _)) => sva_ast::names_a_node(path),
327 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
328 }
329}
330
331fn reads_as_a_value(text: &str) -> bool {
332 match sva_ast::parse_expr(text) {
333 Ok(sva_ast::Expr::Lit(_)) => true,
334 Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
335 _ => false,
336 }
337}
338
339fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
341 let defined = graph
342 .define_arranged(PROBE, expr)
343 .map_err(|r| CliError::Refusals(vec![r]))?;
344 if !defined {
345 return Err(CliError::BadProbe(format!(
346 "this composition already has a node named `{PROBE}`"
347 )));
348 }
349 tempo::refuse_unresolved_bars(graph)
350}
351
352pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
353 let mut graph = loaded.map_err(CliError::Refusals)?;
354 tempo::resolve(&mut graph)?;
355 graph.desugar_arrangement().map_err(CliError::Refusals)?;
356 Ok(graph)
357}
358
359pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
361
362pub fn roots_of(source: &dyn Source, target: Option<&str>) -> Result<Vec<String>, CliError> {
364 let mut roots: Vec<String> = RESERVED_VARIABLES
365 .iter()
366 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
367 .collect();
368 match target {
369 None => roots.push(ROOT.to_string()),
370 Some(target) if source.get(target).map_err(CliError::Io)?.is_some() => {
371 roots.push(target.to_string());
372 }
373 Some(text) => match instance_call(text).map(|(path, _)| path) {
374 Some(path) if source.get(path).map_err(CliError::Io)?.is_some() => {
375 roots.push(path.to_string());
376 }
377 _ => {
378 if let Ok(expr) = sva_ast::parse_expr(text) {
379 roots.extend(sva_ast::reads_of(PROBE, &expr));
380 }
381 }
382 },
383 }
384 Ok(roots)
385}
386
387pub fn config_for(
390 graph: &Graph,
391 root: &str,
392 from: Option<WindowEdge>,
393 until: Option<WindowEdge>,
394 sample_rate: Option<u32>,
395) -> Result<RenderConfig, CliError> {
396 let declared = duration::widest_crop(graph, root);
397 let length = match graph.span(root) {
398 Some(span) if span.unit == SpanUnit::Seconds => Some(span.amount),
399 Some(span) => {
400 return Err(CliError::BadTempo(format!(
401 "`{root}`'s span is still {} bars, unresolved; give bpm/meter, or a seconds span",
402 span.amount
403 )));
404 }
405 None => declared.end,
406 };
407 let asked = window_for(from, until, graph.seconds_per_bar())?;
408 let end = match asked.end_secs.is_finite() {
409 true => asked.end_secs,
410 false => length.unwrap_or(DEFAULT_SECONDS),
411 };
412 let stated = !matches!(from, None | Some(WindowEdge::End));
413 let start = match stated {
414 true => asked.start_secs,
415 false => declared.start.unwrap_or(0.0).min(0.0),
416 };
417 if end <= start {
418 return Err(CliError::Usage(format!(
419 "a reading runs from {start}s to {end}s, which is no window; give --to past --from"
420 )));
421 }
422 Ok(RenderConfig {
423 rate: sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
424 horizon: Horizon::secs(start, end),
425 profile: PSYCHOACOUSTIC_V1,
426 asks: Vec::new(),
427 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
428 volatile: Vec::new(),
429 cache_policy: None,
430 })
431}