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 Printed, Report, SAMPLE_LIMIT, answer_json, label_json, query_data, stats_json,
18 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, DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PEAKS, DEFAULT_OVERSAMPLE, REPRESENTATIONS,
27 RETIRED, asked, call, calls, is_wav, retired, seconds, 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::path::Path;
34
35use sva_ast::{Dir, Graph, Refusal, Source};
36use sva_engine::{
37 Ask, DEFAULT_SAMPLE_RATE, EngineError, Range, RenderConfig, StreamConfig, render,
38 render_through,
39};
40
41pub use sva_engine::{Handle, QuietTail, Stream, Until};
42
43pub use sva_engine::{Answer, Extent, Label, Output, Representation};
44pub use sva_engine::{
45 Backend, Cache, CachePolicy, DEFAULT_STORE_BYTES, Persisted, PrunePolicy, Store,
46};
47
48pub const ROOT: &str = "master";
49pub const PROBE: &str = "probe";
50
51pub struct Rendered {
52 pub config: RenderConfig,
53 pub expression: String,
54 pub target: String,
55 pub render: sva_engine::Render,
56 pub graph: Graph,
58}
59
60impl Rendered {
61 pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
62 let written = |e| CliError::Engine(as_written(e, &self.expression));
63 let id = self.render.node(node).map_err(written)?;
64 let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
65 self.attribute(&mut answer)?;
66 Ok(answer)
67 }
68
69 fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
71 let Output::Alias(alias) = &mut answer.value else {
72 return Ok(());
73 };
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> {
86 pub source: &'a dyn Source,
87 pub target: &'a str,
88 pub until: Option<&'a str>,
90 pub rate: Option<u32>,
91 pub bits: Option<i32>,
92 pub cache: Option<&'a Cache>,
93 pub cache_policy: Option<CachePolicy>,
94 pub asked: &'a [Asked],
95 pub flop_budget: Option<u128>,
97 pub volatile: &'a [String],
98}
99
100impl<'a> Job<'a> {
101 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
102 Job {
103 source,
104 target,
105 until: None,
106 rate: None,
107 bits: None,
108 cache: None,
109 cache_policy: None,
110 asked: &[],
111 flop_budget: None,
112 volatile: &[],
113 }
114 }
115}
116
117fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
118 settle_reaching(job, &[])
119}
120
121fn settle_reaching(job: &Job, also: &[String]) -> Result<(Graph, RenderConfig), CliError> {
122 let Target { expr, interval } = target(job.target)?;
123 let mut roots = roots_of(job.source, &expr)?;
124 roots.extend_from_slice(also);
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(format!(
152 "`{}` reads an interval from sample {start} to {end}, which holds none",
153 job.target
154 )));
155 }
156 let until = match job.until {
157 Some(text) => Some(until(text, rate, per_bar)?),
158 None => None,
159 };
160 let mut config = RenderConfig {
161 range,
162 until,
163 ..RenderConfig::at(rate)
164 };
165 if let Some(budget) = job.flop_budget {
166 config.flop_budget = budget;
167 }
168 if let Some(bits) = job.bits {
169 config.profile.precision_bits = precision(bits)?;
170 }
171 config.volatile = job.volatile.to_vec();
172 config.cache_policy = job.cache_policy;
173 config.asks = job
174 .asked
175 .iter()
176 .map(|asked| Ask {
177 node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
178 representation: asked.representation,
179 })
180 .collect();
181 Ok((graph, config))
182}
183
184fn instance_call(text: &str) -> Option<(&str, &str)> {
186 let (path, rest) = text.split_once('(')?;
187 let binds = rest.strip_suffix(')')?.trim();
188 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
189}
190
191fn all_named(binds: &str) -> bool {
193 let mut depth = 0i32;
194 let mut named = false;
195 for c in binds.chars() {
196 match c {
197 '(' => depth += 1,
198 ')' if depth == 0 => return false,
199 ')' => depth -= 1,
200 '=' if depth == 0 => named = true,
201 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
202 _ => {}
203 }
204 }
205 named && depth == 0
206}
207
208fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
210 let (path, binds) = instance_call(text)?;
211 if !graph.defines(path) {
212 return None;
213 }
214 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
215}
216
217pub fn execute(job: Job) -> Result<Rendered, CliError> {
218 let (graph, config) = settle(&job)?;
219 let render = render(&graph, PROBE, config, job.cache);
220 rendered(&job, graph, render)
221}
222
223pub async fn execute_through<B: Backend>(
225 job: Job<'_>,
226 store: &Store<B>,
227) -> Result<Rendered, CliError> {
228 let (graph, config) = settle(&job)?;
229 let render = render_through(&graph, PROBE, config, store).await;
230 rendered(&job, graph, render)
231}
232
233fn rendered(
234 job: &Job,
235 graph: Graph,
236 render: Result<sva_engine::Render, EngineError>,
237) -> Result<Rendered, CliError> {
238 let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
239 Ok(Rendered {
240 config: render.config.clone(),
241 expression: job.target.to_string(),
242 target: PROBE.to_string(),
243 render,
244 graph,
245 })
246}
247
248pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
249 let (graph, config) = settle(job)?;
250 sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
251}
252
253pub fn types(job: &Job) -> Result<(), CliError> {
255 let (graph, config) = settle(job)?;
256 sva_engine::types_at(&graph, PROBE, config.rate)
257 .map(|_| ())
258 .map_err(|e| CliError::Engine(as_written(e, job.target)))
259}
260
261pub fn quiet_tails(job: &Job) -> Result<Vec<QuietTail>, CliError> {
263 let (graph, config) = settle(job)?;
264 let found = sva_engine::quiet_tails(&graph, PROBE, config)
265 .map_err(|e| CliError::Engine(as_written(e, job.target)))?;
266 Ok(found.into_iter().filter(|t| t.file != PROBE).collect())
267}
268
269pub fn stream(job: &Job, block: usize) -> Result<Stream, CliError> {
270 let (graph, config) = settle(job)?;
271 let target = graph
272 .expr(PROBE)
273 .cloned()
274 .expect("the target was defined as the probe");
275 let config = StreamConfig {
276 block,
277 render: config,
278 };
279 Stream::open(&graph, &target, config, job.cache)
280 .map_err(|e| CliError::Engine(as_written(e, job.target)))
281}
282
283pub fn edit(stream: &mut Stream, source: &dyn Source, target: &str) -> Result<(), CliError> {
286 let (graph, expr) = streamed(stream, source, target)?;
287 stream
288 .edit(&graph, &expr)
289 .map_err(|e| CliError::Engine(as_written(e, target)))
290}
291
292pub fn add(stream: &mut Stream, source: &dyn Source, term: &str) -> Result<Handle, CliError> {
294 let (graph, expr) = streamed(stream, source, term)?;
295 stream
296 .add(&graph, &expr)
297 .map_err(|e| CliError::Engine(as_written(e, term)))
298}
299
300pub fn replace(
302 stream: &mut Stream,
303 source: &dyn Source,
304 handle: Handle,
305 term: &str,
306) -> Result<bool, CliError> {
307 let (graph, expr) = streamed(stream, source, term)?;
308 stream
309 .replace(&graph, handle, &expr)
310 .map_err(|e| CliError::Engine(as_written(e, term)))
311}
312
313fn streamed(
315 stream: &Stream,
316 source: &dyn Source,
317 text: &str,
318) -> Result<(Graph, sva_ast::Expr), CliError> {
319 let playing: Vec<String> = stream
320 .exprs()
321 .flat_map(|e| sva_ast::reads_of(PROBE, e))
322 .collect();
323 let (graph, config) = settle_reaching(&Job::over(source, text), &playing)?;
324 if config.range != Range::default() {
325 return Err(CliError::Usage(format!(
326 "`{text}` reads an interval, and a stream keeps its own"
327 )));
328 }
329 let expr = graph
330 .expr(PROBE)
331 .cloned()
332 .expect("the target was defined as the probe");
333 Ok((graph, expr))
334}
335
336fn precision(bits: i32) -> Result<i32, CliError> {
338 match (2..=52).contains(&bits) {
339 true => Ok(bits),
340 false => Err(CliError::Usage(format!(
341 "`--bits` takes a precision from 2 to 52 bits, not {bits}"
342 ))),
343 }
344}
345
346fn as_written(refused: EngineError, target: &str) -> EngineError {
349 match refused {
350 EngineError::Refused(mut d) => {
351 if d.location.node == PROBE {
352 d.location.node = target.to_string();
353 }
354 d.message = d
355 .message
356 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
357 EngineError::Refused(d)
358 }
359 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
360 node: target.to_string(),
361 span,
362 fault,
363 },
364 other => other,
365 }
366}
367
368pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
369 execute(Job::over(&Dir::at(dir), expression))
370}
371
372pub fn cwd() -> Result<std::path::PathBuf, CliError> {
373 std::env::current_dir()
374 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
375}
376
377pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
378 settled(sva_ast::load(source))
379}
380
381pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
384 let expr = match instance_read(graph, text) {
385 Some(read) => read,
386 None if names_a_missing_node(text) => {
387 return Err(CliError::NotFound(format!(
388 "`{text}` is not a node this composition defines"
389 )));
390 }
391 None => sva_ast::parse_expr(text).map_err(|d| {
392 CliError::BadProbe(format!(
393 "`{text}` is not a node in this composition, and does not parse as an \
394 expression: {}",
395 d.message
396 ))
397 })?,
398 };
399 define_probe(graph, expr)
400}
401
402fn names_a_missing_node(text: &str) -> bool {
405 match instance_call(text) {
406 Some((path, _)) => sva_ast::names_a_node(path),
407 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
408 }
409}
410
411fn reads_as_a_value(text: &str) -> bool {
412 match sva_ast::parse_expr(text) {
413 Ok(sva_ast::Expr::Lit(_)) => true,
414 Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
415 _ => false,
416 }
417}
418
419fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
421 let defined = graph
422 .define_arranged(PROBE, expr)
423 .map_err(|r| CliError::Refusals(vec![r]))?;
424 if !defined {
425 return Err(CliError::BadProbe(format!(
426 "this composition already has a node named `{PROBE}`"
427 )));
428 }
429 tempo::refuse_unresolved_bars(graph)
430}
431
432pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
433 let mut graph = loaded.map_err(CliError::Refusals)?;
434 tempo::resolve(&mut graph)?;
435 graph.desugar_arrangement().map_err(CliError::Refusals)?;
436 Ok(graph)
437}
438
439pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
441
442pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
445 let mut roots: Vec<String> = RESERVED_VARIABLES
446 .iter()
447 .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
448 .collect();
449 let held = |path: &str| -> Result<bool, CliError> {
450 Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
451 };
452 match target {
453 target if held(target)? => {
454 roots.push(target.to_string());
455 }
456 text => match instance_call(text).map(|(path, _)| path) {
457 Some(path) if held(path)? => {
458 roots.push(path.to_string());
459 }
460 _ => {
461 if let Ok(expr) = sva_ast::parse_expr(text) {
462 roots.extend(sva_ast::reads_of(PROBE, &expr));
463 }
464 }
465 },
466 }
467 Ok(roots)
468}