1use crate::diag::{ByteSpan, DiagCode};
4use crate::expr::{Address, Arg, BinOp, Binds, DEGREE, Expr, Literal, children, map_children_ok};
5use crate::filename::{FileSpan, SpanUnit, parse_filename};
6use crate::graph::{Defined, Graph, VARIABLES, resolve_ref_path};
7use crate::ingest::{Data, Parsed, base_name, body_at};
8use crate::refusal::Refusal;
9use crate::scale::{BUILTIN_SCALES, Scale, builtin_scale};
10use crate::score::{Item, Ratio};
11
12pub const RESERVED_VARIABLES: [&str; 4] = ["bpm", "meter", "key", "scale"];
16
17const BARS: [&str; 2] = ["bpm", "meter"];
18const DEGREES: [&str; 2] = ["key", "scale"];
19
20pub fn variable(name: &str) -> String {
21 format!("{VARIABLES}/{name}")
22}
23
24pub(crate) fn reads(
27 body: &Expr,
28 defaults: &[(String, Expr)],
29 span: Option<FileSpan>,
30) -> Vec<&'static str> {
31 let mut bars = in_bars(span);
32 let mut degrees = false;
33 for e in std::iter::once(body).chain(defaults.iter().map(|(_, value)| value)) {
34 scan(e, &mut bars, &mut degrees);
35 }
36 let mut out = Vec::new();
37 if bars {
38 out.extend(BARS);
39 }
40 if degrees {
41 out.extend(DEGREES);
42 }
43 out
44}
45
46pub(crate) fn parsed_reads(parsed: &Parsed, span: Option<FileSpan>) -> Vec<&'static str> {
49 let Some(Data::Score(score)) = &parsed.data else {
50 return reads(&parsed.expr, &parsed.defaults, span);
51 };
52 let mut bars = in_bars(span);
53 let mut degrees = score.reads_degrees();
54 for (_, item) in score.items() {
55 let lasts = match item {
56 Item::Event(event) => {
57 for bind in &event.binds {
58 scan(&bind.value, &mut bars, &mut degrees);
59 }
60 event.len.is_some()
61 }
62 Item::Move(m) => m.ramp.is_some(),
63 _ => false,
64 };
65 bars |= score.unit == SpanUnit::Bars && (lasts || item.at().amount != Ratio::ZERO);
66 }
67 let mut out = Vec::new();
68 if bars {
69 out.extend(BARS);
70 }
71 if degrees {
72 out.extend(DEGREES);
73 }
74 out
75}
76
77fn scan(e: &Expr, bars: &mut bool, degrees: &mut bool) {
78 match e {
79 Expr::Lit(Literal::Bars(_)) => *bars = true,
80 Expr::Call { name, .. } if name == DEGREE => *degrees = true,
81 Expr::Call { name, args, .. } if name == "repeat" || name == "concat" => {
82 *bars |= args.iter().any(|arg| match arg {
83 Arg::Pos(Expr::Ref { path, .. }) => in_bars(spanned(path)),
84 _ => false,
85 });
86 }
87 _ => {}
88 }
89 for c in children(e, Binds::Substitute) {
90 scan(c, bars, degrees);
91 }
92}
93
94fn spanned(path: &str) -> Option<FileSpan> {
95 parse_filename(base_name(path)).1
96}
97
98fn in_bars(span: Option<FileSpan>) -> bool {
99 span.is_some_and(|span| span.unit == SpanUnit::Bars)
100}
101
102#[derive(Clone, Debug, PartialEq)]
104pub struct Tempo {
105 pub bpm: f64,
106 pub meter: String,
107 beats: f64,
108}
109
110impl Tempo {
111 pub fn seconds_per_bar(&self) -> f64 {
112 self.beats * 60.0 / self.bpm
113 }
114
115 pub fn beats(&self) -> f64 {
116 self.beats
117 }
118
119 fn seconds(&self, bars: f64) -> Expr {
120 let num = |v: f64| Box::new(Expr::Lit(Literal::Num(v)));
121 Expr::Bin(
122 BinOp::Mul,
123 num(bars),
124 Box::new(Expr::Bin(BinOp::Div, num(self.beats * 60.0), num(self.bpm))),
125 )
126 }
127}
128
129#[derive(Clone, Debug, Default, PartialEq)]
132pub struct Values {
133 pub tempo: Option<Tempo>,
134 pub scale: Option<Scale>,
135 tonic: bool,
136}
137
138impl Values {
139 fn changed(&self, other: &Values) -> Vec<&'static str> {
141 let mut out = Vec::new();
142 if self.tempo != other.tempo {
143 out.extend(BARS);
144 }
145 if self.scale != other.scale {
146 out.push("scale");
147 }
148 out
149 }
150}
151
152fn tempo_refusal(name: &str, reason: String) -> Refusal {
153 Refusal::new(variable(name), ByteSpan::at(0), DiagCode::BadTempo, reason)
154}
155
156fn beats_per_bar(meter: &str) -> Result<f64, Refusal> {
158 let numerator = &meter[..meter.find('/').unwrap_or(meter.len())];
159 numerator.trim().parse::<f64>().map_err(|e| {
160 tempo_refusal(
161 "meter",
162 format!(
163 "meter `{meter}`'s beats-per-bar component `{numerator}` is not a number: {e}; \
164 a meter is `<beats>/<note>`, as `4/4`"
165 ),
166 )
167 })
168}
169
170fn tempo(bpm: Option<&Expr>, meter: Option<&Expr>) -> Result<Option<Tempo>, Refusal> {
171 match (bpm, meter) {
172 (None, None) => Ok(None),
173 (Some(Expr::Lit(Literal::Num(bpm))), Some(Expr::Lit(Literal::Str(meter)))) => {
174 if *bpm <= 0.0 {
175 return Err(tempo_refusal(
176 "bpm",
177 format!("bpm must be positive, got {bpm}"),
178 ));
179 }
180 Ok(Some(Tempo {
181 bpm: *bpm,
182 meter: meter.clone(),
183 beats: beats_per_bar(meter)?,
184 }))
185 }
186 (Some(_), Some(_)) => Err(tempo_refusal(
187 "bpm",
188 "`variables/bpm` must be a number and `variables/meter` a meter, as `4/4`".to_string(),
189 )),
190 (Some(_), None) => Err(tempo_refusal(
191 "meter",
192 "`variables/bpm` is present but `variables/meter` is missing; both or neither"
193 .to_string(),
194 )),
195 (None, Some(_)) => Err(tempo_refusal(
196 "bpm",
197 "`variables/meter` is present but `variables/bpm` is missing; both or neither"
198 .to_string(),
199 )),
200 }
201}
202
203pub(crate) fn degree(args: &[Arg]) -> Result<i64, String> {
205 let whole = match args {
206 [Arg::Pos(x)] => crate::series::folded(x, &|_| None)
207 .filter(|d| d.fract() == 0.0 && d.abs() < 2f64.powi(53)),
208 _ => None,
209 };
210 whole.map(|d| d as i64).ok_or_else(|| {
211 "`deg` takes one whole scale degree folded from literals, as deg(5) or deg(-6); a \
212 chromatic note is a degree times a ratio, as deg(7)*1st"
213 .to_string()
214 })
215}
216
217pub(crate) fn tonic_times(key: &str, ratio: f64, span: ByteSpan) -> Expr {
219 let tonic = Expr::Ref {
220 path: key.to_string(),
221 arg: Box::new(Expr::Var("t".to_string())),
222 binds: Vec::new(),
223 address: Address::Time,
224 span,
225 };
226 Expr::Bin(
227 BinOp::Mul,
228 Box::new(tonic),
229 Box::new(Expr::Lit(Literal::Num(ratio))),
230 )
231}
232
233pub(crate) fn key_ref(path: &str) -> String {
235 format!(
236 "{}{}",
237 "../".repeat(path.matches('/').count()),
238 variable("key")
239 )
240}
241
242fn scale_name(written: &Expr) -> Option<String> {
245 match written {
246 Expr::Var(word) => Some(word.clone()),
247 Expr::Bin(BinOp::Sub, left, right) => {
248 Some(format!("{}-{}", scale_name(left)?, scale_name(right)?))
249 }
250 _ => None,
251 }
252}
253
254fn builtin_names() -> String {
255 BUILTIN_SCALES.map(|(name, ..)| name).join(", ")
256}
257
258impl Graph {
259 pub fn values(&self, beside: Option<&Graph>) -> Result<Values, Vec<Refusal>> {
261 let held = |path: &str| self.defines(path) || beside.is_some_and(|b| b.defines(path));
262 let node = |name: &str| {
263 let path = variable(name);
264 self.source(&path)
265 .or_else(|| beside?.source(&path))
266 .map(|d| &d.body)
267 };
268 let tempo = tempo(node("bpm"), node("meter"));
269 let scale = match node("scale") {
270 None => Ok(None),
271 Some(written) => self
272 .scale_at(written, &variable("scale"), beside)
273 .map(Some)
274 .map_err(|reason| {
275 Refusal::new(
276 variable("scale"),
277 ByteSpan::at(0),
278 DiagCode::BadScale,
279 reason,
280 )
281 }),
282 };
283 match (tempo, scale) {
284 (Ok(tempo), Ok(scale)) => Ok(Values {
285 tempo,
286 scale,
287 tonic: held(&variable("key")),
288 }),
289 (tempo, scale) => Err(tempo.err().into_iter().chain(scale.err()).collect()),
290 }
291 }
292
293 pub(crate) fn scale_at(
296 &self,
297 written: &Expr,
298 referencing: &str,
299 beside: Option<&Graph>,
300 ) -> Result<Scale, String> {
301 let target = match written {
302 Expr::Ref {
303 path: to,
304 arg,
305 binds,
306 address: Address::Time,
307 ..
308 } if binds.is_empty() && **arg == Expr::Var("t".to_string()) => {
309 resolve_ref_path(referencing, to)
310 }
311 _ => {
312 return match scale_name(written) {
313 Some(name) => builtin_scale(&name).ok_or_else(|| {
314 format!(
315 "`{referencing}` names `{name}`, which is no built-in scale; the \
316 built-ins are {}, and any other scale is a file of cents named by \
317 a ref, as `@../scales/custom`",
318 builtin_names()
319 )
320 }),
321 None => Err(format!(
322 "`{referencing}` holds `{}`; a scale is a built-in named bare, as \
323 `minor`, or a scale file named by a ref, as `@../scales/custom`",
324 crate::print::render(written)
325 )),
326 };
327 }
328 };
329 let found = target
330 .as_deref()
331 .and_then(|target| self.scale(target).or_else(|| beside?.scale(target)));
332 match (target, found) {
333 (Some(_), Some(scale)) => Ok(scale.clone()),
334 (target, _) => Err(format!(
335 "`{referencing}` names {}, and a scale is a file of rows of cents above the \
336 tonic, the last the period, as `scales/custom` would hold them, or a built-in \
337 named bare: {}",
338 match target {
339 Some(t) if self.defines(&t) || beside.is_some_and(|b| b.defines(&t)) => {
340 format!("`{t}`, no scale")
341 }
342 Some(t) => format!("`{t}`, which no file holds"),
343 None => "a path above the composition root".to_string(),
344 },
345 builtin_names()
346 )),
347 }
348 }
349
350 pub fn lower_reserved(&mut self, beside: Option<&Graph>) -> Result<(), Vec<Refusal>> {
355 let values = self.values(beside)?;
356 let mut refusals = Vec::new();
357 if let Some(beside) = beside {
358 let stale = |path: &str| match beside.lowered_under(path) {
359 None => false,
360 Some(was) => {
361 let changed = values.changed(was);
362 beside
363 .reserved_reads(path)
364 .iter()
365 .any(|v| changed.contains(v))
366 }
367 };
368 let stale: Vec<String> = beside
369 .paths()
370 .filter(|path| !self.defines(path) && stale(path))
371 .map(str::to_string)
372 .collect();
373 for path in stale {
374 if let Err(refused) = self.reread(&path, beside) {
375 refusals.push(refused);
376 }
377 }
378 }
379 let reading: Vec<String> = self
380 .paths()
381 .filter(|path| !self.reserved_reads(path).is_empty())
382 .map(str::to_string)
383 .collect();
384 for path in reading {
385 let defined = self.defined(&path).expect("a node this graph holds");
386 match self.lowered(&path, defined, &values) {
387 Ok(lowered) => self.lower(&path, lowered, &values),
388 Err(refused) => refusals.extend(refused),
389 }
390 }
391 match refusals.is_empty() {
392 true => Ok(()),
393 false => Err(refusals),
394 }
395 }
396
397 pub(crate) fn lowered(
400 &self,
401 path: &str,
402 defined: &Defined,
403 values: &Values,
404 ) -> Result<Defined, Vec<Refusal>> {
405 self.lowered_with(path, defined, values, None)
406 }
407
408 pub(crate) fn lowered_with(
411 &self,
412 path: &str,
413 defined: &Defined,
414 values: &Values,
415 ratio: Option<&dyn Fn(i64) -> Expr>,
416 ) -> Result<Defined, Vec<Refusal>> {
417 let reads = match self.defines(path) {
418 true => self.reserved_reads(path).to_vec(),
419 false => reads(&defined.body, &defined.defaults, None),
420 };
421 let missing = |name: &str| match name {
422 "bpm" | "meter" => values.tempo.is_none().then(|| {
423 Refusal::new(
424 path,
425 ByteSpan::at(0),
426 DiagCode::BadTempo,
427 format!(
428 "`{path}` counts in bars, `b`, and this composition declares no tempo; \
429 add `variables/bpm` and `variables/meter`"
430 ),
431 )
432 }),
433 "key" => (!values.tonic).then(|| {
434 Refusal::new(
435 path,
436 ByteSpan::at(0),
437 DiagCode::BadScale,
438 format!(
439 "`{path}` calls `deg`, which counts from the tonic `variables/key` \
440 holds, and this composition has none"
441 ),
442 )
443 }),
444 _ => None,
445 };
446 let mut refusals: Vec<Refusal> = reads.iter().filter_map(|name| missing(name)).collect();
447 refusals.dedup_by(|a, b| a.reason == b.reason);
448 if !refusals.is_empty() {
449 return Err(refusals);
450 }
451 let text = self.text(path);
452 let score = self.score(path).is_some();
453 let refuse = |span: ByteSpan, message: String| match (score, text) {
454 (true, Some(text)) => crate::score::refused(
455 format!("{path}{}", crate::score::EXTENSION),
456 text,
457 span.start,
458 DiagCode::BadScore,
459 &message,
460 ),
461 _ => Refusal::new(path, span, DiagCode::BadScale, message),
462 };
463 let from = match (score, text) {
464 (false, Some(text)) => body_at(text),
465 _ => 0,
466 };
467 let mut lowering = Lowering {
468 values,
469 key: key_ref(path),
470 refuse: &refuse,
471 refusals: Vec::new(),
472 ratio,
473 };
474 let body = lowering.lower(&defined.body, from);
475 let defaults = defined
476 .defaults
477 .iter()
478 .map(|(name, value)| (name.clone(), lowering.lower(value, 0)))
479 .collect();
480 match lowering.refusals.is_empty() {
481 true => Ok(Defined { body, defaults }),
482 false => Err(lowering.refusals),
483 }
484 }
485}
486
487struct Lowering<'a> {
488 values: &'a Values,
489 key: String,
490 refuse: &'a dyn Fn(ByteSpan, String) -> Refusal,
491 refusals: Vec<Refusal>,
492 ratio: Option<&'a dyn Fn(i64) -> Expr>,
493}
494
495impl Lowering<'_> {
496 fn lower(&mut self, e: &Expr, from: usize) -> Expr {
499 match e {
500 Expr::Lit(Literal::Bars(bars)) => self
501 .values
502 .tempo
503 .as_ref()
504 .expect("a node counting in bars is refused with no tempo")
505 .seconds(*bars),
506 Expr::Call { name, args, span } if name == DEGREE && self.ratio.is_some() => {
507 match degree(args) {
508 Ok(n) => {
509 let ratio = (self.ratio.expect("matched"))(n);
510 let tonic = tonic_times(&self.key, 1.0, *span);
511 let Expr::Bin(_, tonic, _) = tonic else {
512 unreachable!("a product")
513 };
514 Expr::Bin(BinOp::Mul, tonic, Box::new(ratio))
515 }
516 Err(message) => {
517 let at = ByteSpan::new(span.start + from, span.end + from);
518 self.refusals.push((self.refuse)(at, message));
519 e.clone()
520 }
521 }
522 }
523 Expr::Call { name, args, span } if name == DEGREE => {
524 let ratio = degree(args).and_then(|n| match &self.values.scale {
525 None => Err(
526 "`deg` counts in the scale `variables/scale` names, and this \
527 composition has none; write a built-in's name in `variables/scale`, \
528 as `major`, or a ref to a scale file"
529 .to_string(),
530 ),
531 Some(scale) => scale.ratio(n).ok_or_else(|| {
532 format!("`deg` {n} lies so far from the tonic its ratio is no number")
533 }),
534 });
535 match ratio {
536 Ok(ratio) => tonic_times(&self.key, ratio, *span),
537 Err(message) => {
538 let at = ByteSpan::new(span.start + from, span.end + from);
539 self.refusals.push((self.refuse)(at, message));
540 e.clone()
541 }
542 }
543 }
544 _ => map_children_ok(e, Binds::Substitute, |c| self.lower(c, from)),
545 }
546 }
547}