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 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
524struct 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 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 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 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 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
721pub 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
757struct 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
780fn 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
834fn 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
855fn 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
886fn 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 fn of(files: &[(&str, &str)]) -> crate::source::Composition {
955 files.iter().copied().collect()
956 }
957
958 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}