Skip to main content

sva_cli/
trace.rs

1// Concern: seeds a trace from every entry point and names what the caller asked for | Non-concern: the traversal itself (sva-engine), JSON shape (output.rs) | IO: (dir, target) -> Traced or CliError
2
3use std::path::Path;
4
5use std::collections::BTreeSet;
6
7use sva_ast::{Binds, Expr, Graph, children, resolve_ref_path};
8use sva_core::{CliError, PROBE, define_probe_for, prepared};
9use sva_engine::{EngineError, Traced};
10
11pub struct Traceable {
12    pub bpm: Option<f64>,
13    pub meter: Option<String>,
14    pub traced: Traced,
15}
16
17/// Every entry point is a root, and an uninstantiated bench refuses. A structure has no
18/// time, so an interval the target reads is dropped.
19pub fn trace(dir: &Path, target: &str) -> Result<Traceable, CliError> {
20    let target = match sva_core::target(target)? {
21        sva_core::Target {
22            expr,
23            interval: Some(_),
24        } => expr,
25        _ => target.to_string(),
26    };
27    let target = target.as_str();
28    let mut graph = prepared(&sva_ast::Dir::at(dir))?;
29
30    let mut roots = entry_points(&graph);
31    sung_together(&mut graph, &roots)?;
32    if graph.defines(target)
33        && !roots.iter().any(|r| r == target)
34        && !referenced(&graph).contains(target)
35    {
36        roots.push(target.to_string());
37    }
38    let mut traced = match sva_engine::trace(&graph, &roots, target) {
39        Err(EngineError::UnknownNode(ref name)) if name == target && !graph.defines(target) => {
40            probe(&mut graph, target)?
41        }
42        other => other.map_err(CliError::Engine)?,
43    };
44    let readers = traced.up.iter_mut().map(|up| (&up.node, &mut up.expr));
45    for (node, expr) in std::iter::once((&traced.node, &mut traced.expr)).chain(readers) {
46        if let Some(score) = graph.score(node) {
47            *expr = summary(score);
48        }
49    }
50    let tempo = graph.tempo();
51    Ok(Traceable {
52        bpm: tempo.as_ref().map(|tempo| tempo.bpm),
53        meter: tempo.map(|tempo| tempo.meter),
54        traced,
55    })
56}
57
58/// Every entry point's song lowered together, each at song time 0.
59fn sung_together(graph: &mut Graph, roots: &[String]) -> Result<(), CliError> {
60    const SONGS: &str = "#songs";
61    let reads = roots.iter().map(|root| Expr::Ref {
62        path: root.clone(),
63        arg: Box::new(Expr::Var("t".to_string())),
64        binds: Vec::new(),
65        address: sva_ast::Address::Time,
66        span: sva_ast::ByteSpan::at(0),
67    });
68    let Some(sum) = reads.reduce(|a, b| Expr::Bin(sva_ast::BinOp::Add, Box::new(a), Box::new(b)))
69    else {
70        return Ok(());
71    };
72    graph
73        .define_settled(None, SONGS, sum)
74        .map_err(CliError::Refusals)?;
75    sva_core::sung(graph, SONGS, sva_engine::DEFAULT_SAMPLE_RATE)?;
76    graph.set(SONGS, None);
77    Ok(())
78}
79
80fn summary(score: &sva_ast::Score) -> String {
81    let mut nodes: Vec<&str> = Vec::new();
82    let mut ats = Vec::new();
83    for (_, item) in score.items() {
84        ats.push(item.at().amount.to_f64());
85        if let sva_ast::score::Item::Event(event) = item
86            && !nodes.contains(&event.node.name.as_str())
87        {
88            nodes.push(&event.node.name);
89        }
90    }
91    let spelled = |at: Option<&f64>| {
92        at.map_or("none".to_string(), |at| {
93            sva_ast::score::spelled(*at, score.unit)
94        })
95    };
96    format!(
97        "a score of {} lines, `at` {} to {}, each playing @{} shifted to its `at`",
98        score.lines.len(),
99        spelled(ats.first()),
100        spelled(ats.last()),
101        nodes.join(", @")
102    )
103}
104
105/// `text` is neither node nor instance: argv math, an entry point of its own.
106fn probe(graph: &mut Graph, text: &str) -> Result<Traced, CliError> {
107    define_probe_for(graph, text)?;
108    sva_core::sung(graph, PROBE, sva_engine::DEFAULT_SAMPLE_RATE)?;
109    let roots = entry_points(graph);
110    sva_engine::trace(graph, &roots, PROBE).map_err(CliError::Engine)
111}
112
113fn referenced(graph: &Graph) -> BTreeSet<String> {
114    let mut reached: BTreeSet<String> = BTreeSet::new();
115    for path in graph.paths() {
116        if let Some(expr) = graph.expr(path) {
117            collect_refs(path, expr, &mut reached);
118        }
119    }
120    reached
121}
122
123/// Every node nothing references, bar a library node (one with a free parameter) and a scale,
124/// which is data a score's degrees read and no signal.
125pub(crate) fn entry_points(graph: &Graph) -> Vec<String> {
126    let reached = referenced(graph);
127    graph
128        .paths()
129        .filter(|p| !reached.contains(*p))
130        .filter(|p| !variable(p) && graph.scale(p).is_none())
131        .filter(|p| graph.free_parameters(p).is_empty())
132        .map(str::to_string)
133        .collect()
134}
135
136/// A variable is data a song reads and moves, never a sound of its own.
137pub(crate) fn variable(path: &str) -> bool {
138    path.starts_with(&format!("{}/", sva_ast::VARIABLES))
139}
140
141fn collect_refs(from: &str, expr: &Expr, out: &mut BTreeSet<String>) {
142    if let Expr::Ref { path, .. } = expr
143        && let Some(resolved) = resolve_ref_path(from, path)
144    {
145        out.insert(resolved);
146    }
147    for child in children(expr, Binds::Substitute) {
148        collect_refs(from, child, out);
149    }
150}