Skip to main content

sva_core/
lib.rs

1// Concern: the parse->tempo->render pipeline shared by both front ends | Non-concern: argv (sva-cli), JS bindings (sva-wasm) | IO: (a Source, a target) -> Rendered, Warmed, a Stream or CliError
2
3mod 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::cell::RefCell;
34use std::path::Path;
35
36use sva_ast::{Dir, Graph, Refusal, Source};
37use sva_engine::{
38    Ask, Change, Changed, DEFAULT_SAMPLE_RATE, EngineError, Range, RenderConfig, StreamConfig,
39    render, render_through,
40};
41
42pub use sva_engine::{Handle, Placed, Stream, Until};
43
44pub use sva_engine::{Answer, Extent, Label, Output, Representation};
45pub use sva_engine::{
46    Backend, Cache, CachePolicy, CacheStats, DEFAULT_STORE_BYTES, NoStore, Persisted, PrunePolicy,
47    Store, Through,
48};
49
50pub const ROOT: &str = "master";
51pub const PROBE: &str = "probe";
52
53pub struct Rendered {
54    pub config: RenderConfig,
55    pub expression: String,
56    pub target: String,
57    pub render: sva_engine::Render,
58    /// So a structural check needs no second parse, and an ad-hoc target is the same `probe`.
59    pub graph: Graph,
60}
61
62impl Rendered {
63    pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
64        let written = |e| CliError::Engine(as_written(e, &self.expression));
65        let id = self.render.node(node).map_err(written)?;
66        let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
67        self.attribute(&mut answer)?;
68        Ok(answer)
69    }
70
71    /// An alias score reads beside how many instances the render holds.
72    fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
73        let Output::Alias(alias) = &mut answer.value else {
74            return Ok(());
75        };
76        alias.instances = self.render.tys.paths().count();
77        Ok(())
78    }
79
80    pub fn label(&self) -> Option<&sva_engine::Label> {
81        self.render.labels.get(&self.render.root)
82    }
83}
84
85/// One render: `target` an expression over the composition `source` holds, its own ref
86/// read over an interval where it writes one.
87pub struct Job<'a> {
88    pub source: &'a dyn Source,
89    pub target: &'a str,
90    /// `None` ends the render where the interval or the target's support ends.
91    pub until: Option<&'a str>,
92    pub rate: Option<u32>,
93    pub bits: Option<i32>,
94    pub cache: Option<&'a Cache>,
95    pub cache_policy: Option<CachePolicy>,
96    pub asked: &'a [Asked],
97    /// The operation count the caller acknowledges paying; the profile's own where `None`.
98    pub flop_budget: Option<u128>,
99    pub volatile: &'a [String],
100}
101
102impl<'a> Job<'a> {
103    pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
104        Job {
105            source,
106            target,
107            until: None,
108            rate: None,
109            bits: None,
110            cache: None,
111            cache_policy: None,
112            asked: &[],
113            flop_budget: None,
114            volatile: &[],
115        }
116    }
117}
118
119fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
120    settle_reaching(job, &[])
121}
122
123fn settle_reaching(job: &Job, also: &[String]) -> Result<(Graph, RenderConfig), CliError> {
124    let Target { expr, interval } = target(job.target)?;
125    let mut roots = roots_of(job.source, &expr)?;
126    roots.extend_from_slice(also);
127    let mut graph = settled(sva_ast::load_reaching(
128        job.source,
129        &roots.iter().map(String::as_str).collect::<Vec<_>>(),
130    ))?;
131    let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
132        CliError::BadProbe(format!(
133            "`{}` does not parse as an expression: {}",
134            job.target, d.message
135        ))
136    })?;
137    define_probe(&mut graph, parsed)?;
138    let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
139    let per_bar = graph.seconds_per_bar();
140    let range = match interval {
141        None => Range::default(),
142        Some((start, end)) => Range {
143            start: start.sample(rate, per_bar)?,
144            end: end.sample(rate, per_bar)?,
145        },
146    };
147    if let Range {
148        start: Some(start),
149        end: Some(end),
150    } = range
151        && end <= start
152    {
153        return Err(CliError::Usage(format!(
154            "`{}` reads an interval from sample {start} to {end}, which holds none",
155            job.target
156        )));
157    }
158    let until = match job.until {
159        Some(text) => Some(until(text, rate, per_bar)?),
160        None => None,
161    };
162    let mut config = RenderConfig {
163        range,
164        until,
165        ..RenderConfig::at(rate)
166    };
167    if let Some(budget) = job.flop_budget {
168        config.flop_budget = budget;
169    }
170    if let Some(bits) = job.bits {
171        config.profile.precision_bits = precision(bits)?;
172    }
173    config.volatile = job.volatile.to_vec();
174    config.cache_policy = job.cache_policy;
175    config.asks = job
176        .asked
177        .iter()
178        .map(|asked| Ask {
179            node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
180            representation: asked.representation,
181        })
182        .collect();
183    Ok((graph, config))
184}
185
186/// `trace` names one instance of a parameterized file as `<path>(<name>=<value>, ..)`.
187fn instance_call(text: &str) -> Option<(&str, &str)> {
188    let (path, rest) = text.split_once('(')?;
189    let binds = rest.strip_suffix(')')?.trim();
190    (!binds.is_empty() && all_named(binds)).then_some((path, binds))
191}
192
193/// Only commas and equals outside a bind's own parens count: a bind's value may be a call.
194fn all_named(binds: &str) -> bool {
195    let mut depth = 0i32;
196    let mut named = false;
197    for c in binds.chars() {
198        match c {
199            '(' => depth += 1,
200            ')' if depth == 0 => return false,
201            ')' => depth -= 1,
202            '=' if depth == 0 => named = true,
203            ',' if depth == 0 && !std::mem::take(&mut named) => return false,
204            _ => {}
205        }
206    }
207    named && depth == 0
208}
209
210/// The ref an instance name stands for, so `lint` and `trace` answer for the same node.
211fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
212    let (path, binds) = instance_call(text)?;
213    if !graph.defines(path) {
214        return None;
215    }
216    sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
217}
218
219pub fn execute(job: Job) -> Result<Rendered, CliError> {
220    let (graph, config) = settle(&job)?;
221    let render = render(&graph, PROBE, config, job.cache);
222    rendered(&job, graph, render)
223}
224
225/// `execute` reading through `store` in place of `job`'s own cache; only `persist` writes it.
226pub async fn execute_through<B: Backend>(
227    job: Job<'_>,
228    store: &Store<B>,
229) -> Result<Rendered, CliError> {
230    let (graph, config) = settle(&job)?;
231    let render = render_through(&graph, PROBE, config, store).await;
232    rendered(&job, graph, render)
233}
234
235pub struct Warmed {
236    pub stats: CacheStats,
237    pub readings: Option<Rendered>,
238}
239
240/// Readings, where asked, are `execute_through`'s, the root computed for them.
241pub async fn warm<B: Backend>(job: Job<'_>, store: &Store<B>) -> Result<Warmed, CliError> {
242    if !job.asked.is_empty() {
243        let mut read = execute_through(job, store).await?;
244        let stats = read.render.cache_stats.take();
245        let stats = stats.expect("a render through a store reports on it");
246        return Ok(Warmed {
247            stats,
248            readings: Some(read),
249        });
250    }
251    let (graph, config) = settle(&job)?;
252    let stats = sva_engine::warm(&graph, PROBE, config, store).await;
253    let stats = stats.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
254    Ok(Warmed {
255        stats,
256        readings: None,
257    })
258}
259
260fn rendered(
261    job: &Job,
262    graph: Graph,
263    render: Result<sva_engine::Render, EngineError>,
264) -> Result<Rendered, CliError> {
265    let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
266    Ok(Rendered {
267        config: render.config.clone(),
268        expression: job.target.to_string(),
269        target: PROBE.to_string(),
270        render,
271        graph,
272    })
273}
274
275pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
276    let (graph, config) = settle(job)?;
277    sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
278}
279
280/// Every refusal typing the target reaches, and no render's.
281pub fn types(job: &Job) -> Result<(), CliError> {
282    let (graph, config) = settle(job)?;
283    sva_engine::types_at(&graph, PROBE, config.rate)
284        .map(|_| ())
285        .map_err(|e| CliError::Engine(as_written(e, job.target)))
286}
287
288pub async fn stream(job: &Job<'_>, block: usize, store: &impl Through) -> Result<Stream, CliError> {
289    let (graph, config) = settle(job)?;
290    let target = graph
291        .expr(PROBE)
292        .cloned()
293        .expect("the target was defined as the probe");
294    let config = StreamConfig {
295        block,
296        render: config,
297    };
298    Stream::open(&graph, &target, config, job.cache, store)
299        .await
300        .map_err(|e| CliError::Engine(as_written(e, job.target)))
301}
302
303/// `target`, an expression over `source` with no interval of its own, in place of what
304/// `stream` plays. Each edit here is `sva_engine::change`'s: the stream plays on meanwhile.
305pub async fn edit(
306    stream: &RefCell<Stream>,
307    source: &dyn Source,
308    target: &str,
309    store: &impl Through,
310) -> Result<(), CliError> {
311    let build = |s: &Stream| {
312        let (graph, expr) = streamed(s, source, target)?;
313        Ok(Change::Target(graph, expr))
314    };
315    changed(stream, build, Some(target), store)
316        .await
317        .map(|_| ())
318}
319
320/// `term` summed into the stream's `@notes`, its sample 0 placed `at`.
321pub async fn add(
322    stream: &RefCell<Stream>,
323    source: &dyn Source,
324    (term, at): (&str, Placed),
325    store: &impl Through,
326) -> Result<Handle, CliError> {
327    let build = |s: &Stream| {
328        let (graph, expr) = streamed(s, source, term)?;
329        Ok(Change::Add(graph, expr, at))
330    };
331    match changed(stream, build, Some(term), store).await? {
332        Changed::Added(handle) => Ok(handle),
333        _ => unreachable!("an add answers its handle"),
334    }
335}
336
337/// False where the stream no longer holds `handle`.
338pub async fn replace(
339    stream: &RefCell<Stream>,
340    source: &dyn Source,
341    (handle, term, at): (Handle, &str, Placed),
342    store: &impl Through,
343) -> Result<bool, CliError> {
344    let build = |s: &Stream| {
345        let (graph, expr) = streamed(s, source, term)?;
346        Ok(Change::Replace(handle, graph, expr, at))
347    };
348    Ok(changed(stream, build, Some(term), store).await? == Changed::Held(true))
349}
350
351/// False where the stream no longer holds `handle`.
352pub async fn remove(
353    stream: &RefCell<Stream>,
354    handle: Handle,
355    store: &impl Through,
356) -> Result<bool, CliError> {
357    let build = |_: &Stream| Ok(Change::Remove(handle));
358    Ok(changed(stream, build, None, store).await? == Changed::Held(true))
359}
360
361async fn changed(
362    stream: &RefCell<Stream>,
363    build: impl FnMut(&Stream) -> Result<Change, CliError>,
364    text: Option<&str>,
365    store: &impl Through,
366) -> Result<Changed, CliError> {
367    let changed = sva_engine::change(stream, build, store).await;
368    changed.map_err(|e| match (e, text) {
369        (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
370        (e, _) => e,
371    })
372}
373
374/// `text` with no interval, and a graph reaching it and all the stream plays.
375fn streamed(
376    stream: &Stream,
377    source: &dyn Source,
378    text: &str,
379) -> Result<(Graph, sva_ast::Expr), CliError> {
380    let playing: Vec<String> = stream
381        .exprs()
382        .flat_map(|e| sva_ast::reads_of(PROBE, e))
383        .collect();
384    let (graph, config) = settle_reaching(&Job::over(source, text), &playing)?;
385    if config.range != Range::default() {
386        return Err(CliError::Usage(format!(
387            "`{text}` reads an interval, and a stream keeps its own"
388        )));
389    }
390    let expr = graph
391        .expr(PROBE)
392        .cloned()
393        .expect("the target was defined as the probe");
394    Ok((graph, expr))
395}
396
397/// A double holds no bit past its own mantissa, and one bit writes only zero.
398fn precision(bits: i32) -> Result<i32, CliError> {
399    match (2..=52).contains(&bits) {
400        true => Ok(bits),
401        false => Err(CliError::Usage(format!(
402            "`--bits` takes a precision from 2 to 52 bits, not {bits}"
403        ))),
404    }
405}
406
407/// The engine knows a target only as the node it was defined as, so a refusal that names
408/// that node names the target as the caller wrote it instead.
409fn as_written(refused: EngineError, target: &str) -> EngineError {
410    match refused {
411        EngineError::Refused(mut d) => {
412            if d.location.node == PROBE {
413                d.location.node = target.to_string();
414            }
415            d.message = d
416                .message
417                .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
418            EngineError::Refused(d)
419        }
420        EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
421            node: target.to_string(),
422            span,
423            fault,
424        },
425        other => other,
426    }
427}
428
429pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
430    execute(Job::over(&Dir::at(dir), expression))
431}
432
433pub fn cwd() -> Result<std::path::PathBuf, CliError> {
434    std::env::current_dir()
435        .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
436}
437
438pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
439    settled(sva_ast::load(source))
440}
441
442/// Argv math: `trace` and `lint` each refuse a target no node answers for here, under one
443/// code and one message.
444pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
445    let expr = match instance_read(graph, text) {
446        Some(read) => read,
447        None if names_a_missing_node(text) => {
448            return Err(CliError::NotFound(format!(
449                "`{text}` is not a node this composition defines"
450            )));
451        }
452        None => sva_ast::parse_expr(text).map_err(|d| {
453            CliError::BadProbe(format!(
454                "`{text}` is not a node in this composition, and does not parse as an \
455                 expression: {}",
456                d.message
457            ))
458        })?,
459    };
460    define_probe(graph, expr)
461}
462
463/// Only a path, or instance syntax, is a node; the rest is math the parser and engine answer
464/// for. `t` and `A4` are spelled like paths and read as values.
465fn names_a_missing_node(text: &str) -> bool {
466    match instance_call(text) {
467        Some((path, _)) => sva_ast::names_a_node(path),
468        None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
469    }
470}
471
472fn reads_as_a_value(text: &str) -> bool {
473    match sva_ast::parse_expr(text) {
474        Ok(sva_ast::Expr::Lit(_)) => true,
475        Ok(sva_ast::Expr::Var(name)) => sva_engine::instantiate::is_reserved(&name),
476        _ => false,
477    }
478}
479
480/// Argv math settled the way a file is; `concat` is no file-only dialect.
481fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
482    let defined = graph
483        .define_arranged(PROBE, expr)
484        .map_err(|r| CliError::Refusals(vec![r]))?;
485    if !defined {
486        return Err(CliError::BadProbe(format!(
487            "this composition already has a node named `{PROBE}`"
488        )));
489    }
490    tempo::refuse_unresolved_bars(graph)
491}
492
493pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
494    let mut graph = loaded.map_err(CliError::Refusals)?;
495    tempo::resolve(&mut graph)?;
496    graph.desugar_arrangement().map_err(CliError::Refusals)?;
497    Ok(graph)
498}
499
500/// Read by name rather than through a ref.
501pub const RESERVED_VARIABLES: [&str; 3] = ["bpm", "meter", "key"];
502
503/// A node path the source answers for is a node; anything else is math, and its reads are
504/// roots. Only a node path is asked of the source: math is never looked up as a file.
505pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
506    let mut roots: Vec<String> = RESERVED_VARIABLES
507        .iter()
508        .flat_map(|n| [(*n).to_string(), format!("{}/{n}", sva_ast::VARIABLES)])
509        .collect();
510    let held = |path: &str| -> Result<bool, CliError> {
511        Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
512    };
513    match target {
514        target if held(target)? => {
515            roots.push(target.to_string());
516        }
517        text => match instance_call(text).map(|(path, _)| path) {
518            Some(path) if held(path)? => {
519                roots.push(path.to_string());
520            }
521            _ => {
522                if let Ok(expr) = sva_ast::parse_expr(text) {
523                    roots.extend(sva_ast::reads_of(PROBE, &expr));
524                }
525            }
526        },
527    }
528    Ok(roots)
529}