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    if graph.defines(target)
32        && !roots.iter().any(|r| r == target)
33        && !referenced(&graph).contains(target)
34    {
35        roots.push(target.to_string());
36    }
37    let mut traced = match sva_engine::trace(&graph, &roots, target) {
38        Err(EngineError::UnknownNode(ref name)) if name == target && !graph.defines(target) => {
39            probe(&mut graph, target)?
40        }
41        other => other.map_err(CliError::Engine)?,
42    };
43    let readers = traced.up.iter_mut().map(|up| (&up.node, &mut up.expr));
44    for (node, expr) in std::iter::once((&traced.node, &mut traced.expr)).chain(readers) {
45        if let Some(score) = graph.score(node) {
46            *expr = summary(score);
47        }
48    }
49    let tempo = graph.tempo();
50    Ok(Traceable {
51        bpm: tempo.as_ref().map(|tempo| tempo.bpm),
52        meter: tempo.map(|tempo| tempo.meter),
53        traced,
54    })
55}
56
57/// A score is its lines, not the sum they lower to: how many, when, and what they play.
58fn summary(score: &sva_ast::Score) -> String {
59    let mut nodes: Vec<&str> = Vec::new();
60    for event in &score.events {
61        if !nodes.contains(&event.node.as_str()) {
62            nodes.push(&event.node);
63        }
64    }
65    let (first, last) = (&score.events[0], &score.events[score.events.len() - 1]);
66    format!(
67        "a score of {} lines, `at` {} to {}, each playing @{} shifted to its `at`",
68        score.events.len(),
69        first.at.spelled(),
70        last.at.spelled(),
71        nodes.join(", @")
72    )
73}
74
75/// `text` is neither node nor instance: argv math, an entry point of its own.
76fn probe(graph: &mut Graph, text: &str) -> Result<Traced, CliError> {
77    define_probe_for(graph, text)?;
78    let roots = entry_points(graph);
79    sva_engine::trace(graph, &roots, PROBE).map_err(CliError::Engine)
80}
81
82fn referenced(graph: &Graph) -> BTreeSet<String> {
83    let mut reached: BTreeSet<String> = BTreeSet::new();
84    for path in graph.paths() {
85        if let Some(expr) = graph.expr(path) {
86            collect_refs(path, expr, &mut reached);
87        }
88    }
89    reached
90}
91
92/// Every node nothing references, bar a library node (one with a free parameter) and a scale,
93/// which is data a score's degrees read and no signal.
94pub(crate) fn entry_points(graph: &Graph) -> Vec<String> {
95    let reached = referenced(graph);
96    graph
97        .paths()
98        .filter(|p| !reached.contains(*p))
99        .filter(|p| !reserved_variable(p) && graph.scale(p).is_none())
100        .filter(|p| graph.free_parameters(p).is_empty())
101        .map(str::to_string)
102        .collect()
103}
104
105pub(crate) fn reserved_variable(path: &str) -> bool {
106    sva_core::RESERVED_VARIABLES
107        .iter()
108        .any(|name| sva_ast::variable(name) == path)
109}
110
111fn collect_refs(from: &str, expr: &Expr, out: &mut BTreeSet<String>) {
112    if let Expr::Ref { path, .. } = expr
113        && let Some(resolved) = resolve_ref_path(from, path)
114    {
115        out.insert(resolved);
116    }
117    for child in children(expr, Binds::Substitute) {
118        collect_refs(from, child, out);
119    }
120}