Skip to main content

sva_ast/
graph.rs

1// Concern: folds a source's node texts into a graph, a song-lowered node kept beside its body | Non-concern: where the texts come from (source.rs), lowering a song (song/) | IO: (&dyn Source) -> Graph
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::Arc;
5
6use crate::diag::{ByteSpan, DiagCode};
7use crate::expr::{Binds, Expr, children, map_children_ok};
8use crate::filename::FileSpan;
9use crate::ingest::{Data, Parsed, base_name, parse_file, parse_score_file};
10use crate::refusal::Refusal;
11use crate::reserved::{Values, variable};
12use crate::score::EXTENSION;
13use crate::skipped::{Skip, Skipped};
14use crate::source::Source;
15
16/// The one reserved folder name; its contents are ordinary nodes, so `@variables/bpm` refs.
17pub const VARIABLES: &str = "variables";
18
19#[derive(Clone, Debug)]
20pub struct Graph {
21    nodes: BTreeMap<String, Arc<Defined>>,
22    data: HashMap<String, Data>,
23    spans: HashMap<String, Option<FileSpan>>,
24    /// The reserved variables each node's own text reads, and the values they were lowered to.
25    reserved: HashMap<String, Vec<&'static str>>,
26    lowered: HashMap<String, Values>,
27    skipped: Vec<Skipped>,
28    /// Each node's text as its source answered it, or as a defined one prints.
29    texts: HashMap<String, String>,
30    origins: HashMap<String, Origin>,
31    /// The edit of its source each node's text was read at, where the source counts them.
32    generations: HashMap<String, u64>,
33}
34
35/// A node's body and the default lines written above it, held whole so a reader may keep it.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Defined {
38    pub body: Expr,
39    pub defaults: Vec<(String, Expr)>,
40}
41
42#[derive(Clone, Debug)]
43enum Origin {
44    Lowered(Arc<Defined>),
45    Made,
46}
47
48/// A node's parse with its text, span and data, as one graph hands it to another.
49#[derive(Clone, Debug)]
50pub struct Held {
51    defined: Arc<Defined>,
52    origin: Option<Origin>,
53    text: String,
54    generation: Option<u64>,
55    span: Option<FileSpan>,
56    data: Option<Data>,
57    reserved: Vec<&'static str>,
58    lowered: Option<Values>,
59}
60
61impl Held {
62    pub fn body(&self) -> &Expr {
63        &self.defined.body
64    }
65}
66
67fn rebind(e: &Expr, to: &dyn Fn(&str) -> Option<Expr>) -> Expr {
68    match e {
69        Expr::Ref {
70            path,
71            arg,
72            binds,
73            address,
74            span,
75        } => Expr::Ref {
76            path: path.clone(),
77            arg: Box::new(rebind(arg, to)),
78            binds: binds
79                .iter()
80                .map(|(k, v)| (k.clone(), to(k).unwrap_or_else(|| rebind(v, to))))
81                .collect(),
82            address: *address,
83            span: *span,
84        },
85        _ => map_children_ok(e, Binds::Substitute, |c| rebind(c, to)),
86    }
87}
88
89impl Graph {
90    /// Every binding of a parameter `to` answers, by a ref or a default, bound to that answer.
91    pub fn rebound(&self, to: &dyn Fn(&str) -> Option<Expr>) -> Graph {
92        let mut out = self.clone();
93        for defined in out.nodes.values_mut() {
94            let defaults = defined.defaults.iter();
95            let defaults =
96                defaults.map(|(k, v)| (k.clone(), to(k).unwrap_or_else(|| rebind(v, to))));
97            *defined = Arc::new(Defined {
98                body: rebind(&defined.body, to),
99                defaults: defaults.collect(),
100            });
101        }
102        out
103    }
104
105    pub fn defines(&self, path: &str) -> bool {
106        self.nodes.contains_key(path)
107    }
108
109    pub fn skipped(&self) -> &[Skipped] {
110        &self.skipped
111    }
112
113    pub fn expr(&self, path: &str) -> Option<&Expr> {
114        self.nodes.get(path).map(|defined| &defined.body)
115    }
116
117    pub fn defined(&self, path: &str) -> Option<&Arc<Defined>> {
118        self.nodes.get(path)
119    }
120
121    pub fn source(&self, path: &str) -> Option<&Arc<Defined>> {
122        match self.origins.get(path) {
123            Some(Origin::Lowered(source)) => Some(source),
124            Some(Origin::Made) => None,
125            None => self.nodes.get(path),
126        }
127    }
128
129    pub fn source_expr(&self, path: &str) -> Option<Expr> {
130        self.source(path).map(|d| d.body.clone())
131    }
132
133    /// Each node as its file holds it, what only a song made gone.
134    pub fn sourced(&self) -> Graph {
135        let mut out = self.clone();
136        for (path, origin) in std::mem::take(&mut out.origins) {
137            match origin {
138                Origin::Lowered(source) => {
139                    out.nodes.insert(path, source);
140                }
141                Origin::Made => {
142                    out.set(&path, None);
143                }
144            }
145        }
146        out
147    }
148
149    pub(crate) fn derive(&mut self, path: &str, defined: Defined) {
150        match self.nodes.get(path) {
151            Some(held) => {
152                if !self.origins.contains_key(path) {
153                    let source = Origin::Lowered(Arc::clone(held));
154                    self.origins.insert(path.to_string(), source);
155                }
156                self.nodes.insert(path.to_string(), Arc::new(defined));
157            }
158            None => {
159                let mut held = self.defining(defined.body);
160                held.defined = Arc::new(Defined {
161                    body: held.defined.body.clone(),
162                    defaults: defined.defaults,
163                });
164                held.origin = Some(Origin::Made);
165                self.set(path, Some(held));
166            }
167        }
168    }
169
170    pub fn text(&self, path: &str) -> Option<&str> {
171        self.texts.get(path).map(String::as_str)
172    }
173
174    pub fn generation(&self, path: &str) -> Option<u64> {
175        self.generations.get(path).copied()
176    }
177
178    /// What an invocation binding nothing for a name gets, in written order. A caller's own
179    /// binding wins, so this is only ever consulted for names the invocation left out.
180    pub fn defaults(&self, path: &str) -> &[(String, Expr)] {
181        self.nodes
182            .get(path)
183            .map_or(&[], |defined| defined.defaults.as_slice())
184    }
185
186    pub fn span(&self, path: &str) -> Option<FileSpan> {
187        self.spans.get(path).copied().flatten()
188    }
189
190    /// The events a score file holds, `None` for every node an equation or a number writes.
191    pub fn score(&self, path: &str) -> Option<&crate::score::Score> {
192        match self.data.get(path)? {
193            Data::Score(score) => Some(score),
194            _ => None,
195        }
196    }
197
198    /// The cents a scale file holds: data a score's degrees read, never a signal.
199    pub fn scale(&self, path: &str) -> Option<&crate::scale::Scale> {
200        match self.data.get(path)? {
201            Data::Scale(scale) => Some(scale),
202            _ => None,
203        }
204    }
205
206    pub(crate) fn reserved_reads(&self, path: &str) -> &[&'static str] {
207        self.reserved.get(path).map_or(&[], Vec::as_slice)
208    }
209
210    pub(crate) fn lowered_under(&self, path: &str) -> Option<&Values> {
211        self.lowered.get(path)
212    }
213
214    pub(crate) fn lower(&mut self, path: &str, defined: Defined, under: &Values) {
215        let held = self.nodes.get_mut(path).expect("a node this graph holds");
216        *Arc::make_mut(held) = defined;
217        self.lowered.insert(path.to_string(), under.clone());
218    }
219
220    /// `from`'s node at `path`, parsed again from its own text, before any pass rewrote it.
221    pub(crate) fn reread(&mut self, path: &str, from: &Graph) -> Result<(), Refusal> {
222        let mut held = from.held(path).expect("a node `from` holds");
223        let parsed = match held.data {
224            Some(Data::Score(_)) => parse_score_file(path, &held.text),
225            _ => parse_file(&held.text),
226        };
227        let parsed = parsed.map_err(|d| Refusal::new(path, d.span, d.code, d.message))?;
228        held.span = crate::filename::parse_filename(base_name(path)).1;
229        held.reserved = crate::reserved::parsed_reads(&parsed, held.span);
230        held.lowered = None;
231        held.defined = Arc::new(Defined {
232            body: parsed.expr,
233            defaults: parsed.defaults,
234        });
235        held.data = parsed.data;
236        self.set(path, Some(held));
237        Ok(())
238    }
239
240    pub fn paths(&self) -> impl Iterator<Item = &str> {
241        self.nodes.keys().map(String::as_str)
242    }
243
244    /// What `variables/bpm` and `variables/meter` hold, where this composition declares them.
245    pub fn tempo(&self) -> Option<crate::reserved::Tempo> {
246        self.values(None)
247            .expect("a lowered graph's reserved variables hold values")
248            .tempo
249    }
250
251    /// Adds a node no file backs — an ad-hoc expression read in this graph's namespace.
252    /// `false` when `path` is taken, so a probe can never shadow a composition's own file.
253    pub fn define(&mut self, path: &str, expr: Expr) -> bool {
254        if self.nodes.contains_key(path) {
255            return false;
256        }
257        let held = self.defining(expr);
258        self.set(path, Some(held));
259        true
260    }
261
262    /// A node no file backs, its reads of reserved variables already lowered.
263    pub fn defining(&self, body: Expr) -> Held {
264        Held {
265            origin: None,
266            text: crate::print::render(&body),
267            generation: None,
268            defined: Arc::new(Defined {
269                body,
270                defaults: Vec::new(),
271            }),
272            span: None,
273            data: None,
274            reserved: Vec::new(),
275            lowered: None,
276        }
277    }
278
279    pub fn held(&self, path: &str) -> Option<Held> {
280        Some(Held {
281            defined: Arc::clone(self.nodes.get(path)?),
282            origin: self.origins.get(path).cloned(),
283            text: self
284                .texts
285                .get(path)
286                .cloned()
287                .expect("every node holds its text"),
288            generation: self.generations.get(path).copied(),
289            span: self.spans.get(path).copied().flatten(),
290            data: self.data.get(path).cloned(),
291            reserved: self.reserved_reads(path).to_vec(),
292            lowered: self.lowered.get(path).cloned(),
293        })
294    }
295
296    pub fn set(&mut self, path: &str, held: Option<Held>) -> Option<Held> {
297        let before = self.held(path);
298        match held {
299            Some(held) => {
300                self.nodes.insert(path.to_string(), held.defined);
301                self.texts.insert(path.to_string(), held.text);
302                match held.generation {
303                    Some(at) => self.generations.insert(path.to_string(), at),
304                    None => self.generations.remove(path),
305                };
306                self.spans.insert(path.to_string(), held.span);
307                match held.data {
308                    Some(data) => self.data.insert(path.to_string(), data),
309                    None => self.data.remove(path),
310                };
311                match held.reserved.is_empty() {
312                    true => self.reserved.remove(path),
313                    false => self.reserved.insert(path.to_string(), held.reserved),
314                };
315                match held.lowered {
316                    Some(values) => self.lowered.insert(path.to_string(), values),
317                    None => self.lowered.remove(path),
318                };
319                match held.origin {
320                    Some(origin) => self.origins.insert(path.to_string(), origin),
321                    None => self.origins.remove(path),
322                };
323            }
324            None => {
325                self.nodes.remove(path);
326                self.texts.remove(path);
327                self.generations.remove(path);
328                self.spans.remove(path);
329                self.data.remove(path);
330                self.reserved.remove(path);
331                self.lowered.remove(path);
332                self.origins.remove(path);
333            }
334        }
335        before
336    }
337
338    pub fn holds_as(&self, path: &str, other: &Graph) -> bool {
339        match (self.nodes.get(path), other.nodes.get(path)) {
340            (Some(a), Some(b)) => Arc::ptr_eq(a, b) || a == b,
341            (None, None) => true,
342            _ => false,
343        }
344    }
345
346    pub fn read_as(&self, path: &str, other: &Graph) -> bool {
347        self.texts.get(path) == other.texts.get(path)
348            && self.generations.get(path) == other.generations.get(path)
349    }
350
351    /// Each node `path` reads directly, by its body, a default or a reserved variable.
352    fn reaching_from(&self, path: &str) -> Vec<String> {
353        let Some(defined) = self.nodes.get(path) else {
354            return Vec::new();
355        };
356        let mut out = reads_of(path, &defined.body);
357        for (_, value) in &defined.defaults {
358            out.extend(reads_of(path, value));
359        }
360        out.extend(self.reserved_reads(path).iter().map(|name| variable(name)));
361        out
362    }
363
364    pub fn reaching(&self, roots: &[String]) -> BTreeSet<String> {
365        let (mut seen, mut open) = (BTreeSet::new(), roots.to_vec());
366        while let Some(path) = open.pop() {
367            if self.defines(&path) && seen.insert(path.clone()) {
368                open.extend(self.reaching_from(&path));
369            }
370        }
371        seen
372    }
373
374    pub fn adopt(&mut self, from: &Graph, roots: &[String]) -> Vec<String> {
375        let lacking: Vec<String> = from
376            .reaching(roots)
377            .into_iter()
378            .filter(|path| !self.defines(path))
379            .collect();
380        for path in &lacking {
381            self.set(path, from.held(path));
382        }
383        lacking
384    }
385
386    pub fn define_settled(
387        &mut self,
388        beside: Option<&Graph>,
389        path: &str,
390        expr: Expr,
391    ) -> Result<bool, Vec<Refusal>> {
392        if self.defines(path) {
393            return Ok(false);
394        }
395        let written = crate::series::written_out(&expr)
396            .map_err(|d| vec![Refusal::new(path, d.span, d.code, d.message)])?;
397        let mut defined = Defined {
398            body: written,
399            defaults: Vec::new(),
400        };
401        if !crate::reserved::reads(&defined.body, &[], None).is_empty() {
402            let values = self.values(beside)?;
403            defined = self.lowered(path, &defined, &values)?;
404        }
405        Ok(self.define(path, defined.body))
406    }
407}
408
409pub(crate) fn variable_text<'g>(graph: &'g Graph, name: &str) -> Option<&'g str> {
410    let text = graph.text(&format!("{VARIABLES}/{name}"))?;
411    Some(&text[crate::ingest::body_at(text)..])
412}
413
414/// Every node the source names. What a whole composition can be told about itself — `lint`, an
415/// entry-point set — needs all of them, so this is what a directory load is.
416pub fn load(source: &dyn Source) -> Result<Graph, Vec<Refusal>> {
417    let mut loading = Loading::new(source.name());
418    let listing = source
419        .paths()
420        .map_err(|reason| vec![loading.directory(reason)])?;
421    for path in &listing.found {
422        let node = path.strip_suffix(EXTENSION).unwrap_or(path);
423        match names_a_node(node) {
424            true if !loading.holds(node) => {
425                loading.pull(source, node);
426            }
427            true => {}
428            false => loading.skipped.push(Skipped {
429                path: path.clone(),
430                reason: Skip::Unnameable,
431            }),
432        }
433    }
434    for path in &listing.unreadable {
435        loading.skipped.push(Skipped {
436            path: path.clone(),
437            reason: Skip::Special,
438        });
439    }
440    loading.finish(None)
441}
442
443/// Whether anything names this path: a ref path, or one an instance's binds follow, which
444/// a render still addresses by name though no `@ref` spells it.
445pub fn names_a_node(path: &str) -> bool {
446    let bare = match path.split_once('(') {
447        Some((before, binds)) if binds.ends_with(')') => before,
448        _ => path,
449    };
450    !bare.is_empty() && crate::lexer::whole_ref_path(bare)
451}
452
453/// The root set [`load_reaching`] follows, for a caller whose expression is not in the source
454/// yet. A bareword call names a file too, so every call name is named speculatively.
455pub fn reads_of(referencing: &str, expr: &Expr) -> Vec<String> {
456    let mut named = Vec::new();
457    collect_named(expr, &mut named);
458    named
459        .iter()
460        .filter_map(|name| resolve_ref_path(referencing, name))
461        .collect()
462}
463
464/// Only what `roots` reach, following each parsed node's own reads: an unreferenced node is
465/// never asked for, and one the source cannot answer for is a located dangling-ref refusal.
466pub fn load_reaching(source: &dyn Source, roots: &[&str]) -> Result<Graph, Vec<Refusal>> {
467    let mut loading = Loading::new(source.name());
468    let mut work: Vec<String> = roots.iter().rev().map(|r| (*r).to_string()).collect();
469    while let Some(path) = work.pop() {
470        if loading.holds(&path) || !loading.pull(source, &path) {
471            continue;
472        }
473        work.extend(loading.reads(&path));
474    }
475    loading.finish(None)
476}
477
478/// What `roots` reach that `base` does not hold, or holds at an earlier edit of `source`:
479/// each node read and parsed once, a ref into `base` resolving there. A node `base` holds as
480/// `source` still does is walked through, never read.
481pub fn load_beside(
482    source: &dyn Source,
483    roots: &[&str],
484    base: &Graph,
485) -> Result<Graph, Vec<Refusal>> {
486    let mut loading = Loading::new(source.name());
487    let mut walked = BTreeSet::new();
488    let mut work: Vec<String> = roots.iter().rev().map(|r| (*r).to_string()).collect();
489    while let Some(path) = work.pop() {
490        let current = base.defines(&path)
491            && generation(source, &path).is_none_or(|at| base.generation(&path) == Some(at));
492        if current {
493            if walked.insert(path.clone()) {
494                work.extend(base.reaching_from(&path));
495            }
496            continue;
497        }
498        if loading.holds(&path) || !loading.pull(source, &path) {
499            continue;
500        }
501        work.extend(loading.reads(&path));
502    }
503    loading.finish(Some(base))
504}
505
506/// A body or default reading `sample("<path>")` holds the recording the source answers for.
507fn recorded(mut parsed: Parsed, source: &dyn Source) -> Result<Parsed, crate::diag::Diag> {
508    let unheld = |e: crate::recording::Unheld| {
509        crate::diag::Diag::new(DiagCode::MissingRecording, ByteSpan::at(0), e.message())
510    };
511    parsed.expr = crate::recording::resolve_recordings(&parsed.expr, source).map_err(unheld)?;
512    for (_, value) in &mut parsed.defaults {
513        *value = crate::recording::resolve_recordings(value, source).map_err(unheld)?;
514    }
515    Ok(parsed)
516}
517
518fn generation(source: &dyn Source, path: &str) -> Option<u64> {
519    source
520        .generation(path)
521        .or_else(|| source.generation(&format!("{path}{EXTENSION}")))
522}
523
524/// One parsed node at a time, so pulling everything and pulling a closure are the same walk.
525struct Loading {
526    name: String,
527    nodes: BTreeMap<String, Expr>,
528    defaults: BTreeMap<String, Vec<(String, Expr)>>,
529    data: HashMap<String, Data>,
530    spans: HashMap<String, Option<FileSpan>>,
531    missing: BTreeSet<String>,
532    refusals: Vec<Refusal>,
533    skipped: Vec<Skipped>,
534    texts: HashMap<String, String>,
535    generations: HashMap<String, u64>,
536    reserved: HashMap<String, Vec<&'static str>>,
537}
538
539impl Loading {
540    fn new(name: String) -> Loading {
541        Loading {
542            name,
543            nodes: BTreeMap::new(),
544            defaults: BTreeMap::new(),
545            data: HashMap::new(),
546            spans: HashMap::new(),
547            missing: BTreeSet::new(),
548            refusals: Vec::new(),
549            skipped: Vec::new(),
550            texts: HashMap::new(),
551            generations: HashMap::new(),
552            reserved: HashMap::new(),
553        }
554    }
555
556    fn directory(&self, reason: String) -> Refusal {
557        Refusal::new(self.name.clone(), ByteSpan::at(0), DiagCode::Io, reason)
558    }
559
560    /// A miss counts as settled: a builtin name every node calls would otherwise cost the
561    /// source one failed read per referencing node.
562    fn holds(&self, path: &str) -> bool {
563        self.nodes.contains_key(path)
564            || self.spans.contains_key(path)
565            || self.missing.contains(path)
566    }
567
568    /// `false` where nothing was added: the source does not hold this path, or its text is not
569    /// a node — and the second is already recorded as its own located refusal.
570    fn pull(&mut self, source: &dyn Source, path: &str) -> bool {
571        let file = format!("{path}{EXTENSION}");
572        let read = |at: &str| {
573            source
574                .get(at)
575                .map(|text| text.map(|text| text.into_owned()))
576        };
577        let (text, score) = match (read(path), read(&file)) {
578            (Ok(Some(_)), Ok(Some(_))) => {
579                self.refusals.push(
580                    Refusal::new(
581                        file.as_str(),
582                        ByteSpan::at(0),
583                        DiagCode::BadScore,
584                        format!(
585                            "line 1, column 1: `@{path}` matches two files, `{path}` and \
586                             `{file}`; a ref names one file, so rename or remove one of them"
587                        ),
588                    )
589                    .placed((1, 1)),
590                );
591                self.spans.insert(path.to_string(), None);
592                return false;
593            }
594            (Ok(Some(text)), Ok(None)) => (text, false),
595            (Ok(None), Ok(Some(text))) => (text, true),
596            (Ok(None), Ok(None)) => {
597                self.missing.insert(path.to_string());
598                return false;
599            }
600            (Err(reason), _) | (_, Err(reason)) => {
601                self.refusals
602                    .push(Refusal::new(path, ByteSpan::at(0), DiagCode::Io, reason));
603                self.spans.insert(path.to_string(), None);
604                return false;
605            }
606        };
607        let (_, span) = crate::filename::parse_filename(base_name(path));
608        self.spans.insert(path.to_string(), span);
609        self.texts.insert(path.to_string(), text.clone());
610        let held = if score { file.as_str() } else { path };
611        if let Some(at) = generation(source, path) {
612            self.generations.insert(path.to_string(), at);
613        }
614        let parsed = match score {
615            true => parse_score_file(path, &text),
616            false => parse_file(&text).and_then(|parsed| recorded(parsed, source)),
617        };
618        match parsed {
619            Ok(parsed) => {
620                let reads = crate::reserved::parsed_reads(&parsed, span);
621                if !reads.is_empty() {
622                    self.reserved.insert(path.to_string(), reads);
623                }
624                self.nodes.insert(path.to_string(), parsed.expr);
625                if !parsed.defaults.is_empty() {
626                    self.defaults.insert(path.to_string(), parsed.defaults);
627                }
628                if let Some(data) = parsed.data {
629                    self.data.insert(path.to_string(), data);
630                }
631                true
632            }
633            Err(d) => {
634                let refusal = Refusal::new(held, d.span, d.code, d.message);
635                self.refusals.push(match score {
636                    true => refusal.placed(crate::score::line_column(&text, d.span.start)),
637                    false => refusal,
638                });
639                false
640            }
641        }
642    }
643
644    fn reads(&self, path: &str) -> Vec<String> {
645        let mut out = match self.nodes.get(path) {
646            Some(expr) => reads_of(path, expr),
647            None => Vec::new(),
648        };
649        let reserved = self.reserved.get(path).into_iter().flatten();
650        out.extend(reserved.map(|name| variable(name)));
651        out
652    }
653
654    /// A refusal in a score names its file, line and column.
655    fn on_its_line(&self, refusal: Refusal) -> Refusal {
656        let path = refusal.at.path.as_str();
657        match (self.data.get(path), self.texts.get(path)) {
658            (Some(Data::Score(_)), Some(text)) => crate::score::refused(
659                format!("{path}{EXTENSION}"),
660                text,
661                refusal.at.span.start,
662                crate::diag::DiagCode::BadScore,
663                &refusal.reason,
664            ),
665            _ => refusal,
666        }
667    }
668
669    /// Every ref resolving here or in `base`.
670    fn finish(self, base: Option<&Graph>) -> Result<Graph, Vec<Refusal>> {
671        if !self.refusals.is_empty() {
672            return Err(self.refusals);
673        }
674        let held =
675            |path: &str| self.nodes.contains_key(path) || base.is_some_and(|b| b.defines(path));
676        let is_score = |path: &str| matches!(self.data.get(path), Some(Data::Score(_)));
677        let dangling: Vec<Refusal> = check_refs((&self.nodes, &held), &self.texts, &self.name)
678            .into_iter()
679            .filter(|r| !(is_score(&r.at.path) && r.reason.contains(&format!("`{VARIABLES}/"))))
680            .collect();
681        if !dangling.is_empty() {
682            return Err(dangling.into_iter().map(|r| self.on_its_line(r)).collect());
683        }
684        let cycles = check_cycles(&self.nodes);
685        if !cycles.is_empty() {
686            let placed = |r: Refusal| match is_score(&r.at.path) {
687                true => Refusal {
688                    reason: format!(
689                        "a score places itself, directly or through others: `{}` places what \
690                         places it back ({})",
691                        r.at.path, r.reason
692                    ),
693                    ..r
694                },
695                false => r,
696            };
697            return Err(cycles
698                .into_iter()
699                .map(|r| self.on_its_line(placed(r)))
700                .collect());
701        }
702        let mut defaults = self.defaults;
703        let nodes = self.nodes.into_iter().map(|(path, body)| {
704            let defaults = defaults.remove(&path).unwrap_or_default();
705            (path, Arc::new(Defined { body, defaults }))
706        });
707        Ok(Graph {
708            nodes: nodes.collect(),
709            data: self.data,
710            spans: self.spans,
711            reserved: self.reserved,
712            lowered: HashMap::new(),
713            skipped: self.skipped,
714            texts: self.texts,
715            generations: self.generations,
716            origins: HashMap::new(),
717        })
718    }
719}
720
721/// Resolves against the REFERENCING file's own directory, `./` and a bare path alike. A `.`
722/// or `..` normalizes anywhere, so `@a/x/../b` and `@a/b` are one node. `None` above the root,
723/// and for an absolute path, which only a command line resolves.
724pub fn resolve_ref_path(referencing: &str, ref_path: &str) -> Option<String> {
725    if ref_path.starts_with('/') {
726        return None;
727    }
728    let dir = match referencing.rsplit_once('/') {
729        Some((dir, _)) => dir,
730        None => "",
731    };
732    let mut segments: Vec<&str> = dir.split('/').filter(|s| !s.is_empty()).collect();
733    for segment in ref_path.split('/') {
734        match segment {
735            "." => {}
736            ".." => {
737                segments.pop()?;
738            }
739            other => segments.push(other),
740        }
741    }
742    Some(segments.join("/"))
743}
744
745fn above_root(referencing: &str, span: ByteSpan, ref_path: &str, root: &str) -> Refusal {
746    Refusal::new(
747        referencing,
748        span,
749        DiagCode::RefAboveRoot,
750        format!(
751            "`@{ref_path}` in `{referencing}` walks above the composition root `{root}`; a \
752             ref may reach anywhere inside the root and nowhere outside it"
753        ),
754    )
755}
756
757/// One `@path(...)` read, and whether it reads at the referencing node's own `t`.
758struct RefSite {
759    path: String,
760    span: ByteSpan,
761    at_t: bool,
762}
763
764fn collect_refs(e: &Expr, out: &mut Vec<RefSite>) {
765    if let Expr::Ref {
766        path, arg, span, ..
767    } = e
768    {
769        out.push(RefSite {
770            path: path.clone(),
771            span: *span,
772            at_t: **arg == Expr::Var("t".to_string()),
773        });
774    }
775    for c in children(e, Binds::Substitute) {
776        collect_refs(c, out);
777    }
778}
779
780/// Every path this expression names, `@ref` or bareword call alike.
781fn collect_named(e: &Expr, out: &mut Vec<String>) {
782    match e {
783        Expr::Call { name, .. } => out.push(name.clone()),
784        Expr::Ref { path, .. } => out.push(path.clone()),
785        _ => {}
786    }
787    for c in children(e, Binds::Substitute) {
788        collect_named(c, out);
789    }
790}
791
792type Holds<'a> = &'a dyn Fn(&str) -> bool;
793
794fn check_refs(
795    (nodes, held): (&BTreeMap<String, Expr>, Holds),
796    texts: &HashMap<String, String>,
797    root: &str,
798) -> Vec<Refusal> {
799    let mut refusals = Vec::new();
800    for (path, expr) in nodes {
801        let mut refs = Vec::new();
802        collect_refs(expr, &mut refs);
803        for site in refs {
804            let Some(target) = resolve_ref_path(path, &site.path) else {
805                refusals.push(above_root(path, site.span, &site.path, root));
806                continue;
807            };
808            if !held(&target) {
809                let hint = texts
810                    .get(path)
811                    .and_then(|text| digit_segment_hint(text, site.span.end))
812                    .map(|seg| {
813                        format!(
814                            "; a path segment cannot begin with a digit, so `{seg}` was not \
815                             read as part of this reference"
816                        )
817                    })
818                    .unwrap_or_default();
819                refusals.push(Refusal::new(
820                    path.clone(),
821                    site.span,
822                    DiagCode::DanglingRef,
823                    format!(
824                        "`@{}` does not resolve to any file (looked for `{target}`){hint}",
825                        site.path
826                    ),
827                ));
828            }
829        }
830    }
831    refusals
832}
833
834/// `end` is where the lexer's digit-segment rule stopped: on a `/` whose next byte is a digit.
835/// Re-inspecting the source there recovers the segment the path lost, e.g. `/1-raw`.
836fn digit_segment_hint(text: &str, end: usize) -> Option<String> {
837    let b = text.as_bytes();
838    if b.get(end) != Some(&b'/') || !b.get(end + 1).is_some_and(u8::is_ascii_digit) {
839        return None;
840    }
841    let mut j = end + 1;
842    while j < b.len() && (b[j].is_ascii_alphanumeric() || matches!(b[j], b'_' | b'-')) {
843        j += 1;
844    }
845    Some(text[end..j].to_string())
846}
847
848#[derive(Clone, Copy, PartialEq)]
849enum Color {
850    White,
851    Gray,
852    Black,
853}
854
855/// A node's resolved, in-graph edges (dangling refs are already refused before this runs).
856fn resolved_refs<'a>(node: &str, nodes: &'a BTreeMap<String, Expr>) -> Vec<Edge<'a>> {
857    let mut raw = Vec::new();
858    if let Some(expr) = nodes.get(node) {
859        collect_refs(expr, &mut raw);
860    }
861    raw.into_iter()
862        .filter_map(|site| {
863            let target = resolve_ref_path(node, &site.path)?;
864            nodes.get_key_value(&target).map(|(k, _)| Edge {
865                target: k.as_str(),
866                span: site.span,
867                at_t: site.at_t,
868            })
869        })
870        .collect()
871}
872
873#[derive(Clone, Copy)]
874struct Edge<'a> {
875    target: &'a str,
876    span: ByteSpan,
877    at_t: bool,
878}
879
880struct Frame<'a> {
881    node: &'a str,
882    refs: Vec<Edge<'a>>,
883    idx: usize,
884}
885
886/// An explicit-stack DFS: a hostile, arbitrarily long acyclic ref chain walks in a heap-sized
887/// `Vec`, never the OS call stack. It walks ONLY the reads written at bare `t`, so a cycle in
888/// it carries no delay at any sample rate; one holding a shifted read is the engine's to settle.
889fn check_cycles(nodes: &BTreeMap<String, Expr>) -> Vec<Refusal> {
890    let mut color: HashMap<&str, Color> =
891        nodes.keys().map(|k| (k.as_str(), Color::White)).collect();
892    let mut refusals = Vec::new();
893    let undelayed = |node: &str| -> Vec<Edge<'_>> {
894        resolved_refs(node, nodes)
895            .into_iter()
896            .filter(|e| e.at_t)
897            .collect()
898    };
899
900    for start in nodes.keys().map(String::as_str) {
901        if color[start] != Color::White {
902            continue;
903        }
904        color.insert(start, Color::Gray);
905        let mut stack = vec![Frame {
906            node: start,
907            refs: undelayed(start),
908            idx: 0,
909        }];
910
911        while let Some(frame) = stack.last_mut() {
912            if frame.idx >= frame.refs.len() {
913                color.insert(frame.node, Color::Black);
914                stack.pop();
915                continue;
916            }
917            let edge = frame.refs[frame.idx];
918            let from = frame.node;
919            frame.idx += 1;
920            match color.get(edge.target) {
921                Some(Color::Gray) => refusals.push(Refusal::new(
922                    from,
923                    edge.span,
924                    DiagCode::RefCycle,
925                    format!(
926                        "a delay-free ref cycle: `{from}` reads `@{}` at its own `t`, and the \
927                         reads leading back to `{from}` are all at `t` too, so this value is its \
928                         own argument. A loop is well-founded only when some read on it reaches \
929                         strictly into the past",
930                        edge.target
931                    ),
932                )),
933                Some(Color::White) => {
934                    color.insert(edge.target, Color::Gray);
935                    stack.push(Frame {
936                        node: edge.target,
937                        refs: undelayed(edge.target),
938                        idx: 0,
939                    });
940                }
941                _ => {}
942            }
943        }
944    }
945    refusals
946}
947
948#[cfg(test)]
949mod tests {
950    use super::*;
951
952    /// The graph's own semantics have nothing to do with a filesystem, so they are stated over
953    /// the in-memory form and a directory is tested where it is read (`fixtures.rs`).
954    fn of(files: &[(&str, &str)]) -> crate::source::Composition {
955        files.iter().copied().collect()
956    }
957
958    /// A source that counts what was asked of it: the pull property is not observable from the
959    /// graph, only from what the source was never opened for.
960    struct Counting {
961        held: crate::source::Composition,
962        asked: std::cell::RefCell<Vec<String>>,
963    }
964
965    impl Source for Counting {
966        fn paths(&self) -> Result<crate::source::Listing, String> {
967            panic!("a pull load must never enumerate the source")
968        }
969        fn get(&self, path: &str) -> Result<Option<std::borrow::Cow<'_, str>>, String> {
970            self.asked.borrow_mut().push(path.to_string());
971            self.held.get(path)
972        }
973    }
974
975    #[test]
976    fn a_pull_load_never_asks_for_a_node_the_roots_do_not_reach() {
977        let source = Counting {
978            held: of(&[
979                (
980                    "song",
981                    "sin(2*pi*t)*0.1 + @drums/kick*0.5 + gain(@drums/kick, k=2)\n",
982                ),
983                ("drums/kick", "sin(2*pi*50*t)\n"),
984                ("gain", "x*k*sin(2*pi*t)\n"),
985                ("zombie", "@nowhere\n"),
986            ]),
987            asked: std::cell::RefCell::new(Vec::new()),
988        };
989        let g = load_reaching(&source, &["song"]).expect("the reached set is whole");
990        let mut reached: Vec<&str> = g.paths().collect();
991        reached.sort();
992        assert_eq!(reached, ["drums/kick", "gain", "song"]);
993        let asked = source.asked.borrow().clone();
994        assert!(
995            !asked.contains(&"zombie".to_string()),
996            "an unreferenced node was read: {asked:?}"
997        );
998        assert!(
999            asked.contains(&"gain".to_string()),
1000            "a bareword invocation names a file too: {asked:?}"
1001        );
1002        let mut once = asked.clone();
1003        once.sort();
1004        once.dedup();
1005        assert_eq!(
1006            once.len(),
1007            asked.len(),
1008            "`sin` is called from two root nodes and must cost one read: {asked:?}"
1009        );
1010    }
1011
1012    /// Nodes arriving one at a time is the browser's shape, and a set still short of whole is
1013    /// the dangling-ref refusal it already was, not a new failure mode.
1014    #[test]
1015    fn a_composition_built_node_by_node_refuses_until_the_reached_set_is_whole() {
1016        let mut c = crate::source::Composition::new();
1017        c.insert("song", "@drums/kick*0.5\n");
1018        let errs = load_reaching(&c, &["song"]).unwrap_err();
1019        assert!(errs.iter().any(|r| r.code == DiagCode::DanglingRef));
1020
1021        c.insert("drums/kick", "sin(2*pi*50*t)\n");
1022        let g = load_reaching(&c, &["song"]).expect("the last node arrived");
1023        assert!(g.expr("drums/kick").is_some());
1024    }
1025
1026    /// The lexer's digit-segment rule truncates `@../growl/1-raw` to `@../growl`, and the
1027    /// dangling ref must say so, not just "looked for `growl`".
1028    #[test]
1029    fn a_dangling_ref_truncated_by_the_digit_segment_rule_names_what_fell_off() {
1030        let g = of(&[
1031            ("tracks/melody", "@../growl/1-raw\n"),
1032            ("growl/other", "0.2\n"),
1033        ]);
1034        let errs = load(&g).unwrap_err();
1035        let err = errs
1036            .iter()
1037            .find(|r| r.code == DiagCode::DanglingRef)
1038            .expect("the truncated ref must dangle");
1039        assert!(err.reason.contains("looked for `growl`"), "{}", err.reason);
1040        assert!(
1041            err.reason.contains("digit") && err.reason.contains("/1-raw"),
1042            "must explain the truncation, not just report `growl`: {}",
1043            err.reason
1044        );
1045    }
1046
1047    #[test]
1048    fn cross_file_refs_resolve() {
1049        let files: Vec<(&str, &str)> = vec![("kick", "sin(2*pi*50*t)\n"), ("lead", "@kick*0.5\n")];
1050        let g = load(&of(&files)).unwrap();
1051        assert!(g.expr("kick").is_some());
1052        assert!(g.defines("lead"));
1053    }
1054
1055    #[test]
1056    fn same_file_cycle_is_refused() {
1057        let files: Vec<(&str, &str)> = vec![("a", "@a + 1\n")];
1058        let errs = load(&of(&files)).unwrap_err();
1059        assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1060    }
1061
1062    #[test]
1063    fn a_node_merely_referencing_a_cycle_member_is_not_itself_flagged() {
1064        let files: Vec<(&str, &str)> = vec![("a", "@b\n"), ("b", "@a\n"), ("c", "@b\n")];
1065        let errs = load(&of(&files)).unwrap_err();
1066        assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1067        assert!(
1068            !errs.iter().any(|r| r.at.path == "c"),
1069            "c only reaches into the cycle, it does not loop back to itself: {errs:?}"
1070        );
1071    }
1072
1073    /// The relaxation this file owns: a loop with a shifted read on it is left for the engine,
1074    /// which alone knows how many samples that shift is worth.
1075    #[test]
1076    fn a_mutual_cycle_with_a_shifted_read_on_it_parses() {
1077        let files: Vec<(&str, &str)> =
1078            vec![("a", "@b(t - 0.01s)*0.5\n"), ("b", "@a*0.5 + sin(t)\n")];
1079        let g = load(&of(&files)).unwrap();
1080        assert!(g.expr("a").is_some() && g.expr("b").is_some());
1081
1082        let files: Vec<(&str, &str)> = vec![("ring", "@ring(t - 0.002s)*0.9 + sin(t)\n")];
1083        assert!(load(&of(&files)).is_ok());
1084    }
1085
1086    /// And the half that still refuses, located, whatever the sample rate turns out to be.
1087    #[test]
1088    fn a_cycle_read_only_at_bare_t_is_refused_where_the_read_is_written() {
1089        let files: Vec<(&str, &str)> =
1090            vec![("a", "@b(t - 0.01s)\n"), ("b", "@c\n"), ("c", "@b + 1\n")];
1091        let errs = load(&of(&files)).unwrap_err();
1092        let r = errs
1093            .iter()
1094            .find(|r| r.code == DiagCode::RefCycle)
1095            .expect("b <-> c carries no delay");
1096        assert!(r.at.path == "b" || r.at.path == "c", "{:?}", r.at.path);
1097        assert!(r.at.span.end > r.at.span.start);
1098    }
1099
1100    /// Reaching a node down a delayed leg first must not settle it: only the undelayed reads
1101    /// are a graph here, and `a -> c -> d -> a` is a loop in it whichever way `d` was found.
1102    #[test]
1103    fn a_delay_free_leg_is_found_even_when_a_delayed_one_reaches_it_first() {
1104        let mut files: Vec<(&str, &str)> = vec![
1105            ("a", "@b + @c\n"),
1106            ("b", "@d(t - 0.01s)\n"),
1107            ("c", "@d\n"),
1108            ("d", "@a\n"),
1109        ];
1110        let errs = load(&of(&files)).unwrap_err();
1111        assert!(
1112            errs.iter().any(|r| r.code == DiagCode::RefCycle),
1113            "a -> c -> d -> a is delay-free: {errs:?}"
1114        );
1115
1116        files.push(("c", "@d(t - 0.01s)\n"));
1117        assert!(
1118            load(&of(&files)).is_ok(),
1119            "every leg now carries a shift, so the engine sizes it"
1120        );
1121    }
1122
1123    #[test]
1124    fn deep_ref_chain_cycle_is_refused() {
1125        let files: Vec<(&str, &str)> =
1126            vec![("a", "@b\n"), ("b", "@c\n"), ("c", "@d\n"), ("d", "@a\n")];
1127        let errs = load(&of(&files)).unwrap_err();
1128        assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1129    }
1130
1131    /// A parameter list does not make a self-reference well-founded: an invocation is still a
1132    /// ref, and a ref cycle is still a cycle. `self(...)` stays the one exemption.
1133    #[test]
1134    fn a_function_invoking_itself_is_refused_but_self_inside_one_is_not() {
1135        let files: Vec<(&str, &str)> = vec![("f", "x + @f(t, x=x*0.5)\n")];
1136        let errs = load(&of(&files)).unwrap_err();
1137        assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1138
1139        let files: Vec<(&str, &str)> = vec![("a", "@b(t, x=x)\n"), ("b", "@a(t, x=x)\n")];
1140        let errs = load(&of(&files)).unwrap_err();
1141        assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1142
1143        let files: Vec<(&str, &str)> = vec![("comb", "x + g*self(t - delay)\n")];
1144        assert!(load(&of(&files)).is_ok());
1145    }
1146
1147    #[test]
1148    fn self_ref_is_not_flagged_a_cycle() {
1149        let files: Vec<(&str, &str)> = vec![
1150            ("filt", "0.3*@dry(t) + 0.7*self(t - 1sp)\n"),
1151            ("dry", "sin(2*pi*220*t)\n"),
1152        ];
1153        let g = load(&of(&files)).unwrap();
1154        assert!(g.expr("filt").is_some());
1155    }
1156
1157    #[test]
1158    fn two_graphs_over_the_same_text_hold_structurally_equal_expressions() {
1159        let mut files_a: Vec<(&str, &str)> = Vec::new();
1160        let mut files_b: Vec<(&str, &str)> = Vec::new();
1161        files_a.push(("kick", "sin(2*pi*50*t)\n"));
1162        files_b.push(("kick", "sin(2*pi*50*t)\n"));
1163        let ga = load(&of(&files_a)).unwrap();
1164        let gb = load(&of(&files_b)).unwrap();
1165
1166        assert_eq!(ga.expr("kick"), gb.expr("kick"));
1167    }
1168
1169    #[test]
1170    fn a_ref_resolves_relative_to_its_own_files_directory() {
1171        let files: Vec<(&str, &str)> = vec![
1172            ("drums/kick", "sin(2*pi*50*t)\n"),
1173            ("drums/pattern-1b", "@kick\n"),
1174        ];
1175        let g = load(&of(&files)).unwrap();
1176        assert!(g.expr("drums/pattern-1b").is_some());
1177    }
1178
1179    /// A bare path is exactly what it always was; `./` spells that out and `../` walks up.
1180    #[test]
1181    fn a_dot_segment_resolves_against_the_referencing_files_own_directory() {
1182        let at = |file: &str, path: &str| resolve_ref_path(file, path).unwrap();
1183        assert_eq!(at("lead", "kick"), "kick");
1184        assert_eq!(at("piano/bench", "partials"), "piano/partials");
1185        assert_eq!(at("piano/bench", "./partials"), "piano/partials");
1186        assert_eq!(at("piano/bench", "../variables/bpm"), "variables/bpm");
1187        assert_eq!(at("a/b/c/d", "../../x"), "a/x");
1188        assert_eq!(at("song", "./drums/kick"), "drums/kick");
1189    }
1190
1191    /// One rule wherever `..` appears, so two spellings of a node are one node.
1192    #[test]
1193    fn a_dot_segment_normalizes_in_the_middle_of_a_path_too() {
1194        let at = |file: &str, path: &str| resolve_ref_path(file, path).unwrap();
1195        assert_eq!(at("song", "a/x/../b"), at("song", "a/b"));
1196        assert_eq!(at("song", "a/./b"), "a/b");
1197        assert_eq!(at("drums/bus", "../lead/./pad/../pad"), "lead/pad");
1198    }
1199
1200    #[test]
1201    fn a_walk_above_the_composition_root_resolves_to_nothing() {
1202        assert_eq!(resolve_ref_path("piano/bench", "../../x"), None);
1203        assert_eq!(resolve_ref_path("song", "../x"), None);
1204        assert_eq!(resolve_ref_path("a/b", "../../../x"), None);
1205    }
1206
1207    #[test]
1208    fn a_ref_reaching_a_sibling_directory_resolves_and_renders_as_one_graph() {
1209        let files: Vec<(&str, &str)> = vec![
1210            ("variables/bpm", "120\n"),
1211            ("piano/partials", "sin(2*pi*440*t)\n"),
1212            ("piano/bench", "@./partials*0.5 + @../variables/bpm*0\n"),
1213            ("song", "@piano/bench\n"),
1214        ];
1215        let g = load(&of(&files)).unwrap();
1216        assert!(g.expr("piano/bench").is_some());
1217    }
1218
1219    #[test]
1220    fn a_ref_walking_above_the_root_is_a_located_refusal_naming_it() {
1221        let files: Vec<(&str, &str)> = vec![("piano/bench", "@../../secrets\n")];
1222        let errs = load(&of(&files).named("./song1")).unwrap_err();
1223        let r = errs
1224            .iter()
1225            .find(|r| r.code == DiagCode::RefAboveRoot)
1226            .expect("walking above the root must refuse");
1227        assert_eq!(r.at.path, "piano/bench");
1228        assert!(r.reason.contains("@../../secrets"), "{}", r.reason);
1229        assert!(r.reason.contains("./song1"), "{}", r.reason);
1230    }
1231}