1use 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
16pub 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 reserved: HashMap<String, Vec<&'static str>>,
26 lowered: HashMap<String, Values>,
27 skipped: Vec<Skipped>,
28 texts: HashMap<String, String>,
30 origins: HashMap<String, Origin>,
31 generations: HashMap<String, u64>,
33}
34
35#[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#[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 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 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 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 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 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 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 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 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 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 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
414pub 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
443pub 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
453pub 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
464pub 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
478pub 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
506fn recorded(mut parsed: Parsed, source: &dyn Source) -> Result<Parsed, crate::diag::Diag> {
508 let unheld = |e: crate::recording::Unheld| {
509 let code = match e.why {
510 None => DiagCode::MissingRecording,
511 Some(_) => DiagCode::Io,
512 };
513 crate::diag::Diag::new(code, ByteSpan::at(0), e.message())
514 };
515 parsed.expr = crate::recording::resolve_recordings(&parsed.expr, source).map_err(unheld)?;
516 for (_, value) in &mut parsed.defaults {
517 *value = crate::recording::resolve_recordings(value, source).map_err(unheld)?;
518 }
519 Ok(parsed)
520}
521
522fn generation(source: &dyn Source, path: &str) -> Option<u64> {
523 source
524 .generation(path)
525 .or_else(|| source.generation(&format!("{path}{EXTENSION}")))
526}
527
528struct Loading {
530 name: String,
531 nodes: BTreeMap<String, Expr>,
532 defaults: BTreeMap<String, Vec<(String, Expr)>>,
533 data: HashMap<String, Data>,
534 spans: HashMap<String, Option<FileSpan>>,
535 missing: BTreeSet<String>,
536 refusals: Vec<Refusal>,
537 skipped: Vec<Skipped>,
538 texts: HashMap<String, String>,
539 generations: HashMap<String, u64>,
540 reserved: HashMap<String, Vec<&'static str>>,
541}
542
543impl Loading {
544 fn new(name: String) -> Loading {
545 Loading {
546 name,
547 nodes: BTreeMap::new(),
548 defaults: BTreeMap::new(),
549 data: HashMap::new(),
550 spans: HashMap::new(),
551 missing: BTreeSet::new(),
552 refusals: Vec::new(),
553 skipped: Vec::new(),
554 texts: HashMap::new(),
555 generations: HashMap::new(),
556 reserved: HashMap::new(),
557 }
558 }
559
560 fn directory(&self, reason: String) -> Refusal {
561 Refusal::new(self.name.clone(), ByteSpan::at(0), DiagCode::Io, reason)
562 }
563
564 fn holds(&self, path: &str) -> bool {
567 self.nodes.contains_key(path)
568 || self.spans.contains_key(path)
569 || self.missing.contains(path)
570 }
571
572 fn pull(&mut self, source: &dyn Source, path: &str) -> bool {
575 let file = format!("{path}{EXTENSION}");
576 let read = |at: &str| {
577 source
578 .get(at)
579 .map(|text| text.map(|text| text.into_owned()))
580 };
581 let (text, score) = match (read(path), read(&file)) {
582 (Ok(Some(_)), Ok(Some(_))) => {
583 self.refusals.push(
584 Refusal::new(
585 file.as_str(),
586 ByteSpan::at(0),
587 DiagCode::BadScore,
588 format!(
589 "line 1, column 1: `@{path}` matches two files, `{path}` and \
590 `{file}`; a ref names one file, so rename or remove one of them"
591 ),
592 )
593 .placed((1, 1)),
594 );
595 self.spans.insert(path.to_string(), None);
596 return false;
597 }
598 (Ok(Some(text)), Ok(None)) => (text, false),
599 (Ok(None), Ok(Some(text))) => (text, true),
600 (Ok(None), Ok(None)) => {
601 self.missing.insert(path.to_string());
602 return false;
603 }
604 (Err(reason), _) | (_, Err(reason)) => {
605 self.refusals
606 .push(Refusal::new(path, ByteSpan::at(0), DiagCode::Io, reason));
607 self.spans.insert(path.to_string(), None);
608 return false;
609 }
610 };
611 let (_, span) = crate::filename::parse_filename(base_name(path));
612 self.spans.insert(path.to_string(), span);
613 self.texts.insert(path.to_string(), text.clone());
614 let held = if score { file.as_str() } else { path };
615 if let Some(at) = generation(source, path) {
616 self.generations.insert(path.to_string(), at);
617 }
618 let parsed = match score {
619 true => parse_score_file(path, &text),
620 false => parse_file(&text).and_then(|parsed| recorded(parsed, source)),
621 };
622 match parsed {
623 Ok(parsed) => {
624 let reads = crate::reserved::parsed_reads(&parsed, span);
625 if !reads.is_empty() {
626 self.reserved.insert(path.to_string(), reads);
627 }
628 self.nodes.insert(path.to_string(), parsed.expr);
629 if !parsed.defaults.is_empty() {
630 self.defaults.insert(path.to_string(), parsed.defaults);
631 }
632 if let Some(data) = parsed.data {
633 self.data.insert(path.to_string(), data);
634 }
635 true
636 }
637 Err(d) => {
638 let refusal = Refusal::new(held, d.span, d.code, d.message);
639 self.refusals.push(match score {
640 true => refusal.placed(crate::score::line_column(&text, d.span.start)),
641 false => refusal,
642 });
643 false
644 }
645 }
646 }
647
648 fn reads(&self, path: &str) -> Vec<String> {
649 let mut out = match self.nodes.get(path) {
650 Some(expr) => reads_of(path, expr),
651 None => Vec::new(),
652 };
653 let reserved = self.reserved.get(path).into_iter().flatten();
654 out.extend(reserved.map(|name| variable(name)));
655 out
656 }
657
658 fn on_its_line(&self, refusal: Refusal) -> Refusal {
660 let path = refusal.at.path.as_str();
661 match (self.data.get(path), self.texts.get(path)) {
662 (Some(Data::Score(_)), Some(text)) => crate::score::refused(
663 format!("{path}{EXTENSION}"),
664 text,
665 refusal.at.span.start,
666 crate::diag::DiagCode::BadScore,
667 &refusal.reason,
668 ),
669 _ => refusal,
670 }
671 }
672
673 fn finish(self, base: Option<&Graph>) -> Result<Graph, Vec<Refusal>> {
675 if !self.refusals.is_empty() {
676 return Err(self.refusals);
677 }
678 let held =
679 |path: &str| self.nodes.contains_key(path) || base.is_some_and(|b| b.defines(path));
680 let is_score = |path: &str| matches!(self.data.get(path), Some(Data::Score(_)));
681 let dangling: Vec<Refusal> = check_refs((&self.nodes, &held), &self.texts, &self.name)
682 .into_iter()
683 .filter(|r| !(is_score(&r.at.path) && r.reason.contains(&format!("`{VARIABLES}/"))))
684 .collect();
685 if !dangling.is_empty() {
686 return Err(dangling.into_iter().map(|r| self.on_its_line(r)).collect());
687 }
688 let cycles = check_cycles(&self.nodes);
689 if !cycles.is_empty() {
690 let placed = |r: Refusal| match is_score(&r.at.path) {
691 true => Refusal {
692 reason: format!(
693 "a score places itself, directly or through others: `{}` places what \
694 places it back ({})",
695 r.at.path, r.reason
696 ),
697 ..r
698 },
699 false => r,
700 };
701 return Err(cycles
702 .into_iter()
703 .map(|r| self.on_its_line(placed(r)))
704 .collect());
705 }
706 let mut defaults = self.defaults;
707 let nodes = self.nodes.into_iter().map(|(path, body)| {
708 let defaults = defaults.remove(&path).unwrap_or_default();
709 (path, Arc::new(Defined { body, defaults }))
710 });
711 Ok(Graph {
712 nodes: nodes.collect(),
713 data: self.data,
714 spans: self.spans,
715 reserved: self.reserved,
716 lowered: HashMap::new(),
717 skipped: self.skipped,
718 texts: self.texts,
719 generations: self.generations,
720 origins: HashMap::new(),
721 })
722 }
723}
724
725pub fn resolve_ref_path(referencing: &str, ref_path: &str) -> Option<String> {
729 if ref_path.starts_with('/') {
730 return None;
731 }
732 let dir = match referencing.rsplit_once('/') {
733 Some((dir, _)) => dir,
734 None => "",
735 };
736 let mut segments: Vec<&str> = dir.split('/').filter(|s| !s.is_empty()).collect();
737 for segment in ref_path.split('/') {
738 match segment {
739 "." => {}
740 ".." => {
741 segments.pop()?;
742 }
743 other => segments.push(other),
744 }
745 }
746 Some(segments.join("/"))
747}
748
749fn above_root(referencing: &str, span: ByteSpan, ref_path: &str, root: &str) -> Refusal {
750 Refusal::new(
751 referencing,
752 span,
753 DiagCode::RefAboveRoot,
754 format!(
755 "`@{ref_path}` in `{referencing}` walks above the composition root `{root}`; a \
756 ref may reach anywhere inside the root and nowhere outside it"
757 ),
758 )
759}
760
761struct RefSite {
763 path: String,
764 span: ByteSpan,
765 at_t: bool,
766}
767
768fn collect_refs(e: &Expr, out: &mut Vec<RefSite>) {
769 if let Expr::Ref {
770 path, arg, span, ..
771 } = e
772 {
773 out.push(RefSite {
774 path: path.clone(),
775 span: *span,
776 at_t: **arg == Expr::Var("t".to_string()),
777 });
778 }
779 for c in children(e, Binds::Substitute) {
780 collect_refs(c, out);
781 }
782}
783
784fn collect_named(e: &Expr, out: &mut Vec<String>) {
786 match e {
787 Expr::Call { name, .. } => out.push(name.clone()),
788 Expr::Ref { path, .. } => out.push(path.clone()),
789 _ => {}
790 }
791 for c in children(e, Binds::Substitute) {
792 collect_named(c, out);
793 }
794}
795
796type Holds<'a> = &'a dyn Fn(&str) -> bool;
797
798fn check_refs(
799 (nodes, held): (&BTreeMap<String, Expr>, Holds),
800 texts: &HashMap<String, String>,
801 root: &str,
802) -> Vec<Refusal> {
803 let mut refusals = Vec::new();
804 for (path, expr) in nodes {
805 let mut refs = Vec::new();
806 collect_refs(expr, &mut refs);
807 for site in refs {
808 let Some(target) = resolve_ref_path(path, &site.path) else {
809 refusals.push(above_root(path, site.span, &site.path, root));
810 continue;
811 };
812 if !held(&target) {
813 let hint = texts
814 .get(path)
815 .and_then(|text| digit_segment_hint(text, site.span.end))
816 .map(|seg| {
817 format!(
818 "; a path segment cannot begin with a digit, so `{seg}` was not \
819 read as part of this reference"
820 )
821 })
822 .unwrap_or_default();
823 refusals.push(Refusal::new(
824 path.clone(),
825 site.span,
826 DiagCode::DanglingRef,
827 format!(
828 "`@{}` does not resolve to any file (looked for `{target}`){hint}",
829 site.path
830 ),
831 ));
832 }
833 }
834 }
835 refusals
836}
837
838fn digit_segment_hint(text: &str, end: usize) -> Option<String> {
841 let b = text.as_bytes();
842 if b.get(end) != Some(&b'/') || !b.get(end + 1).is_some_and(u8::is_ascii_digit) {
843 return None;
844 }
845 let mut j = end + 1;
846 while j < b.len() && (b[j].is_ascii_alphanumeric() || matches!(b[j], b'_' | b'-')) {
847 j += 1;
848 }
849 Some(text[end..j].to_string())
850}
851
852#[derive(Clone, Copy, PartialEq)]
853enum Color {
854 White,
855 Gray,
856 Black,
857}
858
859fn resolved_refs<'a>(node: &str, nodes: &'a BTreeMap<String, Expr>) -> Vec<Edge<'a>> {
861 let mut raw = Vec::new();
862 if let Some(expr) = nodes.get(node) {
863 collect_refs(expr, &mut raw);
864 }
865 raw.into_iter()
866 .filter_map(|site| {
867 let target = resolve_ref_path(node, &site.path)?;
868 nodes.get_key_value(&target).map(|(k, _)| Edge {
869 target: k.as_str(),
870 span: site.span,
871 at_t: site.at_t,
872 })
873 })
874 .collect()
875}
876
877#[derive(Clone, Copy)]
878struct Edge<'a> {
879 target: &'a str,
880 span: ByteSpan,
881 at_t: bool,
882}
883
884struct Frame<'a> {
885 node: &'a str,
886 refs: Vec<Edge<'a>>,
887 idx: usize,
888}
889
890fn check_cycles(nodes: &BTreeMap<String, Expr>) -> Vec<Refusal> {
894 let mut color: HashMap<&str, Color> =
895 nodes.keys().map(|k| (k.as_str(), Color::White)).collect();
896 let mut refusals = Vec::new();
897 let undelayed = |node: &str| -> Vec<Edge<'_>> {
898 resolved_refs(node, nodes)
899 .into_iter()
900 .filter(|e| e.at_t)
901 .collect()
902 };
903
904 for start in nodes.keys().map(String::as_str) {
905 if color[start] != Color::White {
906 continue;
907 }
908 color.insert(start, Color::Gray);
909 let mut stack = vec![Frame {
910 node: start,
911 refs: undelayed(start),
912 idx: 0,
913 }];
914
915 while let Some(frame) = stack.last_mut() {
916 if frame.idx >= frame.refs.len() {
917 color.insert(frame.node, Color::Black);
918 stack.pop();
919 continue;
920 }
921 let edge = frame.refs[frame.idx];
922 let from = frame.node;
923 frame.idx += 1;
924 match color.get(edge.target) {
925 Some(Color::Gray) => refusals.push(Refusal::new(
926 from,
927 edge.span,
928 DiagCode::RefCycle,
929 format!(
930 "a delay-free ref cycle: `{from}` reads `@{}` at its own `t`, and the \
931 reads leading back to `{from}` are all at `t` too, so this value is its \
932 own argument. A loop is well-founded only when some read on it reaches \
933 strictly into the past",
934 edge.target
935 ),
936 )),
937 Some(Color::White) => {
938 color.insert(edge.target, Color::Gray);
939 stack.push(Frame {
940 node: edge.target,
941 refs: undelayed(edge.target),
942 idx: 0,
943 });
944 }
945 _ => {}
946 }
947 }
948 }
949 refusals
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 fn of(files: &[(&str, &str)]) -> crate::source::Composition {
959 files.iter().copied().collect()
960 }
961
962 struct Counting {
965 held: crate::source::Composition,
966 asked: std::cell::RefCell<Vec<String>>,
967 }
968
969 impl Source for Counting {
970 fn paths(&self) -> Result<crate::source::Listing, String> {
971 panic!("a pull load must never enumerate the source")
972 }
973 fn get(&self, path: &str) -> Result<Option<std::borrow::Cow<'_, str>>, String> {
974 self.asked.borrow_mut().push(path.to_string());
975 self.held.get(path)
976 }
977 }
978
979 #[test]
980 fn a_pull_load_never_asks_for_a_node_the_roots_do_not_reach() {
981 let source = Counting {
982 held: of(&[
983 (
984 "song",
985 "sin(2*pi*t)*0.1 + @drums/kick*0.5 + gain(@drums/kick, k=2)\n",
986 ),
987 ("drums/kick", "sin(2*pi*50*t)\n"),
988 ("gain", "x*k*sin(2*pi*t)\n"),
989 ("zombie", "@nowhere\n"),
990 ]),
991 asked: std::cell::RefCell::new(Vec::new()),
992 };
993 let g = load_reaching(&source, &["song"]).expect("the reached set is whole");
994 let mut reached: Vec<&str> = g.paths().collect();
995 reached.sort();
996 assert_eq!(reached, ["drums/kick", "gain", "song"]);
997 let asked = source.asked.borrow().clone();
998 assert!(
999 !asked.contains(&"zombie".to_string()),
1000 "an unreferenced node was read: {asked:?}"
1001 );
1002 assert!(
1003 asked.contains(&"gain".to_string()),
1004 "a bareword invocation names a file too: {asked:?}"
1005 );
1006 let mut once = asked.clone();
1007 once.sort();
1008 once.dedup();
1009 assert_eq!(
1010 once.len(),
1011 asked.len(),
1012 "`sin` is called from two root nodes and must cost one read: {asked:?}"
1013 );
1014 }
1015
1016 #[test]
1019 fn a_composition_built_node_by_node_refuses_until_the_reached_set_is_whole() {
1020 let mut c = crate::source::Composition::new();
1021 c.insert("song", "@drums/kick*0.5\n");
1022 let errs = load_reaching(&c, &["song"]).unwrap_err();
1023 assert!(errs.iter().any(|r| r.code == DiagCode::DanglingRef));
1024
1025 c.insert("drums/kick", "sin(2*pi*50*t)\n");
1026 let g = load_reaching(&c, &["song"]).expect("the last node arrived");
1027 assert!(g.expr("drums/kick").is_some());
1028 }
1029
1030 #[test]
1033 fn a_dangling_ref_truncated_by_the_digit_segment_rule_names_what_fell_off() {
1034 let g = of(&[
1035 ("tracks/melody", "@../growl/1-raw\n"),
1036 ("growl/other", "0.2\n"),
1037 ]);
1038 let errs = load(&g).unwrap_err();
1039 let err = errs
1040 .iter()
1041 .find(|r| r.code == DiagCode::DanglingRef)
1042 .expect("the truncated ref must dangle");
1043 assert!(err.reason.contains("looked for `growl`"), "{}", err.reason);
1044 assert!(
1045 err.reason.contains("digit") && err.reason.contains("/1-raw"),
1046 "must explain the truncation, not just report `growl`: {}",
1047 err.reason
1048 );
1049 }
1050
1051 #[test]
1052 fn cross_file_refs_resolve() {
1053 let files: Vec<(&str, &str)> = vec![("kick", "sin(2*pi*50*t)\n"), ("lead", "@kick*0.5\n")];
1054 let g = load(&of(&files)).unwrap();
1055 assert!(g.expr("kick").is_some());
1056 assert!(g.defines("lead"));
1057 }
1058
1059 #[test]
1060 fn same_file_cycle_is_refused() {
1061 let files: Vec<(&str, &str)> = vec![("a", "@a + 1\n")];
1062 let errs = load(&of(&files)).unwrap_err();
1063 assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1064 }
1065
1066 #[test]
1067 fn a_node_merely_referencing_a_cycle_member_is_not_itself_flagged() {
1068 let files: Vec<(&str, &str)> = vec![("a", "@b\n"), ("b", "@a\n"), ("c", "@b\n")];
1069 let errs = load(&of(&files)).unwrap_err();
1070 assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1071 assert!(
1072 !errs.iter().any(|r| r.at.path == "c"),
1073 "c only reaches into the cycle, it does not loop back to itself: {errs:?}"
1074 );
1075 }
1076
1077 #[test]
1080 fn a_mutual_cycle_with_a_shifted_read_on_it_parses() {
1081 let files: Vec<(&str, &str)> =
1082 vec![("a", "@b(t - 0.01s)*0.5\n"), ("b", "@a*0.5 + sin(t)\n")];
1083 let g = load(&of(&files)).unwrap();
1084 assert!(g.expr("a").is_some() && g.expr("b").is_some());
1085
1086 let files: Vec<(&str, &str)> = vec![("ring", "@ring(t - 0.002s)*0.9 + sin(t)\n")];
1087 assert!(load(&of(&files)).is_ok());
1088 }
1089
1090 #[test]
1092 fn a_cycle_read_only_at_bare_t_is_refused_where_the_read_is_written() {
1093 let files: Vec<(&str, &str)> =
1094 vec![("a", "@b(t - 0.01s)\n"), ("b", "@c\n"), ("c", "@b + 1\n")];
1095 let errs = load(&of(&files)).unwrap_err();
1096 let r = errs
1097 .iter()
1098 .find(|r| r.code == DiagCode::RefCycle)
1099 .expect("b <-> c carries no delay");
1100 assert!(r.at.path == "b" || r.at.path == "c", "{:?}", r.at.path);
1101 assert!(r.at.span.end > r.at.span.start);
1102 }
1103
1104 #[test]
1107 fn a_delay_free_leg_is_found_even_when_a_delayed_one_reaches_it_first() {
1108 let mut files: Vec<(&str, &str)> = vec![
1109 ("a", "@b + @c\n"),
1110 ("b", "@d(t - 0.01s)\n"),
1111 ("c", "@d\n"),
1112 ("d", "@a\n"),
1113 ];
1114 let errs = load(&of(&files)).unwrap_err();
1115 assert!(
1116 errs.iter().any(|r| r.code == DiagCode::RefCycle),
1117 "a -> c -> d -> a is delay-free: {errs:?}"
1118 );
1119
1120 files.push(("c", "@d(t - 0.01s)\n"));
1121 assert!(
1122 load(&of(&files)).is_ok(),
1123 "every leg now carries a shift, so the engine sizes it"
1124 );
1125 }
1126
1127 #[test]
1128 fn deep_ref_chain_cycle_is_refused() {
1129 let files: Vec<(&str, &str)> =
1130 vec![("a", "@b\n"), ("b", "@c\n"), ("c", "@d\n"), ("d", "@a\n")];
1131 let errs = load(&of(&files)).unwrap_err();
1132 assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1133 }
1134
1135 #[test]
1138 fn a_function_invoking_itself_is_refused_but_self_inside_one_is_not() {
1139 let files: Vec<(&str, &str)> = vec![("f", "x + @f(t, x=x*0.5)\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![("a", "@b(t, x=x)\n"), ("b", "@a(t, x=x)\n")];
1144 let errs = load(&of(&files)).unwrap_err();
1145 assert!(errs.iter().any(|r| r.code == DiagCode::RefCycle));
1146
1147 let files: Vec<(&str, &str)> = vec![("comb", "x + g*self(t - delay)\n")];
1148 assert!(load(&of(&files)).is_ok());
1149 }
1150
1151 #[test]
1152 fn self_ref_is_not_flagged_a_cycle() {
1153 let files: Vec<(&str, &str)> = vec![
1154 ("filt", "0.3*@dry(t) + 0.7*self(t - 1sp)\n"),
1155 ("dry", "sin(2*pi*220*t)\n"),
1156 ];
1157 let g = load(&of(&files)).unwrap();
1158 assert!(g.expr("filt").is_some());
1159 }
1160
1161 #[test]
1162 fn two_graphs_over_the_same_text_hold_structurally_equal_expressions() {
1163 let mut files_a: Vec<(&str, &str)> = Vec::new();
1164 let mut files_b: Vec<(&str, &str)> = Vec::new();
1165 files_a.push(("kick", "sin(2*pi*50*t)\n"));
1166 files_b.push(("kick", "sin(2*pi*50*t)\n"));
1167 let ga = load(&of(&files_a)).unwrap();
1168 let gb = load(&of(&files_b)).unwrap();
1169
1170 assert_eq!(ga.expr("kick"), gb.expr("kick"));
1171 }
1172
1173 #[test]
1174 fn a_ref_resolves_relative_to_its_own_files_directory() {
1175 let files: Vec<(&str, &str)> = vec![
1176 ("drums/kick", "sin(2*pi*50*t)\n"),
1177 ("drums/pattern-1b", "@kick\n"),
1178 ];
1179 let g = load(&of(&files)).unwrap();
1180 assert!(g.expr("drums/pattern-1b").is_some());
1181 }
1182
1183 #[test]
1185 fn a_dot_segment_resolves_against_the_referencing_files_own_directory() {
1186 let at = |file: &str, path: &str| resolve_ref_path(file, path).unwrap();
1187 assert_eq!(at("lead", "kick"), "kick");
1188 assert_eq!(at("piano/bench", "partials"), "piano/partials");
1189 assert_eq!(at("piano/bench", "./partials"), "piano/partials");
1190 assert_eq!(at("piano/bench", "../variables/bpm"), "variables/bpm");
1191 assert_eq!(at("a/b/c/d", "../../x"), "a/x");
1192 assert_eq!(at("song", "./drums/kick"), "drums/kick");
1193 }
1194
1195 #[test]
1197 fn a_dot_segment_normalizes_in_the_middle_of_a_path_too() {
1198 let at = |file: &str, path: &str| resolve_ref_path(file, path).unwrap();
1199 assert_eq!(at("song", "a/x/../b"), at("song", "a/b"));
1200 assert_eq!(at("song", "a/./b"), "a/b");
1201 assert_eq!(at("drums/bus", "../lead/./pad/../pad"), "lead/pad");
1202 }
1203
1204 #[test]
1205 fn a_walk_above_the_composition_root_resolves_to_nothing() {
1206 assert_eq!(resolve_ref_path("piano/bench", "../../x"), None);
1207 assert_eq!(resolve_ref_path("song", "../x"), None);
1208 assert_eq!(resolve_ref_path("a/b", "../../../x"), None);
1209 }
1210
1211 #[test]
1212 fn a_ref_reaching_a_sibling_directory_resolves_and_renders_as_one_graph() {
1213 let files: Vec<(&str, &str)> = vec![
1214 ("variables/bpm", "120\n"),
1215 ("piano/partials", "sin(2*pi*440*t)\n"),
1216 ("piano/bench", "@./partials*0.5 + @../variables/bpm*0\n"),
1217 ("song", "@piano/bench\n"),
1218 ];
1219 let g = load(&of(&files)).unwrap();
1220 assert!(g.expr("piano/bench").is_some());
1221 }
1222
1223 #[test]
1224 fn a_ref_walking_above_the_root_is_a_located_refusal_naming_it() {
1225 let files: Vec<(&str, &str)> = vec![("piano/bench", "@../../secrets\n")];
1226 let errs = load(&of(&files).named("./song1")).unwrap_err();
1227 let r = errs
1228 .iter()
1229 .find(|r| r.code == DiagCode::RefAboveRoot)
1230 .expect("walking above the root must refuse");
1231 assert_eq!(r.at.path, "piano/bench");
1232 assert!(r.reason.contains("@../../secrets"), "{}", r.reason);
1233 assert!(r.reason.contains("./song1"), "{}", r.reason);
1234 }
1235}