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, 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 set;
13mod target;
14mod until;
15
16pub use answer::{
17    Played, Printed, Report, SAMPLE_LIMIT, answer_json, counters_json, engine_stats_json,
18    label_json, query_data, stats_json, 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, Destination, RETIRED, asked, asked_all, call, calls, is_wav, retired, seconds,
27    wav_path,
28};
29pub use set::{overlaid, set};
30pub use sva_ast::RESERVED_VARIABLES;
31pub use target::{Edge, Target, target};
32pub use until::until;
33
34use std::cell::RefCell;
35use std::num::NonZeroUsize;
36use std::path::Path;
37
38use sva_ast::{Dir, Graph, Refusal, Source};
39use sva_engine::{
40    Ask, Change, Changed, DEFAULT_SAMPLE_RATE, EngineError, Kind, PSYCHOACOUSTIC_V1, Phase, Range,
41    RenderConfig, StreamConfig, render, render_called,
42};
43
44pub use sva_engine::{Abandon, Handle, Hear, Never, Out, Placed, Session, Stream, Until};
45
46pub use sva_engine::{Answer, Extent, Label, Output, Representation};
47pub use sva_engine::{
48    Backend, CacheStats, Counters, DEFAULT_CACHE_BYTES, DEFAULT_STORE_BYTES, EngineStats,
49    FETCH_READS, Nothing, Persisted, Store, Tier,
50};
51
52pub const ROOT: &str = "master";
53pub const PROBE: &str = "probe";
54
55pub struct Rendered {
56    pub config: RenderConfig,
57    pub expression: String,
58    pub target: String,
59    pub render: sva_engine::Render,
60    /// So a structural check needs no second parse, and an ad-hoc target is the same `probe`.
61    pub graph: Graph,
62}
63
64impl Rendered {
65    pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
66        let written = |e| CliError::Engine(as_written(e, &self.expression));
67        let id = self.render.node(node).map_err(written)?;
68        let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
69        self.attribute(&mut answer)?;
70        Ok(answer)
71    }
72
73    /// An alias score reads beside how many instances the render holds.
74    fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
75        let Output::Alias(alias) = &mut answer.value else {
76            return Ok(());
77        };
78        alias.instances = self.render.tys.paths().count();
79        Ok(())
80    }
81
82    pub fn label(&self) -> Option<&sva_engine::Label> {
83        self.render.labels.get(&self.render.root)
84    }
85}
86
87/// One render: `target` an expression over the composition `source` holds, its own ref
88/// read over an interval where it writes one.
89pub struct Job<'a> {
90    pub source: &'a dyn Source,
91    pub target: &'a str,
92    /// `None` ends the render where the interval or the target's support ends.
93    pub until: Option<&'a str>,
94    pub rate: Option<u32>,
95    pub bits: Option<i32>,
96    pub asked: &'a [Asked],
97    pub volatile: &'a [String],
98    pub out: Out,
99    pub abandon: &'a dyn Abandon,
100    pub hear: Option<&'a dyn Hear>,
101    /// The most threads it computes on; `None` takes every core the platform reports.
102    pub threads: Option<NonZeroUsize>,
103}
104
105impl<'a> Job<'a> {
106    pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
107        Job {
108            source,
109            target,
110            until: None,
111            rate: None,
112            bits: None,
113            asked: &[],
114            volatile: &[],
115            out: Out::Kept,
116            abandon: &Never,
117            hear: None,
118            threads: None,
119        }
120    }
121}
122
123fn settle(job: &Job) -> Result<(Graph, RenderConfig), CliError> {
124    let Target { expr, interval } = target(job.target)?;
125    let roots = roots_of(job.source, &expr)?;
126    let mut graph = settled(sva_ast::load_reaching(
127        job.source,
128        &roots.iter().map(String::as_str).collect::<Vec<_>>(),
129    ))?;
130    let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
131        CliError::BadProbe(format!(
132            "`{}` does not parse as an expression: {}",
133            job.target, d.message
134        ))
135    })?;
136    define_probe(&mut graph, parsed)?;
137    let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
138    let per_bar = graph.tempo().map(|tempo| tempo.seconds_per_bar());
139    let range = match interval {
140        None => Range::default(),
141        Some((start, end)) => Range {
142            start: start.sample(rate, per_bar)?,
143            end: end.sample(rate, per_bar)?,
144        },
145    };
146    if let Range {
147        start: Some(start),
148        end: Some(end),
149    } = range
150        && end <= start
151    {
152        return Err(CliError::usage(
153            "render.empty_interval",
154            format!(
155                "`{}` reads an interval from sample {start} to {end}, which holds none",
156                job.target
157            ),
158        ));
159    }
160    let until = match job.until {
161        Some(text) => Some(until(text, rate, per_bar)?),
162        None => None,
163    };
164    let mut config = RenderConfig {
165        range,
166        until,
167        ..RenderConfig::at(rate)
168    };
169    config.profile.precision_bits = precision_bits(job.bits)?;
170    config.volatile = job.volatile.to_vec();
171    config.out = job.out;
172    if let Some(threads) = job.threads {
173        config.threads = threads;
174    }
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, tier: &Tier) -> Result<Rendered, CliError> {
220    let (graph, config) = settle(&job)?;
221    let render = render(&graph, PROBE, config, tier);
222    rendered(&job, graph, render)
223}
224
225/// `job` over `tier`, typing anew only what `session` typed otherwise; only `persist` commits
226/// what it offers a disk beneath.
227pub async fn execute_over<B: Backend>(
228    job: Job<'_>,
229    tier: &Tier<B>,
230    session: &mut Session,
231) -> Result<Rendered, CliError> {
232    let call = tier.call(Kind::Render, job.target, Phase::Parsing);
233    let (graph, config) = settle(&job)?;
234    let hooks = (job.abandon, job.hear);
235    let render = render_called(session, &graph, PROBE, config, tier, hooks, call).await;
236    rendered(&job, graph, render)
237}
238
239fn rendered(
240    job: &Job,
241    graph: Graph,
242    render: Result<sva_engine::Render, EngineError>,
243) -> Result<Rendered, CliError> {
244    let render = render.map_err(|e| CliError::Engine(as_written(e, job.target)))?;
245    Ok(Rendered {
246        config: render.config.clone(),
247        expression: job.target.to_string(),
248        target: PROBE.to_string(),
249        render,
250        graph,
251    })
252}
253
254pub fn plan(job: &Job) -> Result<sva_engine::Render, CliError> {
255    let (graph, config) = settle(job)?;
256    sva_engine::plan(&graph, PROBE, config).map_err(|e| CliError::Engine(as_written(e, job.target)))
257}
258
259/// Every refusal typing the target reaches, and no render's.
260pub fn types(job: &Job) -> Result<(), CliError> {
261    let (graph, config) = settle(job)?;
262    sva_engine::types_at(&graph, PROBE, config.rate)
263        .map(|_| ())
264        .map_err(|e| CliError::Engine(as_written(e, job.target)))
265}
266
267/// `channels`: what it plays, the target's own where `None`.
268pub async fn stream<B: Backend>(
269    job: &Job<'_>,
270    (block, channels): (usize, Option<usize>),
271    tier: &Tier<B>,
272) -> Result<Stream, CliError> {
273    let call = tier.call(Kind::Stream, job.target, Phase::Parsing);
274    let (graph, config) = settle(job)?;
275    let target = graph
276        .expr(PROBE)
277        .cloned()
278        .expect("the target was defined as the probe");
279    let config = StreamConfig {
280        block,
281        channels,
282        render: config,
283    };
284    Stream::open_called(&graph, &target, config, tier, call)
285        .await
286        .map_err(|e| CliError::Engine(as_written(e, job.target)))
287}
288
289/// `target`, an expression over `source` with no interval of its own, in place of what
290/// `stream` plays. Each edit here is `sva_engine::change`'s: the stream plays on meanwhile.
291pub async fn edit<B: Backend>(
292    stream: &RefCell<Stream>,
293    source: &dyn Source,
294    target: &str,
295    tier: &Tier<B>,
296) -> Result<(), CliError> {
297    let build = |s: &Stream| {
298        let (graph, expr) = streamed(s, source, target)?;
299        Ok(Change::Target(graph, expr))
300    };
301    changed(stream, build, Some(target), tier).await.map(|_| ())
302}
303
304/// `term` summed into the stream's `@notes`, its sample 0 placed `at`.
305pub async fn add<B: Backend>(
306    stream: &RefCell<Stream>,
307    source: &dyn Source,
308    (term, at): (&str, Placed),
309    tier: &Tier<B>,
310) -> Result<Handle, CliError> {
311    let build = |s: &Stream| {
312        let (graph, expr) = streamed(s, source, term)?;
313        Ok(Change::Add(graph, expr, at))
314    };
315    match changed(stream, build, Some(term), tier).await? {
316        Changed::Added(handle) => Ok(handle),
317        _ => unreachable!("an add answers its handle"),
318    }
319}
320
321/// False where the stream no longer holds `handle`.
322pub async fn replace<B: Backend>(
323    stream: &RefCell<Stream>,
324    source: &dyn Source,
325    (handle, term, at): (Handle, &str, Placed),
326    tier: &Tier<B>,
327) -> Result<bool, CliError> {
328    let build = |s: &Stream| {
329        let (graph, expr) = streamed(s, source, term)?;
330        Ok(Change::Replace(handle, graph, expr, at))
331    };
332    Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
333}
334
335/// False where the stream no longer holds `handle`.
336pub async fn remove<B: Backend>(
337    stream: &RefCell<Stream>,
338    handle: Handle,
339    tier: &Tier<B>,
340) -> Result<bool, CliError> {
341    let build = |_: &Stream| Ok(Change::Remove(handle));
342    Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
343}
344
345async fn changed<B: Backend>(
346    stream: &RefCell<Stream>,
347    build: impl FnMut(&Stream) -> Result<Change, CliError>,
348    text: Option<&str>,
349    tier: &Tier<B>,
350) -> Result<Changed, CliError> {
351    let changed = sva_engine::change(stream, build, tier).await;
352    changed.map_err(|e| match (e, text) {
353        (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
354        (e, _) => e,
355    })
356}
357
358pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
359    sva_engine::fetch(stream, tier).await;
360}
361
362/// `text` with no interval, and each node it reads that `stream` does not hold, or holds at an
363/// earlier edit of `source`, read and parsed off `source`: an edit is a version the stream plays.
364fn streamed(
365    stream: &Stream,
366    source: &dyn Source,
367    text: &str,
368) -> Result<(Graph, sva_ast::Expr), CliError> {
369    let Target { expr, interval } = target(text)?;
370    if interval.is_some() {
371        return Err(CliError::usage(
372            "stream.interval",
373            format!("`{text}` reads an interval, and a stream keeps its own"),
374        ));
375    }
376    let held = stream.graph();
377    let roots = roots_of(source, &expr)?;
378    let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
379    let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
380    delta
381        .lower_reserved(Some(held))
382        .map_err(CliError::Refusals)?;
383    delta.check_scores(Some(held)).map_err(CliError::Refusals)?;
384    delta
385        .desugar_arrangement_beside(held)
386        .map_err(CliError::Refusals)?;
387    let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
388        CliError::BadProbe(format!(
389            "`{text}` does not parse as an expression: {}",
390            d.message
391        ))
392    })?;
393    let defined = delta
394        .define_arranged_beside(held, PROBE, parsed)
395        .map_err(CliError::Refusals)?;
396    if !defined {
397        return Err(CliError::BadProbe(format!(
398            "this composition already has a node named `{PROBE}`"
399        )));
400    }
401    let expr = delta
402        .expr(PROBE)
403        .cloned()
404        .expect("the target was defined as the probe");
405    Ok((delta, expr))
406}
407
408/// The precision a render writes every sample to: `bits`, else the profile's own.
409pub fn precision_bits(bits: Option<i32>) -> Result<i32, CliError> {
410    bits.map_or(Ok(PSYCHOACOUSTIC_V1.precision_bits), precision)
411}
412
413/// A double holds no bit past its own mantissa, and one bit writes only zero.
414fn precision(bits: i32) -> Result<i32, CliError> {
415    match (2..=52).contains(&bits) {
416        true => Ok(bits),
417        false => Err(CliError::usage(
418            "cli.bad_value",
419            format!("`--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(CliError::Refusals)?;
502    if !defined {
503        return Err(CliError::BadProbe(format!(
504            "this composition already has a node named `{PROBE}`"
505        )));
506    }
507    Ok(())
508}
509
510pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
511    let mut graph = loaded.map_err(CliError::Refusals)?;
512    graph.lower_reserved(None).map_err(CliError::Refusals)?;
513    graph.check_scores(None).map_err(CliError::Refusals)?;
514    graph.desugar_arrangement().map_err(CliError::Refusals)?;
515    Ok(graph)
516}
517
518/// A node path the source answers for is a node; anything else is math, and its reads are
519/// roots. Only a node path is asked of the source: math is never looked up as a file.
520pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
521    let mut roots: Vec<String> = RESERVED_VARIABLES.map(sva_ast::variable).to_vec();
522    let held = |path: &str| -> Result<bool, CliError> {
523        Ok(sva_ast::whole_ref_path(path) && source.get(path).map_err(CliError::Io)?.is_some())
524    };
525    match target {
526        target if held(target)? => {
527            roots.push(target.to_string());
528        }
529        text => match instance_call(text).map(|(path, _)| path) {
530            Some(path) if held(path)? => {
531                roots.push(path.to_string());
532            }
533            _ => {
534                if let Ok(expr) = sva_ast::parse_expr(text) {
535                    roots.extend(sva_ast::reads_of(PROBE, &expr));
536                }
537            }
538        },
539    }
540    Ok(roots)
541}