Skip to main content

sva_ast/song/
mod.rs

1// Concern: derives a song from its root: placements on the tempo curve, lanes, the equations it lowers to | Non-concern: an instance's span, which the caller asks the engine | IO: (&Graph, root) -> Song
2
3mod lanes;
4mod lower;
5mod place;
6mod reach;
7mod tempo;
8
9use std::collections::BTreeSet;
10use std::rc::Rc;
11
12pub use lanes::{Held, Hold, Home, Lane, LaneId, Swept};
13pub use place::parameters;
14pub use place::{Chain, Instance, PlacedMark, Pos, Reached};
15pub use tempo::Curve;
16
17use crate::diag::{ByteSpan, DiagCode};
18use crate::expr::Expr;
19use crate::graph::{Defined, Graph, VARIABLES, variable_text};
20use crate::refusal::Refusal;
21use crate::scale::Scale;
22use crate::score::{EXTENSION, Item, Ratio, Shape, located};
23
24use lanes::Sweeping;
25use place::{Container, Offset, Placing, at_line, shifted};
26use reach::Reach;
27use tempo::TempoMove;
28
29/// One instance's span from its onset: the instants of its first and last samples, `None` an
30/// end the engine leaves open.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Span {
33    pub start: Option<f64>,
34    pub end: Option<f64>,
35}
36
37/// A warning a song's files earn: it prints, and the render goes on.
38#[derive(Clone, Debug, PartialEq)]
39pub struct Warning {
40    pub code: &'static str,
41    pub file: String,
42    pub span: ByteSpan,
43    pub start: (usize, usize),
44    pub message: String,
45}
46
47pub struct Song {
48    root: String,
49    curve: Curve,
50    tree: Rc<Container>,
51    instances: Vec<Instance>,
52    lanes: Vec<Lane>,
53    marks: Vec<PlacedMark>,
54    instruments: Vec<(String, Defined)>,
55    asked: Vec<(usize, Expr)>,
56    warnings: Vec<Warning>,
57    key: Option<f64>,
58    scale: Option<Scale>,
59    passed: Vec<(usize, LaneId)>,
60    containers: BTreeSet<String>,
61}
62
63/// What a song lowers to: each node's new body, the root's included.
64pub struct Lowered {
65    pub nodes: Vec<(String, Defined)>,
66}
67
68fn refused(refusals: Vec<Refusal>) -> Result<(), Vec<Refusal>> {
69    match refusals.is_empty() {
70        true => Ok(()),
71        false => Err(refusals),
72    }
73}
74
75impl Graph {
76    /// Steps 3 to 7 short of the spans: every line `root` reaches placed on the tempo curve, every
77    /// lane swept, and each node no score sits under lowered with the lanes reaching it.
78    pub fn song(&self, root: &str) -> Result<Song, Vec<Refusal>> {
79        let values = self.values(None)?;
80        let (bpm, beats) = match &values.tempo {
81            Some(tempo) => (tempo.bpm, tempo.beats()),
82            None => (60.0, 1.0),
83        };
84        let steady = Curve::new(bpm, beats, &[]);
85        let tempo = {
86            let mut first = Placing::new(self, &steady);
87            first.song(root);
88            refused(std::mem::take(&mut first.refusals))?;
89            let reached: Vec<Reached> = first
90                .moves
91                .into_iter()
92                .filter(|m| m.lane.is_variable("bpm"))
93                .collect();
94            let mut refusals = Vec::new();
95            for m in reached
96                .iter()
97                .filter(|m| matches!(m.start, Pos::Seconds(_)))
98            {
99                refusals.push(self.through_seconds(m, &steady, "a `bpm` move"));
100            }
101            refused(refusals)?;
102            let sweeping = Sweeping {
103                graph: self,
104                curve: &steady,
105                per_bar: values.tempo.as_ref().map(|t| t.seconds_per_bar()),
106                degree: &|_, _| Err("a `bpm` move reads no degree".to_string()),
107            };
108            let lanes = sweeping.sweep(reached)?;
109            lanes
110                .first()
111                .map(|lane| {
112                    lane.moves
113                        .iter()
114                        .map(|m| {
115                            let Pos::Bars(start) = m.reached.start else {
116                                unreachable!("refused above")
117                            };
118                            let len = match m.reached.end {
119                                Some(Pos::Bars(end)) => end.minus(start).expect("a bar span"),
120                                _ => Ratio::ZERO,
121                            };
122                            TempoMove {
123                                start,
124                                len,
125                                shape: m.shape.unwrap_or(Shape::Lin),
126                                from: m.from.as_ref().and_then(Held::number).unwrap_or(bpm),
127                                to: m.to.number().expect("bpm is a number"),
128                            }
129                        })
130                        .collect::<Vec<_>>()
131                })
132                .unwrap_or_default()
133        };
134        let curve = Curve::new(bpm, beats, &tempo);
135        let mut placing = Placing::new(self, &curve);
136        let tree = placing.song(root);
137        let mut refusals = std::mem::take(&mut placing.refusals);
138        if curve.moves() {
139            for m in placing
140                .moves
141                .iter()
142                .filter(|m| matches!(m.start, Pos::Seconds(_)))
143            {
144                refusals.push(self.through_seconds(m, &curve, "a move"));
145            }
146            for (score, line, item, lost) in &placing.bars_lost {
147                let at = self.score(score).expect("a score").lines[*line].items[*item].at();
148                refusals.push(at_line(
149                    self,
150                    score,
151                    at.at,
152                    DiagCode::BadLane,
153                    &format!(
154                        "this line counts in bars, and the tempo moves, so a bar is a place on \
155                         the tempo curve; `{}` reads what places it at a time that is no \
156                         constant shift of `t`, so it has no song time: place it at `t - d`",
157                        lost.path
158                    ),
159                ));
160            }
161        }
162        refused(refusals)?;
163        let key = self.key_hz();
164        let scale = values.scale.clone();
165        let degree = |at: f64, n: i64| -> Result<f64, String> {
166            let _ = at;
167            let ratio = scale.as_ref().and_then(|s| s.ratio(n)).ok_or_else(|| {
168                "`deg` counts in `variables/scale`, and there is none".to_string()
169            })?;
170            key.map(|k| k * ratio)
171                .ok_or_else(|| "`deg` counts from `variables/key`, a constant note".to_string())
172        };
173        let sweeping = Sweeping {
174            graph: self,
175            curve: &curve,
176            per_bar: values.tempo.as_ref().map(|t| t.seconds_per_bar()),
177            degree: &degree,
178        };
179        let placed_moves = std::mem::take(&mut placing.moves);
180        let lanes = sweeping.sweep(placed_moves)?;
181        let marks = std::mem::take(&mut placing.marks);
182        let containers = std::mem::take(&mut placing.placed);
183        let instances_lost = std::mem::take(&mut placing.instances_lost);
184        let mut instances = std::mem::take(&mut placing.instances);
185        let moved: BTreeSet<LaneId> = lanes.iter().map(|l| l.id.clone()).collect();
186        let mut reach = Reach::new(self, &moved, &values);
187        for instance in &mut instances {
188            let bound: BTreeSet<&str> = instance.bound.iter().map(String::as_str).collect();
189            let mut held = reach.sound(&instance.node, &bound);
190            let read = instance.node.strip_prefix(&format!("{VARIABLES}/"));
191            if !read.is_some_and(|name| moved.contains(&LaneId::Variable(name.to_string()))) {
192                held.extend(reach.of(&instance.node, &bound));
193            }
194            held.retain(|l| !l.is_variable("scale"));
195            held.sort();
196            held.dedup();
197            instance.lanes = held;
198        }
199        let mut lanes = lanes;
200        if let Some(scale) = lanes.iter().find(|l| l.id.is_variable("scale")) {
201            let read: BTreeSet<i64> = instances
202                .iter()
203                .flat_map(|i| &i.lanes)
204                .filter_map(|l| match l {
205                    LaneId::Degree(n) => Some(*n),
206                    _ => None,
207                })
208                .collect();
209            let derived: Vec<Lane> = read
210                .into_iter()
211                .filter_map(|n| lanes::degree_lane(scale, n))
212                .collect();
213            lanes.extend(derived);
214        }
215        let mut song = Song {
216            root: root.to_string(),
217            curve,
218            tree,
219            instances,
220            lanes,
221            marks,
222            instruments: Vec::new(),
223            asked: Vec::new(),
224            warnings: Vec::new(),
225            key,
226            scale: values.scale.clone(),
227            passed: Vec::new(),
228            containers,
229        };
230        let mut refusals = song.unbound(self);
231        for (k, lost) in instances_lost {
232            let instance = &song.instances[k];
233            if instance.lanes.is_empty() {
234                continue;
235            }
236            let lanes: Vec<String> = instance.lanes.iter().map(LaneId::name).collect();
237            let (file, span) = lost.unwrap_or((song.root.clone(), ByteSpan::at(0)));
238            refusals.push(Refusal::new(
239                file.clone(),
240                span,
241                DiagCode::BadLane,
242                format!(
243                    "`@{}` has no song time: `{file}` reads it at a time that is no constant \
244                     shift of `t`, and the lanes `{}` move it; a lane reaches an instance at its \
245                     song position, so read it at `t - d`",
246                    instance.node,
247                    lanes.join("`, `")
248                ),
249            ));
250        }
251        refused(refusals)?;
252        let mut lowered = BTreeSet::new();
253        for instance in &song.instances {
254            let mut open: Vec<String> = vec![instance.node.clone()];
255            while let Some(node) = open.pop() {
256                if reach.of(&node, &BTreeSet::new()).is_empty() || !lowered.insert(node.clone()) {
257                    continue;
258                }
259                let mut sites = Vec::new();
260                if let Some(defined) = self.source(&node) {
261                    for e in std::iter::once(&defined.body)
262                        .chain(defined.defaults.iter().map(|(_, v)| v))
263                    {
264                        place::sites(self, &node, e, &mut sites);
265                    }
266                }
267                open.extend(
268                    sites
269                        .iter()
270                        .filter_map(|e| reach::invoked(self, &node, e).map(|(n, ..)| n)),
271                );
272            }
273        }
274        let mut refusals = Vec::new();
275        for node in lowered {
276            match reach.lowered(&node) {
277                Ok(defined) => song.instruments.push((node, defined)),
278                Err(refusal) => refusals.push(refusal),
279            }
280        }
281        refused(refusals)?;
282        song.asked = song.asked_spans(self)?;
283        song.warnings = song.warned(self);
284        Ok(song)
285    }
286
287    fn through_seconds(&self, m: &Reached, curve: &Curve, what: &str) -> Refusal {
288        let at = match &self.score(&m.score).expect("a score").lines[m.line].items[m.item] {
289            Item::Move(mv) => mv.at.at,
290            _ => 0,
291        };
292        at_line(
293            self,
294            &m.score,
295            at,
296            DiagCode::BadLane,
297            &format!(
298                "{what} on `{}` is reached through seconds ({}), and while the tempo moves a \
299                 move sits on a bar, so a lane compares exact positions; place it from a file \
300                 counting in bars",
301                m.lane.name(),
302                m.chain.text(curve)
303            ),
304        )
305    }
306
307    /// The tonic `variables/key` holds, in hertz, where it is a constant.
308    fn key_hz(&self) -> Option<f64> {
309        let text = variable_text(self, "key")?;
310        let reading = crate::score::Reading::<f64> {
311            field: crate::score::Field::Value,
312            mark: &|_| None,
313            degree: &|_| Err(String::new()),
314            own: None,
315        };
316        crate::score::evaluate(text.trim(), &reading)
317            .ok()
318            .map(|v| v.amount)
319    }
320}
321
322impl Song {
323    pub fn curve(&self) -> &Curve {
324        &self.curve
325    }
326
327    pub fn lanes(&self) -> &[Lane] {
328        &self.lanes
329    }
330
331    pub fn marks(&self) -> &[PlacedMark] {
332        &self.marks
333    }
334
335    pub fn instances(&self) -> &[Instance] {
336        &self.instances
337    }
338
339    pub fn warnings(&self) -> &[Warning] {
340        &self.warnings
341    }
342
343    /// Each node no score sits under, lowered with the lanes reaching it as parameters.
344    pub fn instruments(&self) -> &[(String, Defined)] {
345        &self.instruments
346    }
347
348    /// Each instance whose span decides a fold, as the node a probe at the root reads:
349    /// `@node(t, ...)` with every lane at its onset value.
350    pub fn asked(&self) -> Vec<Expr> {
351        self.asked.iter().map(|(_, e)| e.clone()).collect()
352    }
353
354    pub fn lane(&self, id: &LaneId) -> Option<&Lane> {
355        self.lanes.iter().find(|l| &l.id == id)
356    }
357
358    /// The moved lanes of `node`'s own parameters: a container equation's are passed on as
359    /// moving values, never folded, since no one span bounds what it places.
360    pub(crate) fn own_lanes(&self, graph: &Graph, node: &str) -> Vec<LaneId> {
361        parameters(graph, node)
362            .into_iter()
363            .map(|p| LaneId::Sound(node.to_string(), p))
364            .filter(|l| self.lane(l).is_some())
365            .collect()
366    }
367
368    /// Where instance `i`'s line's `len` ends in song time; `None` where it writes none.
369    pub fn ends(&self, graph: &Graph, i: &Instance) -> Option<Pos> {
370        let (score, li, ii) = i.line.as_ref()?;
371        let file = graph.score(score)?;
372        let Item::Event(event) = &file.lines[*li].items[*ii] else {
373            return None;
374        };
375        let len = event.len?.amount;
376        let by = match file.unit {
377            crate::filename::SpanUnit::Bars => Offset::Bars(len),
378            crate::filename::SpanUnit::Seconds => Offset::Seconds(len.to_f64()),
379        };
380        shifted(i.at, by, &self.curve)
381    }
382
383    /// The key at second `at`, in hertz.
384    pub fn key_at(&self, at: f64) -> Option<f64> {
385        match self.lane(&LaneId::Variable("key".to_string())) {
386            Some(lane) => lane.at(at).and_then(|h| h.number()),
387            None => self.key,
388        }
389    }
390
391    pub fn scale_at(&self, at: f64) -> Option<Scale> {
392        match self.lane(&LaneId::Variable("scale".to_string())) {
393            Some(lane) => match lane.at(at) {
394                Some(Held::Scale(scale)) => Some(scale),
395                _ => None,
396            },
397            None => self.scale.clone(),
398        }
399    }
400
401    /// Every parameter a score line's instance reads that nothing binds: not the line, not a
402    /// lane set by its onset, not a default.
403    fn unbound(&self, graph: &Graph) -> Vec<Refusal> {
404        let mut out = Vec::new();
405        let mut seen = BTreeSet::new();
406        for instance in &self.instances {
407            let Some((score, li, ii)) = &instance.line else {
408                continue;
409            };
410            let Item::Event(event) = &graph.score(score).expect("a score").lines[*li].items[*ii]
411            else {
412                continue;
413            };
414            let params = parameters(graph, &instance.node);
415            let read: Vec<&str> = params.iter().map(String::as_str).collect();
416            for bound in &instance.bound {
417                if read.contains(&bound.as_str()) {
418                    continue;
419                }
420                let (key, at) = match bound.as_str() {
421                    "f0" if event.deg.is_some() => ("deg", event.deg.expect("a deg").1),
422                    "len" => ("len", event.len.expect("a len").at),
423                    name => (
424                        name,
425                        event
426                            .binds
427                            .iter()
428                            .find(|b| b.name == name)
429                            .map_or(event.node.at, |b| b.key),
430                    ),
431                };
432                if !seen.insert((score.clone(), *li, key.to_string())) {
433                    continue;
434                }
435                let set = match key == bound {
436                    true => format!("`{key}`"),
437                    false => format!("`{key}`, which sets `{bound}`,"),
438                };
439                let chord = match key {
440                    "chord" => "; a chord is a `deg` list, one event per voice",
441                    _ => "",
442                };
443                out.push(at_line(
444                    graph,
445                    score,
446                    at,
447                    DiagCode::BadScore,
448                    &format!(
449                        "{set} is no parameter `@{}` reads; it reads `{}`{chord}",
450                        event.node.name,
451                        read.join("`, `")
452                    ),
453                ));
454            }
455            let defaults: Vec<&str> = graph
456                .source(&instance.node)
457                .map(|d| d.defaults.iter().map(|(k, _)| k.as_str()).collect())
458                .unwrap_or_default();
459            let at = instance.at.map(|p| p.seconds(&self.curve));
460            for p in &params {
461                if defaults.contains(&p.as_str()) || instance.bound.contains(p) {
462                    continue;
463                }
464                let lane = LaneId::Sound(instance.node.clone(), p.clone());
465                let set = self
466                    .lane(&lane)
467                    .is_some_and(|l| at.is_some_and(|at| l.at(at).is_some()));
468                if set || !seen.insert((score.clone(), *li, format!("unbound {p}"))) {
469                    continue;
470                }
471                out.push(at_line(
472                    graph,
473                    score,
474                    event.node.at,
475                    DiagCode::BadScore,
476                    &format!(
477                        "`@{}` reads `{p}`, which nothing binds here: bind it on this line, set \
478                         the lane `{}` at or before this line, or give it a default, `{p} = ...`",
479                        event.node.name,
480                        lane.name()
481                    ),
482                ));
483            }
484        }
485        out
486    }
487
488    /// The probe expressions for every instance a lane may move under.
489    fn asked_spans(&self, graph: &Graph) -> Result<Vec<(usize, Expr)>, Vec<Refusal>> {
490        let mut out = Vec::new();
491        for (k, instance) in self.instances.iter().enumerate() {
492            let Some(at) = instance.at else { continue };
493            let at = at.seconds(&self.curve);
494            let moving = instance
495                .lanes
496                .iter()
497                .filter_map(|l| self.lane(l))
498                .any(|l| l.changes_after(at));
499            if moving {
500                out.push((k, self.probe(graph, k, at)?));
501            }
502        }
503        Ok(out)
504    }
505
506    fn warned(&self, graph: &Graph) -> Vec<Warning> {
507        let mut out = Vec::new();
508        let curve = &self.curve;
509        for instance in &self.instances {
510            let (Some((score, li, ii)), Some(at)) = (&instance.line, instance.at) else {
511                continue;
512            };
513            let Item::Event(event) = &graph.score(score).expect("a score").lines[*li].items[*ii]
514            else {
515                continue;
516            };
517            let start = at.seconds(curve);
518            let end = start + self.len_seconds(graph, score, at, event.len.map(|l| l.amount));
519            for bound in &instance.bound {
520                let lane = LaneId::Sound(instance.node.clone(), bound.clone());
521                let Some(l) = self.lane(&lane) else { continue };
522                let ramp = l.moves.iter().find(|m| {
523                    m.end > m.start && m.end > start && (m.start < end || m.start <= start)
524                });
525                if let Some(ramp) = ramp {
526                    let key_at = event
527                        .binds
528                        .iter()
529                        .find(|b| &b.name == bound)
530                        .map_or(event.node.at, |b| b.key);
531                    out.push(warn(
532                        graph,
533                        score,
534                        key_at,
535                        "line-overrides-lane",
536                        &format!(
537                            "this line binds `{bound}`, and the lane `{}` ramps during its event \
538                             ({}); the line wins over the lane",
539                            lane.name(),
540                            self.line_of(graph, &ramp.reached)
541                        ),
542                    ));
543                }
544            }
545        }
546        for lane in &self.lanes {
547            let reached = lane.id.is_variable("bpm")
548                || self
549                    .instances
550                    .iter()
551                    .any(|i| i.lanes.contains(&lane.id) || i.node == lane.id.name())
552                || matches!(&lane.id, LaneId::Sound(node, _) if self.containers.contains(node))
553                || self.degrees_read(graph, &lane.id);
554            if !reached {
555                let first = &lane.moves[0].reached;
556                let at = match &graph.score(&first.score).expect("a score").lines[first.line].items
557                    [first.item]
558                {
559                    Item::Move(m) => m.lane.at,
560                    _ => 0,
561                };
562                out.push(warn(
563                    graph,
564                    &first.score,
565                    at,
566                    "lane-reaches-nothing",
567                    &format!(
568                        "the lane `{}` has moves, and no instance in the song reads it: its \
569                         node is never placed, or every invoker passes the parameter",
570                        lane.id.name()
571                    ),
572                ));
573            }
574        }
575        out
576    }
577
578    /// Whether a degree reads `key` or `scale` here: a score line's, or an equation's own.
579    fn degrees_read(&self, graph: &Graph, id: &LaneId) -> bool {
580        let own = |i: &Instance| i.lanes.iter().any(|l| matches!(l, LaneId::Degree(_)));
581        if id.is_variable("scale") && self.instances.iter().any(own) {
582            return true;
583        }
584        (id.is_variable("key") || id.is_variable("scale"))
585            && self.instances.iter().any(|i| {
586                i.line.as_ref().is_some_and(|(score, li, ii)| {
587                    match &graph.score(score).expect("a score").lines[*li].items[*ii] {
588                        Item::Event(e) => {
589                            e.deg.is_some()
590                                || e.binds
591                                    .iter()
592                                    .any(|b| crate::render_expr(&b.value).contains("deg("))
593                        }
594                        _ => false,
595                    }
596                })
597            })
598    }
599
600    fn line_of(&self, graph: &Graph, r: &Reached) -> String {
601        let number = graph.score(&r.score).expect("a score").lines[r.line].number;
602        format!("{}{EXTENSION}:{number}", r.score)
603    }
604
605    /// The seconds a line's `len` lasts from `at`, on the curve where it counts in bars.
606    fn len_seconds(&self, graph: &Graph, score: &str, at: Pos, len: Option<Ratio>) -> f64 {
607        let Some(len) = len else { return 0.0 };
608        match graph.score(score).expect("a score").unit {
609            crate::filename::SpanUnit::Seconds => len.to_f64(),
610            crate::filename::SpanUnit::Bars => match at {
611                Pos::Bars(b) => self.curve.duration(b, len),
612                Pos::Seconds(s) => self
613                    .curve
614                    .after(s, len)
615                    .unwrap_or(len.to_f64() * self.curve.seconds_per_bar()),
616            },
617        }
618    }
619
620    /// Steps 7 and 8: each instance's lanes folded over its span or passed on, and every
621    /// container lowered per placement; what is passed is kept for a refusal to name.
622    pub fn lowered(&mut self, graph: &Graph, spans: &[Span]) -> Result<Lowered, Vec<Refusal>> {
623        let (lowered, passed) = lower::Lowering::new(graph, self, spans).lowered()?;
624        self.passed = passed;
625        Ok(lowered)
626    }
627
628    /// Each instance and lane a lowering passed on as a moving value.
629    pub fn passed(&self) -> &[(usize, LaneId)] {
630        &self.passed
631    }
632
633    /// Where the first move of `lane` under instance `k` is written, its `to`: the file, the
634    /// byte and the file's text.
635    pub fn moved_under<'g>(
636        &self,
637        graph: &'g Graph,
638        k: usize,
639        lane: &LaneId,
640    ) -> Option<(String, usize, &'g str)> {
641        let at = self.instances[k].at?.seconds(&self.curve);
642        let l = self.lane(lane)?;
643        let m = l
644            .moves
645            .iter()
646            .find(|m| m.end > at || m.start >= at)
647            .or(l.moves.last())?;
648        let r = &m.reached;
649        let Item::Move(mv) = &graph.score(&r.score)?.lines[r.line].items[r.item] else {
650            return None;
651        };
652        Some((
653            format!("{}{EXTENSION}", r.score),
654            mv.to.at,
655            graph.text(&r.score)?,
656        ))
657    }
658}
659
660fn warn(graph: &Graph, score: &str, at: usize, code: &'static str, message: &str) -> Warning {
661    let text = graph.text(score).unwrap_or("");
662    let diag = located(text, at, message);
663    Warning {
664        code,
665        file: format!("{score}{EXTENSION}"),
666        span: diag.span,
667        start: crate::score::line_column(text, at),
668        message: diag.message,
669    }
670}
671
672impl Graph {
673    /// Each node's body a song lowered, kept beside its source.
674    pub fn apply(&mut self, lowered: Lowered) {
675        for (path, defined) in lowered.nodes {
676            self.derive(&path, defined);
677        }
678    }
679}