sva-ast 0.12.0

The document model for a composition
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// Concern: finds the reserved variables' values and lowers each read of them, again where one changes | Non-concern: score lines (score_check.rs) | IO: (&mut Graph[, beside]) -> Refusals

use crate::diag::{ByteSpan, DiagCode};
use crate::expr::{Address, Arg, BinOp, Binds, DEGREE, Expr, Literal, children, map_children_ok};
use crate::filename::{FileSpan, SpanUnit, parse_filename};
use crate::graph::{Defined, Graph, VARIABLES, resolve_ref_path};
use crate::ingest::{base_name, body_at};
use crate::refusal::Refusal;
use crate::scale::Scale;
use crate::score::refused;

/// Each one the node `variables/<name>`: a `b` literal or span reads `bpm` and `meter`, and a
/// `deg` call reads `key`, its tonic, and `scale`, a ref to the scale file it counts in.
pub const RESERVED_VARIABLES: [&str; 4] = ["bpm", "meter", "key", "scale"];

const BARS: [&str; 2] = ["bpm", "meter"];
const DEGREES: [&str; 2] = ["key", "scale"];

pub fn variable(name: &str) -> String {
    format!("{VARIABLES}/{name}")
}

/// The reserved variables a node's written body, defaults and file span read; a `repeat` or
/// `concat` of a file spanned in bars reads that span.
pub(crate) fn reads(
    body: &Expr,
    defaults: &[(String, Expr)],
    span: Option<FileSpan>,
) -> Vec<&'static str> {
    let mut bars = in_bars(span);
    let mut degrees = false;
    for e in std::iter::once(body).chain(defaults.iter().map(|(_, value)| value)) {
        scan(e, &mut bars, &mut degrees);
    }
    let mut out = Vec::new();
    if bars {
        out.extend(BARS);
    }
    if degrees {
        out.extend(DEGREES);
    }
    out
}

fn scan(e: &Expr, bars: &mut bool, degrees: &mut bool) {
    match e {
        Expr::Lit(Literal::Bars(_)) => *bars = true,
        Expr::Call { name, .. } if name == DEGREE => *degrees = true,
        Expr::Call { name, args, .. } if name == "repeat" || name == "concat" => {
            *bars |= args.iter().any(|arg| match arg {
                Arg::Pos(Expr::Ref { path, .. }) => in_bars(spanned(path)),
                _ => false,
            });
        }
        _ => {}
    }
    for c in children(e, Binds::Substitute) {
        scan(c, bars, degrees);
    }
}

fn spanned(path: &str) -> Option<FileSpan> {
    parse_filename(base_name(path)).1
}

fn in_bars(span: Option<FileSpan>) -> bool {
    span.is_some_and(|span| span.unit == SpanUnit::Bars)
}

/// `variables/bpm` and `variables/meter` together: seconds per bar is `beats * 60 / bpm`.
#[derive(Clone, Debug, PartialEq)]
pub struct Tempo {
    pub bpm: f64,
    pub meter: String,
    beats: f64,
}

impl Tempo {
    pub fn seconds_per_bar(&self) -> f64 {
        self.beats * 60.0 / self.bpm
    }

    fn seconds(&self, bars: f64) -> Expr {
        let num = |v: f64| Box::new(Expr::Lit(Literal::Num(v)));
        Expr::Bin(
            BinOp::Mul,
            num(bars),
            Box::new(Expr::Bin(BinOp::Div, num(self.beats * 60.0), num(self.bpm))),
        )
    }
}

/// What lowering reads off each reserved variable; the tonic stays a ref, so only whether it
/// is held.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Values {
    pub tempo: Option<Tempo>,
    pub scale: Option<Scale>,
    tonic: bool,
}

impl Values {
    /// Each reserved variable a node lowered under `other` reads otherwise under these.
    fn changed(&self, other: &Values) -> Vec<&'static str> {
        let mut out = Vec::new();
        if self.tempo != other.tempo {
            out.extend(BARS);
        }
        if self.scale != other.scale {
            out.push("scale");
        }
        out
    }
}

fn tempo_refusal(name: &str, reason: String) -> Refusal {
    Refusal::new(variable(name), ByteSpan::at(0), DiagCode::BadTempo, reason)
}

/// An unparsable numerator is refused, not guessed.
fn beats_per_bar(meter: &str) -> Result<f64, Refusal> {
    let numerator = &meter[..meter.find('/').unwrap_or(meter.len())];
    numerator.trim().parse::<f64>().map_err(|e| {
        tempo_refusal(
            "meter",
            format!(
                "meter `{meter}`'s beats-per-bar component `{numerator}` is not a number: {e}; \
                 a meter is `<beats>/<note>`, as `4/4`"
            ),
        )
    })
}

fn tempo(bpm: Option<&Expr>, meter: Option<&Expr>) -> Result<Option<Tempo>, Refusal> {
    match (bpm, meter) {
        (None, None) => Ok(None),
        (Some(Expr::Lit(Literal::Num(bpm))), Some(Expr::Lit(Literal::Str(meter)))) => {
            if *bpm <= 0.0 {
                return Err(tempo_refusal(
                    "bpm",
                    format!("bpm must be positive, got {bpm}"),
                ));
            }
            Ok(Some(Tempo {
                bpm: *bpm,
                meter: meter.clone(),
                beats: beats_per_bar(meter)?,
            }))
        }
        (Some(_), Some(_)) => Err(tempo_refusal(
            "bpm",
            "`variables/bpm` must be a number and `variables/meter` a meter, as `4/4`".to_string(),
        )),
        (Some(_), None) => Err(tempo_refusal(
            "meter",
            "`variables/bpm` is present but `variables/meter` is missing; both or neither"
                .to_string(),
        )),
        (None, Some(_)) => Err(tempo_refusal(
            "bpm",
            "`variables/meter` is present but `variables/bpm` is missing; both or neither"
                .to_string(),
        )),
    }
}

/// The one whole degree a `deg` call names, folded from literals.
fn degree(args: &[Arg]) -> Result<i64, String> {
    let whole = match args {
        [Arg::Pos(x)] => crate::series::folded(x, &|_| None)
            .filter(|d| d.fract() == 0.0 && d.abs() < 2f64.powi(53)),
        _ => None,
    };
    whole.map(|d| d as i64).ok_or_else(|| {
        "`deg` takes one whole scale degree folded from literals, as deg(5) or deg(-6); a \
         chromatic note is a degree times a ratio, as deg(7)*1st"
            .to_string()
    })
}

/// `variables/key` as a node at `path` spells a ref to it.
fn key_ref(path: &str) -> String {
    format!(
        "{}{}",
        "../".repeat(path.matches('/').count()),
        variable("key")
    )
}

impl Graph {
    /// Each reserved variable's value, read off the node this graph holds, else `beside`'s.
    pub fn values(&self, beside: Option<&Graph>) -> Result<Values, Vec<Refusal>> {
        let held = |path: &str| self.defines(path) || beside.is_some_and(|b| b.defines(path));
        let node = |name: &str| {
            let path = variable(name);
            self.expr(&path).or_else(|| beside?.expr(&path))
        };
        let tempo = tempo(node("bpm"), node("meter"));
        let scale = match node("scale") {
            None => Ok(None),
            Some(written) => self.scale_named(written, beside).map(Some),
        };
        match (tempo, scale) {
            (Ok(tempo), Ok(scale)) => Ok(Values {
                tempo,
                scale,
                tonic: held(&variable("key")),
            }),
            (tempo, scale) => Err(tempo.err().into_iter().chain(scale.err()).collect()),
        }
    }

    /// The scale `variables/scale` names by a bare ref.
    fn scale_named(&self, written: &Expr, beside: Option<&Graph>) -> Result<Scale, Refusal> {
        let path = variable("scale");
        let target = match written {
            Expr::Ref {
                path: to,
                arg,
                binds,
                address: Address::Time,
                ..
            } if binds.is_empty() && **arg == Expr::Var("t".to_string()) => {
                resolve_ref_path(&path, to)
            }
            _ => {
                return Err(Refusal::new(
                    path,
                    ByteSpan::at(0),
                    DiagCode::BadScale,
                    format!(
                        "`variables/scale` holds `{}`; it names the scale every `deg` counts in \
                         by a ref, as `@../scales/major`",
                        crate::print::render(written)
                    ),
                ));
            }
        };
        let found = target
            .as_deref()
            .and_then(|target| self.scale(target).or_else(|| beside?.scale(target)));
        match (target, found) {
            (Some(_), Some(scale)) => Ok(scale.clone()),
            (target, _) => Err(Refusal::new(
                path,
                ByteSpan::at(0),
                DiagCode::BadScale,
                format!(
                    "`variables/scale` names {}, and a scale is a file of rows of cents above the \
                     tonic, the last the period, as `scales/major` holds them",
                    match target {
                        Some(t) if self.defines(&t) || beside.is_some_and(|b| b.defines(&t)) => {
                            format!("`{t}`, no scale")
                        }
                        Some(t) => format!("`{t}`, which no file holds"),
                        None => "a path above the composition root".to_string(),
                    }
                ),
            )),
        }
    }

    /// Every read of a reserved variable lowered to its value: a `b` literal or span to
    /// seconds, a `deg` call to the tonic's ref times its ratio. Beside a graph lowered before,
    /// each of its nodes that read a variable under another value than it holds now is read
    /// again from its text and lowered here.
    pub fn lower_reserved(&mut self, beside: Option<&Graph>) -> Result<(), Vec<Refusal>> {
        let values = self.values(beside)?;
        let mut refusals = Vec::new();
        if let Some(beside) = beside {
            let stale = |path: &str| match beside.lowered_under(path) {
                None => false,
                Some(was) => {
                    let changed = values.changed(was);
                    beside
                        .reserved_reads(path)
                        .iter()
                        .any(|v| changed.contains(v))
                }
            };
            let stale: Vec<String> = beside
                .paths()
                .filter(|path| !self.defines(path) && stale(path))
                .map(str::to_string)
                .collect();
            for path in stale {
                if let Err(refused) = self.reread(&path, beside) {
                    refusals.push(refused);
                }
            }
        }
        let reading: Vec<String> = self
            .paths()
            .filter(|path| !self.reserved_reads(path).is_empty())
            .map(str::to_string)
            .collect();
        for path in reading {
            let defined = self.defined(&path).expect("a node this graph holds");
            match self.lowered(&path, defined, &values) {
                Ok(lowered) => self.lower(&path, lowered, &values),
                Err(refused) => refusals.extend(refused),
            }
        }
        if let Some(tempo) = &values.tempo {
            self.spans_in_seconds(tempo.seconds_per_bar());
        }
        match refusals.is_empty() {
            true => Ok(()),
            false => Err(refusals),
        }
    }

    /// `defined` at `path` with its reads of a reserved variable lowered under `values`; a
    /// refusal in a score names its line and column.
    pub(crate) fn lowered(
        &self,
        path: &str,
        defined: &Defined,
        values: &Values,
    ) -> Result<Defined, Vec<Refusal>> {
        let reads = match self.defines(path) {
            true => self.reserved_reads(path).to_vec(),
            false => reads(&defined.body, &defined.defaults, None),
        };
        let missing = |name: &str| match name {
            "bpm" | "meter" => values.tempo.is_none().then(|| {
                Refusal::new(
                    path,
                    ByteSpan::at(0),
                    DiagCode::BadTempo,
                    format!(
                        "`{path}` counts in bars, `b`, and this composition declares no tempo; \
                         add `variables/bpm` and `variables/meter`"
                    ),
                )
            }),
            "key" => (!values.tonic).then(|| {
                Refusal::new(
                    path,
                    ByteSpan::at(0),
                    DiagCode::BadScale,
                    format!(
                        "`{path}` calls `deg`, which counts from the tonic `variables/key` \
                         holds, and this composition has none"
                    ),
                )
            }),
            _ => None,
        };
        let mut refusals: Vec<Refusal> = reads.iter().filter_map(|name| missing(name)).collect();
        refusals.dedup_by(|a, b| a.reason == b.reason);
        if !refusals.is_empty() {
            return Err(refusals);
        }
        let text = self.text(path);
        let score = self.score(path).is_some();
        let refuse = |span: ByteSpan, message: String| match (score, text) {
            (true, Some(text)) => {
                let before = &text[..span.start];
                let line = before.matches('\n').count() + 1;
                let column = before.len() - before.rfind('\n').map_or(0, |at| at + 1) + 1;
                let diag = refused(span.start, (line, column), &message);
                Refusal::new(path, diag.span, diag.code, diag.message)
            }
            _ => Refusal::new(path, span, DiagCode::BadScale, message),
        };
        let from = match (score, text) {
            (false, Some(text)) => body_at(text),
            _ => 0,
        };
        let mut lowering = Lowering {
            values,
            key: key_ref(path),
            refuse: &refuse,
            refusals: Vec::new(),
        };
        let body = lowering.lower(&defined.body, from);
        let defaults = defined
            .defaults
            .iter()
            .map(|(name, value)| (name.clone(), lowering.lower(value, 0)))
            .collect();
        match lowering.refusals.is_empty() {
            true => Ok(Defined { body, defaults }),
            false => Err(lowering.refusals),
        }
    }
}

struct Lowering<'a> {
    values: &'a Values,
    key: String,
    refuse: &'a dyn Fn(ByteSpan, String) -> Refusal,
    refusals: Vec<Refusal>,
}

impl Lowering<'_> {
    /// `e` with each read lowered; `from` is where `e`'s spans start in its file. A degree it
    /// cannot read is refused and left as written.
    fn lower(&mut self, e: &Expr, from: usize) -> Expr {
        match e {
            Expr::Lit(Literal::Bars(bars)) => self
                .values
                .tempo
                .as_ref()
                .expect("a node counting in bars is refused with no tempo")
                .seconds(*bars),
            Expr::Call { name, args, span } if name == DEGREE => {
                let ratio = degree(args).and_then(|n| match &self.values.scale {
                    None => Err(
                        "`deg` counts in the scale `variables/scale` names, and this \
                         composition has none; write `@../scales/major` in `variables/scale`"
                            .to_string(),
                    ),
                    Some(scale) => scale.ratio(n).ok_or_else(|| {
                        format!("`deg` {n} lies so far from the tonic its ratio is no number")
                    }),
                });
                match ratio {
                    Ok(ratio) => {
                        let tonic = Expr::Ref {
                            path: self.key.clone(),
                            arg: Box::new(Expr::Var("t".to_string())),
                            binds: Vec::new(),
                            address: Address::Time,
                            span: *span,
                        };
                        Expr::Bin(
                            BinOp::Mul,
                            Box::new(tonic),
                            Box::new(Expr::Lit(Literal::Num(ratio))),
                        )
                    }
                    Err(message) => {
                        let at = ByteSpan::new(span.start + from, span.end + from);
                        self.refusals.push((self.refuse)(at, message));
                        e.clone()
                    }
                }
            }
            _ => map_children_ok(e, Binds::Substitute, |c| self.lower(c, from)),
        }
    }
}