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