1use std::borrow::Cow;
4use std::cell::RefCell;
5use std::collections::BTreeMap;
6
7use sva_formula::closed_form::children;
8use sva_formula::spectral_sum::atom::Indicator;
9use sva_formula::spectral_sum::build::{multiply_lanes_read, sole_constant};
10use sva_formula::spectral_sum::image;
11use sva_formula::spectral_sum::merge::simplify;
12use sva_formula::{
13 Body, C64, ClosedForm, Lane, Left, NodeId, Opaque, Part, Reads, SpectralSum, Through, Var,
14 dual_read, inverse_read, normalize_closed_form, normalize_read,
15};
16
17use crate::cast::Cast;
18use crate::error::{Diagnostic, EngineError, Located};
19use crate::typing::{Typing, Value};
20
21mod identity;
22mod prefix;
23
24pub use identity::{identity, symbolic_hash};
25pub(crate) use identity::{passes, subterm_identity};
26pub(crate) use prefix::switches;
27
28pub fn spectral_sum_of(
31 typing: &Typing,
32 node: NodeId,
33 want: Var,
34) -> Result<SpectralSum, EngineError> {
35 let held = |id: NodeId| {
36 let var = typing.var(id);
37 let key = identity(typing, id).ok();
38 typing.folds().refused((id, var)).is_some()
39 || key.is_some_and(|key| typing.folds().composed((key, var)).is_some())
40 };
41 for read in typing.unfolded_over(node, |id| composes(typing, id), held) {
42 if read != node {
43 let _ = composed(typing, read, typing.var(read), &mut Open::default());
44 }
45 }
46 composed(typing, node, want, &mut Open::default())
47}
48
49fn composes(typing: &Typing, id: NodeId) -> Vec<NodeId> {
50 match typing.value(id) {
51 Value::ClosedForm(form) => nodes_in(&form.body),
52 Value::Cast(Cast::Fourier | Cast::IFourier, source) => vec![*source],
53 _ => Vec::new(),
54 }
55}
56
57#[derive(Default)]
60struct Open {
61 chain: Vec<NodeId>,
62 cut: bool,
63}
64
65fn composed(
69 typing: &Typing,
70 node: NodeId,
71 want: Var,
72 open: &mut Open,
73) -> Result<SpectralSum, EngineError> {
74 let here = written_at(typing, node);
75 let key = identity(typing, node).ok().map(|held| (held, want));
76 if let Some(held) = key.and_then(|key| typing.folds().composed(key)) {
77 return Ok(held.located(here));
78 }
79 if let Some(refused) = typing.folds().refused((node, want)) {
80 return Err(refused);
81 }
82 let outer = std::mem::take(&mut open.cut);
83 let found = composing(typing, node, want, open);
84 match (&found, key) {
85 (Ok(found), Some(key)) => {
86 let unlocated = found.clone().located(sva_formula::Origin::UNKNOWN);
87 typing.folds().keep_composed(key, unlocated);
88 }
89 (Err(refused), _) if !open.cut => {
90 typing.folds().keep_refused((node, want), refused.clone());
91 }
92 _ => {}
93 }
94 open.cut |= outer;
95 Ok(found?.located(here))
96}
97
98fn written_at(typing: &Typing, node: NodeId) -> sva_formula::Origin {
99 match typing.value(node) {
100 Value::ClosedForm(form) => form.origin,
101 Value::Cast(_, source) => written_at(typing, *source),
102 _ => sva_formula::Origin::UNKNOWN,
103 }
104}
105
106fn composing(
109 typing: &Typing,
110 node: NodeId,
111 want: Var,
112 open: &mut Open,
113) -> Result<SpectralSum, EngineError> {
114 #[cfg(test)]
115 typing
116 .folds()
117 .composings
118 .set(typing.folds().composings.get() + 1);
119 if open.chain.contains(&node) {
120 open.cut = true;
121 return Err(cyclic(typing, node));
122 }
123 open.chain.push(node);
124 let held = match typing.value(node) {
125 Value::ClosedForm(form) => {
126 let body = fold_constants(typing, &form.body);
127 compose(typing, &body, form.var, open)?
128 }
129 Value::Cast(Cast::Fourier, source) => {
130 let inner = composed(typing, *source, Var::T, open)?;
131 turn(typing, node, read_through(typing, |t| dual_read(&inner, t)))?
132 }
133 Value::Cast(Cast::IFourier, source) => {
134 let inner = composed(typing, *source, Var::F, open)?;
135 turn(
136 typing,
137 node,
138 read_through(typing, |t| inverse_read(&inner, t)),
139 )?
140 }
141 Value::Op { name, .. } if typing.ty(node).is_closed_form() => {
142 return Err(across(typing, node, name));
143 }
144 _ => return Err(no_closed_form(typing, node)),
145 };
146 open.chain.pop();
147 on_axis(typing, node, held, typing.var(node), want)
148}
149
150pub(crate) fn cyclic(typing: &Typing, node: NodeId) -> EngineError {
151 EngineError::refused(Diagnostic {
152 code: "engine.cyclic_substitution".to_string(),
153 message: format!(
154 "`{}` reads itself around a loop of refs.",
155 typing.name(node)
156 ),
157 location: Located::at(typing.name(node), None),
158 help: "write the loop with self(...), which the engine classifies".to_string(),
159 })
160}
161
162fn on_axis(
163 typing: &Typing,
164 node: NodeId,
165 held: SpectralSum,
166 axis: Var,
167 want: Var,
168) -> Result<SpectralSum, EngineError> {
169 if axis == want {
170 return Ok(held);
171 }
172 let turned = read_through(typing, |through| match want {
173 Var::F => dual_read(&held, through),
174 Var::T => inverse_read(&held, through),
175 });
176 turn(typing, node, turned)
177}
178
179fn turn(
180 typing: &Typing,
181 node: NodeId,
182 turned: Result<SpectralSum, Left>,
183) -> Result<SpectralSum, EngineError> {
184 turned.map_err(|left| {
185 EngineError::of_closed_form(
186 &left.refusal(),
187 typing.locate(left.origin),
188 format!(
189 "write `{}` inside sample(...) to leave A deliberately",
190 typing.name(node)
191 ),
192 )
193 })
194}
195
196pub(crate) fn across(typing: &Typing, node: NodeId, call: &str) -> EngineError {
199 no_spectral_sum(typing.name(node), call)
200}
201
202fn no_spectral_sum(node: &str, blocking: &str) -> EngineError {
205 EngineError::refused(Diagnostic {
206 code: "read.no_spectral_sum".to_string(),
207 message: format!(
208 "`{node}` has no spectral sum to read: `{blocking}` composes no value across a ref."
209 ),
210 location: Located::at(node, None),
211 help: "write the construct inside the node it reads, or read it off sample(...)"
212 .to_string(),
213 })
214}
215
216fn no_closed_form(typing: &Typing, node: NodeId) -> EngineError {
217 EngineError::refused(Diagnostic {
218 code: "type.samples_in_closed_form".to_string(),
219 message: format!(
220 "`{}` is samples; nothing returns from samples to a closed form",
221 typing.name(node)
222 ),
223 location: Located::at(typing.name(node), None),
224 help: "read it as samples, or build the closed form without it".to_string(),
225 })
226}
227
228pub(crate) fn fold_constants<'a>(typing: &Typing, f: &'a Body) -> Cow<'a, Body> {
231 let fold = &mut Folding::default();
232 match names_number(typing, f, fold) {
233 true => Cow::Owned(folded(typing, f, fold)),
234 false => Cow::Borrowed(f),
235 }
236}
237
238fn names_number(typing: &Typing, f: &Body, fold: &mut Folding) -> bool {
239 match f {
240 Body::Node(id) => number(typing, *id, fold).is_some(),
241 _ => children(f)
242 .into_iter()
243 .any(|p| names_number(typing, &p.body, fold)),
244 }
245}
246
247#[derive(Default)]
250struct Folding {
251 open: Vec<NodeId>,
252 cut: bool,
253}
254
255fn folded(typing: &Typing, f: &Body, fold: &mut Folding) -> Body {
256 let Body::Node(id) = f else {
257 return sva_formula::closed_form::map_children(f, |p| {
258 Part::new(p.origin, folded(typing, &p.body, fold))
259 });
260 };
261 match number(typing, *id, fold) {
262 Some(c) => Body::Const(c),
263 None => f.clone(),
264 }
265}
266
267fn number(typing: &Typing, node: NodeId, fold: &mut Folding) -> Option<C64> {
269 if let Some(held) = typing.folds().number(node) {
270 return held;
271 }
272 if fold.open.contains(&node) {
273 fold.cut = true;
274 return None;
275 }
276 let Value::ClosedForm(form) = typing.value(node) else {
277 return None;
278 };
279 if let Body::Const(c) = form.body {
280 return Some(c);
281 }
282 let outer = std::mem::take(&mut fold.cut);
283 fold.open.push(node);
284 let folded = names_number(typing, &form.body, fold).then(|| ClosedForm {
285 body: folded(typing, &form.body, fold),
286 ..*form
287 });
288 fold.open.pop();
289 let number = normalize_closed_form(folded.as_ref().unwrap_or(form))
290 .ok()
291 .and_then(|sum| sole_constant(&sum));
292 if !fold.cut {
293 typing.folds().keep_number(node, number);
294 }
295 fold.cut |= outer;
296 number
297}
298
299fn compose(
302 typing: &Typing,
303 body: &Body,
304 var: Var,
305 open: &mut Open,
306) -> Result<SpectralSum, EngineError> {
307 let here = *open
308 .chain
309 .last()
310 .expect("compose runs inside the node it composes");
311 if !holds_node(body) {
312 return normalize_here(typing, body, var);
313 }
314 match body {
315 Body::Node(id) => composed(typing, *id, var, open),
316 Body::Add(parts) => {
317 let mut lanes: Vec<Lane> = Vec::new();
318 for part in parts {
319 add_into(&mut lanes, compose(typing, &part.body, var, open)?);
320 }
321 Ok(sum(var, lanes))
322 }
323 Body::Mul(parts) => {
324 let mut acc: Option<SpectralSum> = None;
325 for part in parts {
326 let next = compose(typing, &part.body, var, open)?;
327 acc = Some(match acc {
328 None => next,
329 Some(held) => multiply(typing, &held, &next, var)?,
330 });
331 }
332 Ok(acc.unwrap_or_else(|| sum(var, Vec::new())))
333 }
334 Body::Shift { by, of } => {
335 let held = compose(typing, &of.body, var, open)?;
336 image::shift(held, *by).map_err(|left| left_of(typing, left))
337 }
338 Body::Crop {
339 of,
340 l,
341 r,
342 rise,
343 fall,
344 } if *rise > 0.0 || *fall > 0.0 => {
345 let held = compose(typing, &of.body, var, open)?;
346 let window = image::crop_window(*l, *r, *rise, *fall, of.origin, var);
347 multiply(typing, &held, &window, var)
348 }
349 Body::Crop { of, l, r, .. } => {
350 let held = compose(typing, &of.body, var, open)?;
351 image::crop(held, Indicator { l: *l, r: *r }).map_err(|left| left_of(typing, left))
352 }
353 Body::Div(num, den) => {
354 let over = compose(typing, &den.body, var, open)?;
355 let numerator = compose(typing, &num.body, var, open)?;
356 multiply(typing, &numerator, &reciprocal(typing, here, &over)?, var)
357 }
358 Body::Join(parts) => {
359 let mut lanes = Vec::new();
360 for part in parts {
361 lanes.extend(compose(typing, &part.body, var, open)?.lanes);
362 }
363 Ok(sum(var, lanes))
364 }
365 Body::Channel(of, k) => {
366 let held = compose(typing, &of.body, var, open)?;
367 match held.lanes.into_iter().nth(usize::from(*k)) {
368 Some(lane) => Ok(sum(var, vec![lane])),
369 None => Err(unsubstituted(typing, here, body)),
370 }
371 }
372 other if reads_through(typing, other, var) => read_through(typing, |through| {
374 normalize_with(typing, other, var, through)
375 }),
376 other => Err(unsubstituted(typing, here, other)),
377 }
378}
379
380fn reciprocal(
382 typing: &Typing,
383 node: NodeId,
384 over: &SpectralSum,
385) -> Result<SpectralSum, EngineError> {
386 let divided = || no_spectral_sum(typing.name(node), "a division by a closed form");
387 let [lane] = over.lanes.as_slice() else {
388 return Err(divided());
389 };
390 match lane.atoms.as_slice() {
391 [atom] if atom.is_bare() => Ok(SpectralSum::mono(
392 over.var,
393 vec![sva_formula::spectral_sum::atom::SpectralAtom::constant(
394 atom.c.inv(),
395 atom.origin,
396 )],
397 )),
398 _ => Err(divided()),
399 }
400}
401
402fn left_of(typing: &Typing, left: Left) -> EngineError {
403 EngineError::of_closed_form(
404 &left.refusal(),
405 typing.locate(left.origin),
406 "write the subterm inside sample(...) to leave A deliberately",
407 )
408}
409
410fn normalize_here(typing: &Typing, body: &Body, var: Var) -> Result<SpectralSum, EngineError> {
411 normalize_with(typing, body, var, &Opaque)
412}
413
414fn normalize_with(
415 typing: &Typing,
416 body: &Body,
417 var: Var,
418 reads: &dyn Reads,
419) -> Result<SpectralSum, EngineError> {
420 normalize_read(body, var, reads).map_err(|left| {
421 EngineError::of_closed_form(
422 &left.refusal(),
423 typing.locate(left.origin),
424 "write the subterm inside sample(...) to leave A deliberately",
425 )
426 })
427}
428
429fn unsubstituted(typing: &Typing, node: NodeId, body: &Body) -> EngineError {
432 no_spectral_sum(typing.name(node), named(body))
433}
434
435fn named(body: &Body) -> &'static str {
436 match body {
437 Body::Apply(op, _) => op.name(),
438 Body::Pow(..) => "pow",
439 Body::Fold(..) => "max, min or mod",
440 Body::Join(_) => "join",
441 Body::Channel(..) => "ch",
442 Body::Series(_) => "sum",
443 Body::Delta { .. } => "delta",
444 Body::Pv(_) => "pv",
445 Body::Deriv { .. } => "a derivative",
446 Body::Warp { .. } => "a warped time",
447 _ => "a construct",
448 }
449}
450
451pub fn nodes_in(f: &Body) -> Vec<NodeId> {
453 let mut out = Vec::new();
454 collect_nodes(f, &mut out);
455 out
456}
457
458fn collect_nodes(f: &Body, out: &mut Vec<NodeId>) {
459 if let Body::Node(id) = f {
460 if !out.contains(id) {
461 out.push(*id);
462 }
463 return;
464 }
465 for part in children(f) {
466 collect_nodes(&part.body, out);
467 }
468}
469
470fn holds_node(f: &Body) -> bool {
471 matches!(f, Body::Node(_)) || children(f).iter().any(|p| holds_node(&p.body))
472}
473
474fn sum(var: Var, mut lanes: Vec<Lane>) -> SpectralSum {
475 for lane in &mut lanes {
476 simplify(lane);
477 }
478 SpectralSum::of(var, lanes)
479}
480
481fn add_into(lanes: &mut Vec<Lane>, other: SpectralSum) {
483 if other.lanes.is_empty() {
484 return;
485 }
486 let width = lanes.len().max(other.lanes.len());
487 if lanes.len() == 1 {
488 let held = lanes[0].clone();
489 lanes.resize(width, held);
490 }
491 for at in 0..width {
492 let lane = lane_at(&other, at).clone();
493 match lanes.get_mut(at) {
494 Some(held) => {
495 held.atoms.extend(lane.atoms);
496 held.series.extend(lane.series);
497 held.modal.extend(lane.modal);
498 }
499 None => lanes.push(lane),
500 }
501 }
502}
503
504fn multiply(
505 typing: &Typing,
506 a: &SpectralSum,
507 b: &SpectralSum,
508 var: Var,
509) -> Result<SpectralSum, EngineError> {
510 let width = a.lanes.len().max(b.lanes.len());
511 let mut lanes = Vec::with_capacity(width);
512 for at in 0..width {
513 let (x, y) = (lane_at(a, at).clone(), lane_at(b, at).clone());
514 let held = read_through(typing, |through| multiply_lanes_read(x, y, through));
515 lanes.push(held.map_err(|left| {
516 EngineError::of_closed_form(
517 &left.refusal(),
518 typing.locate(left.origin),
519 "write one of the factors inside sample(...)",
520 )
521 })?);
522 }
523 Ok(sum(var, lanes))
524}
525
526fn lane_at(n: &SpectralSum, at: usize) -> &Lane {
527 n.lanes.get(at).unwrap_or(&n.lanes[0])
528}
529
530pub(crate) fn reads_through(typing: &Typing, body: &Body, var: Var) -> bool {
533 let open = &mut Vec::new();
534 nodes_in(body)
535 .into_iter()
536 .all(|id| inlinable(typing, id, var, open))
537}
538
539pub(crate) fn sums_through(typing: &Typing, form: &ClosedForm) -> bool {
543 let Body::Series(s) = &form.body else {
544 return false;
545 };
546 let refs = nodes_in(&form.body);
547 form.var == Var::T
548 && s.hi == sva_formula::Bound::Infinite
549 && moves_a_ref(&s.term.body, s.index)
550 && reads_through(typing, &form.body, Var::T)
551 && refs
552 .iter()
553 .all(|id| spectral_sum_of(typing, *id, Var::T).is_ok())
554}
555
556fn moves_a_ref(f: &Body, k: sva_formula::IndexId) -> bool {
557 match f {
558 Body::Warp { at, of } if matches!(*of.body, Body::Node(_)) => {
559 sva_formula::series::mentions(&at.body, k)
560 }
561 other => children(other).iter().any(|p| moves_a_ref(&p.body, k)),
562 }
563}
564
565fn inlinable(typing: &Typing, id: NodeId, var: Var, open: &mut Vec<NodeId>) -> bool {
567 if let Some(held) = typing.folds().inlinable((id, var)) {
568 return held;
569 }
570 if open.contains(&id) {
571 return false;
572 }
573 let found = match typing.value(id) {
574 Value::ClosedForm(form) if form.var == var => {
575 open.push(id);
576 let held = nodes_in(&form.body)
577 .into_iter()
578 .all(|n| inlinable(typing, n, var, open));
579 open.pop();
580 held
581 }
582 _ => false,
583 };
584 typing.folds().keep_inlinable((id, var), found);
585 found
586}
587
588pub(crate) struct PerNode<T>(RefCell<BTreeMap<NodeId, T>>);
590
591impl<T: Clone> PerNode<T> {
592 pub(crate) fn new() -> PerNode<T> {
593 PerNode(RefCell::default())
594 }
595
596 pub(crate) fn of(&self, id: NodeId, read: impl FnOnce() -> T) -> T {
597 if let Some(held) = self.0.borrow().get(&id) {
598 return held.clone();
599 }
600 let found = read();
601 self.0.borrow_mut().insert(id, found.clone());
602 found
603 }
604
605 pub(crate) fn try_of<E>(
607 &self,
608 id: NodeId,
609 read: impl FnOnce() -> Result<T, E>,
610 ) -> Result<T, E> {
611 if let Some(held) = self.0.borrow().get(&id) {
612 return Ok(held.clone());
613 }
614 let found = read()?;
615 self.0.borrow_mut().insert(id, found.clone());
616 Ok(found)
617 }
618}
619
620pub(crate) fn written_form(typing: &Typing, id: NodeId) -> &Body {
622 match typing.value(id) {
623 Value::ClosedForm(form) => &form.body,
624 _ => unreachable!("a ref read through names a closed form"),
625 }
626}
627
628pub(crate) fn read_through<R>(typing: &Typing, with: impl FnOnce(&Through) -> R) -> R {
630 let written = |id: NodeId| match typing.value(id) {
631 Value::ClosedForm(form) => Some((&form.body, form.origin)),
632 _ => None,
633 };
634 with(&Through::new(&written, typing.folds().written()))
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 fn typed(name: &str, files: &[(&str, String)], root: &str) -> Typing {
642 let dir = std::env::temp_dir().join(format!("sva-refs-{name}-{}", std::process::id()));
643 let _ = std::fs::remove_dir_all(&dir);
644 std::fs::create_dir_all(&dir).expect("a directory");
645 for (file, body) in files {
646 std::fs::write(dir.join(file), body).expect("a node file");
647 }
648 let graph = sva_ast::parse_composition(&dir).expect("a composition");
649 crate::types(&graph, root).expect("typed")
650 }
651
652 fn composings(tys: &Typing) -> usize {
653 tys.folds().composings.get()
654 }
655
656 #[test]
659 fn a_chain_composes_each_node_once() {
660 let names: Vec<String> = (0..=8).map(|k| format!("n{k}")).collect();
661 let mut files = vec![(names[0].as_str(), "sin(2*pi*220*t)\n".to_string())];
662 for pair in names.windows(2) {
663 let p = format!("@{}(t)", pair[0]);
664 files.push((&pair[1], format!("{p}*0.5 + {p}*0.3 + {p}*0.2\n")));
665 }
666 let tys = typed("chain", &files, "n8");
667 let root = tys.id("n8").expect("the root");
668 spectral_sum_of(&tys, root, Var::T).expect("a sum");
669 assert_eq!(composings(&tys), 9);
670 spectral_sum_of(&tys, root, Var::T).expect("a sum");
671 assert_eq!(composings(&tys), 9);
672 }
673
674 #[test]
677 fn a_refusal_a_chain_meets_is_composed_once_per_node() {
678 let names: Vec<String> = (0..=20).map(|k| format!("n{k}")).collect();
679 let tone = "crop(sin(2*pi*220*t), 0s, 0.1s)\n".to_string();
680 let mut files = vec![(names[0].as_str(), tone)];
681 for pair in names.windows(2) {
682 files.push((&pair[1], format!("lowpass(@{}(t), 1000)\n", pair[0])));
683 }
684 let tys = typed("refused", &files, "n20");
685 let at = |k: usize| tys.id(&names[k]).expect("a level");
686 assert!(
687 spectral_sum_of(&tys, at(20), Var::T).is_err(),
688 "the poles refuse"
689 );
690 let once = composings(&tys);
691 for k in (0..=20).rev() {
692 let _ = spectral_sum_of(&tys, at(k), Var::T);
693 }
694 assert_eq!(composings(&tys), once, "a kept refusal composes nothing");
695 }
696
697 #[test]
700 fn an_edit_lets_go_of_the_folds_of_what_it_changed_alone() {
701 let dir = std::env::temp_dir().join(format!("sva-refs-narrow-{}", std::process::id()));
702 let _ = std::fs::remove_dir_all(&dir);
703 std::fs::create_dir_all(&dir).expect("a directory");
704 let files = [
705 ("a", "sin(2*pi*220*t)*0.5\n"),
706 ("b", "sin(2*pi*330*t)*0.5\n"),
707 ("mix", "@a(t) + @b(t)\n"),
708 ];
709 for (file, body) in files {
710 std::fs::write(dir.join(file), body).expect("a node file");
711 }
712 let graph = sva_ast::parse_composition(&dir).expect("a composition");
713 let held = crate::render::prepared(&graph, "mix", 8_000).expect("typed");
714 let (inst, mut tys) = (held.instances, held.tys);
715 let a = tys.id("a").expect("a");
716 spectral_sum_of(&tys, a, Var::T).expect("a sum");
717 let before = composings(&tys);
718 let changed = [vec!["b".to_string()], vec!["mix".to_string()]];
719 tys.lower(&inst, &changed).expect("lowered anew");
720 tys.commit(&inst);
721 spectral_sum_of(&tys, a, Var::T).expect("a sum");
722 assert_eq!(composings(&tys), before, "`a` composes from what it kept");
723 let mix = tys.id("mix").expect("mix");
724 spectral_sum_of(&tys, mix, Var::T).expect("a sum");
725 assert!(composings(&tys) > before, "what changed composes anew");
726 let _ = std::fs::remove_dir_all(&dir);
727 }
728
729 #[test]
731 fn two_names_for_one_body_compose_once() {
732 let files = [
733 ("tone", "sin(2*pi*3*t)\n".to_string()),
734 ("a", "@tone(t)*0.5\n".to_string()),
735 ("b", "@tone(t)*0.5\n".to_string()),
736 ("mix", "@a(t) + @b(t)\n".to_string()),
737 ];
738 let tys = typed("twins", &files, "mix");
739 let [a, b] = ["a", "b"].map(|n| tys.id(n).expect("a node"));
740 assert_ne!(a, b);
741 assert_eq!(identity(&tys, a).ok(), identity(&tys, b).ok());
742 spectral_sum_of(&tys, tys.id("mix").expect("mix"), Var::T).expect("a sum");
743 assert_eq!(composings(&tys), 3);
744 }
745}