Skip to main content

sva_ast/score/
mod.rs

1// Concern: a score file's lines as its own file reads them: kinds, items, times from its zero | Non-concern: where a song places it, lanes and lowering (song/) | IO: (&str) -> Score or a located Diag
2
3mod ratio;
4mod read;
5mod value;
6
7pub use ratio::Ratio;
8pub(crate) use read::is_old_score;
9pub use read::parse_score;
10pub use value::{Amount, Field, Kind, MOVING, Reading, Refused, Value, evaluate};
11
12use crate::diag::{ByteSpan, Diag, DiagCode};
13use crate::expr::{Address, BinOp, DEGREE, Expr, Literal, children};
14use crate::filename::SpanUnit;
15use crate::graph::VARIABLES;
16
17pub const EXTENSION: &str = ".jsonl";
18
19#[derive(Clone, Debug, PartialEq)]
20pub struct Score {
21    pub annotation: Option<String>,
22    pub unit: SpanUnit,
23    pub lines: Vec<Line>,
24}
25
26impl Score {
27    pub fn items(&self) -> impl Iterator<Item = (&Line, &Item)> {
28        self.lines
29            .iter()
30            .flat_map(|line| line.items.iter().map(move |item| (line, item)))
31    }
32
33    /// Every file a line names: what loading a score follows, as any node's body. An event
34    /// reads its node at `t`; a move names its node and plays it at no time of its own, so a
35    /// song moving what places it is no cycle. A score sounds only as a song lowers it.
36    pub(crate) fn reads(&self, path: &str) -> Expr {
37        let root = "../".repeat(path.matches('/').count());
38        let mut named: Vec<(String, usize, bool)> = Vec::new();
39        for (_, item) in self.items() {
40            match item {
41                Item::Event(e) => named.push((e.node.name.clone(), e.node.at, true)),
42                Item::Move(m) => match &m.node {
43                    Some(node) => named.push((node.name.clone(), node.at, false)),
44                    None => {
45                        let variable = format!("{root}{VARIABLES}/{}", m.lane.name);
46                        named.push((variable, m.lane.at, false));
47                        if let Some(scale) = m.to.text.strip_prefix('@') {
48                            named.push((scale.to_string(), m.to.inner, false));
49                        }
50                    }
51                },
52                Item::Mark(_) | Item::Comment(_) => {}
53            }
54        }
55        let mut seen = std::collections::BTreeSet::new();
56        named.retain(|(name, _, played)| seen.insert((name.clone(), *played)));
57        let read = |(path, at, played): (String, usize, bool)| Expr::Ref {
58            path,
59            arg: Box::new(match played {
60                true => Expr::Var("t".to_string()),
61                false => Expr::Lit(Literal::Num(0.0)),
62            }),
63            binds: Vec::new(),
64            address: Address::Time,
65            span: ByteSpan::new(at, at),
66        };
67        named
68            .into_iter()
69            .map(read)
70            .reduce(|a, b| Expr::Bin(BinOp::Add, Box::new(a), Box::new(b)))
71            .unwrap_or(Expr::Lit(Literal::Num(0.0)))
72    }
73
74    pub(crate) fn reads_degrees(&self) -> bool {
75        fn calls(e: &Expr) -> bool {
76            matches!(e, Expr::Call { name, .. } if name == DEGREE)
77                || children(e, crate::expr::Binds::Substitute)
78                    .into_iter()
79                    .any(calls)
80        }
81        self.items().any(|(_, item)| match item {
82            Item::Event(e) => e.deg.is_some() || e.binds.iter().any(|b| calls(&b.value)),
83            Item::Move(m) => m.to.text.contains("deg("),
84            _ => false,
85        })
86    }
87}
88
89#[derive(Clone, Debug, PartialEq)]
90pub struct Line {
91    pub number: usize,
92    pub span: ByteSpan,
93    pub items: Vec<Item>,
94}
95
96#[derive(Clone, Debug, PartialEq)]
97pub enum Item {
98    Event(Event),
99    Mark(Mark),
100    Comment(Time),
101    Move(Move),
102}
103
104impl Item {
105    pub fn at(&self) -> Time {
106        match self {
107            Item::Event(e) => e.at,
108            Item::Mark(m) => m.at,
109            Item::Comment(at) => *at,
110            Item::Move(m) => m.at,
111        }
112    }
113
114    pub fn kind(&self) -> &'static str {
115        match self {
116            Item::Event(_) => "event",
117            Item::Mark(_) => "mark",
118            Item::Comment(_) => "comment",
119            Item::Move(_) => "move",
120        }
121    }
122}
123
124/// From the score's zero in its file's unit, beside the byte its value is written at.
125#[derive(Clone, Copy, Debug, PartialEq)]
126pub struct Time {
127    pub amount: Ratio,
128    pub at: usize,
129}
130
131#[derive(Clone, Debug, PartialEq)]
132pub struct Named {
133    pub name: String,
134    pub at: usize,
135}
136
137#[derive(Clone, Debug, PartialEq)]
138pub struct Event {
139    pub at: Time,
140    pub node: Named,
141    pub deg: Option<(i64, usize)>,
142    pub len: Option<Time>,
143    pub binds: Vec<Bind>,
144}
145
146/// A parameter a line binds: `key` is the byte its key is written at, `at` its value's.
147#[derive(Clone, Debug, PartialEq)]
148pub struct Bind {
149    pub name: String,
150    pub value: Expr,
151    pub key: usize,
152    pub at: usize,
153}
154
155#[derive(Clone, Debug, PartialEq)]
156pub struct Mark {
157    pub at: Time,
158    pub name: String,
159}
160
161#[derive(Clone, Debug, PartialEq)]
162pub struct Move {
163    pub at: Time,
164    pub node: Option<Named>,
165    pub lane: Named,
166    pub to: To,
167    pub ramp: Option<Ramp>,
168}
169
170/// A move's `to` as written, read once the lane's home value is known.
171#[derive(Clone, Debug, PartialEq)]
172pub struct To {
173    pub text: String,
174    /// The byte the value starts at, and the byte its text starts at, past a string's quote.
175    pub at: usize,
176    pub inner: usize,
177}
178
179#[derive(Clone, Copy, Debug, PartialEq)]
180pub struct Ramp {
181    pub len: Time,
182    pub shape: Shape,
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub enum Shape {
187    Lin,
188    Exp,
189}
190
191impl Shape {
192    pub fn name(self) -> &'static str {
193        match self {
194            Shape::Lin => "lin",
195            Shape::Exp => "exp",
196        }
197    }
198}
199
200pub fn spelled(amount: f64, unit: SpanUnit) -> String {
201    match unit {
202        SpanUnit::Bars => format!("{amount}b"),
203        SpanUnit::Seconds => format!("{amount}s"),
204    }
205}
206
207/// The line and column, from 1, a byte of `text` stands at; columns count bytes.
208pub fn line_column(text: &str, at: usize) -> (usize, usize) {
209    let before = &text[..at.min(text.len())];
210    let line = before.matches('\n').count() + 1;
211    let column = before.len() - before.rfind('\n').map_or(0, |n| n + 1) + 1;
212    (line, column)
213}
214
215pub fn refused(
216    file: String,
217    text: &str,
218    at: usize,
219    code: DiagCode,
220    message: &str,
221) -> crate::Refusal {
222    let place = line_column(text, at);
223    let diag = located(text, at, message);
224    crate::Refusal::new(file, diag.span, code, diag.message).placed(place)
225}
226
227pub fn located(text: &str, at: usize, message: &str) -> Diag {
228    let (line, column) = line_column(text, at);
229    Diag::new(
230        DiagCode::BadScore,
231        ByteSpan::at(at),
232        format!("line {line}, column {column}: {message}"),
233    )
234}