1use std::borrow::Cow;
4
5use sva_formula::closed_form::children;
6use sva_formula::spectral_sum::atom::Indicator;
7use sva_formula::spectral_sum::build::{multiply_lanes, sole_constant};
8use sva_formula::spectral_sum::image;
9use sva_formula::spectral_sum::merge::simplify;
10use sva_formula::{
11 Body, C64, ClosedForm, Lane, Left, NodeId, Part, SpectralSum, Var, dual, inverse,
12 normalize_closed_form,
13};
14
15use crate::cast::Cast;
16use crate::error::{Diagnostic, EngineError, Located};
17use crate::typing::{Typing, Value};
18
19mod identity;
20mod prefix;
21
22pub(crate) use identity::{formula_identity, identity_in};
23pub use identity::{identity, symbolic_hash};
24pub(crate) use prefix::switches;
25
26pub fn spectral_sum_of(
29 typing: &Typing,
30 node: NodeId,
31 want: Var,
32) -> Result<SpectralSum, EngineError> {
33 composed(typing, node, want, &mut Vec::new())
34}
35
36fn composed(
39 typing: &Typing,
40 node: NodeId,
41 want: Var,
42 open: &mut Vec<NodeId>,
43) -> Result<SpectralSum, EngineError> {
44 if open.contains(&node) {
45 return Err(cyclic(typing, node));
46 }
47 open.push(node);
48 let held = match typing.value(node) {
49 Value::ClosedForm(form) => {
50 let body = fold_constants(typing, &form.body);
51 compose(typing, &body, form.var, open)?
52 }
53 Value::Cast(Cast::Fourier, source) => {
54 let inner = composed(typing, *source, Var::T, open)?;
55 turn(typing, node, dual(&inner))?
56 }
57 Value::Cast(Cast::IFourier, source) => {
58 let inner = composed(typing, *source, Var::F, open)?;
59 turn(typing, node, inverse(&inner))?
60 }
61 Value::Op { name, .. } if typing.ty(node).is_closed_form() => {
62 return Err(across(typing, node, name));
63 }
64 _ => return Err(no_closed_form(typing, node)),
65 };
66 open.pop();
67 on_axis(typing, node, held, typing.var(node), want)
68}
69
70pub(crate) fn cyclic(typing: &Typing, node: NodeId) -> EngineError {
71 EngineError::refused(Diagnostic {
72 code: "engine.cyclic_substitution".to_string(),
73 message: format!(
74 "`{}` reads itself around a loop of refs.",
75 typing.name(node)
76 ),
77 location: Located::at(typing.name(node), None),
78 help: "write the loop with self(...), which the engine classifies".to_string(),
79 })
80}
81
82fn on_axis(
83 typing: &Typing,
84 node: NodeId,
85 held: SpectralSum,
86 axis: Var,
87 want: Var,
88) -> Result<SpectralSum, EngineError> {
89 if axis == want {
90 return Ok(held);
91 }
92 let turned = match want {
93 Var::F => dual(&held),
94 Var::T => inverse(&held),
95 };
96 turn(typing, node, turned)
97}
98
99fn turn(
100 typing: &Typing,
101 node: NodeId,
102 turned: Result<SpectralSum, Left>,
103) -> Result<SpectralSum, EngineError> {
104 turned.map_err(|left| {
105 EngineError::of_closed_form(
106 &left.refusal(),
107 typing.locate(left.origin),
108 format!(
109 "write `{}` inside sample(...) to leave A deliberately",
110 typing.name(node)
111 ),
112 )
113 })
114}
115
116pub(crate) fn across(typing: &Typing, node: NodeId, call: &str) -> EngineError {
119 no_spectral_sum(typing.name(node), call)
120}
121
122fn no_spectral_sum(node: &str, blocking: &str) -> EngineError {
125 EngineError::refused(Diagnostic {
126 code: "read.no_spectral_sum".to_string(),
127 message: format!(
128 "`{node}` has no spectral sum to read: `{blocking}` composes no value across a ref."
129 ),
130 location: Located::at(node, None),
131 help: "write the construct inside the node it reads, or read it off sample(...)"
132 .to_string(),
133 })
134}
135
136fn no_closed_form(typing: &Typing, node: NodeId) -> EngineError {
137 EngineError::refused(Diagnostic {
138 code: "type.samples_in_closed_form".to_string(),
139 message: format!(
140 "`{}` is samples; nothing returns from samples to a closed form",
141 typing.name(node)
142 ),
143 location: Located::at(typing.name(node), None),
144 help: "read it as samples, or build the closed form without it".to_string(),
145 })
146}
147
148pub(crate) fn fold_constants<'a>(typing: &Typing, f: &'a Body) -> Cow<'a, Body> {
151 let fold = &mut Folding::default();
152 match names_number(typing, f, fold) {
153 true => Cow::Owned(folded(typing, f, fold)),
154 false => Cow::Borrowed(f),
155 }
156}
157
158fn names_number(typing: &Typing, f: &Body, fold: &mut Folding) -> bool {
159 match f {
160 Body::Node(id) => number(typing, *id, fold).is_some(),
161 _ => children(f)
162 .into_iter()
163 .any(|p| names_number(typing, &p.body, fold)),
164 }
165}
166
167#[derive(Default)]
170struct Folding {
171 open: Vec<NodeId>,
172 cut: bool,
173}
174
175fn folded(typing: &Typing, f: &Body, fold: &mut Folding) -> Body {
176 let Body::Node(id) = f else {
177 return sva_formula::closed_form::map_children(f, |p| {
178 Part::new(p.origin, folded(typing, &p.body, fold))
179 });
180 };
181 match number(typing, *id, fold) {
182 Some(c) => Body::Const(c),
183 None => f.clone(),
184 }
185}
186
187fn number(typing: &Typing, node: NodeId, fold: &mut Folding) -> Option<C64> {
189 if let Some(held) = typing.folded_number(node) {
190 return held;
191 }
192 if fold.open.contains(&node) {
193 fold.cut = true;
194 return None;
195 }
196 let Value::ClosedForm(form) = typing.value(node) else {
197 return None;
198 };
199 if let Body::Const(c) = form.body {
200 return Some(c);
201 }
202 let outer = std::mem::take(&mut fold.cut);
203 fold.open.push(node);
204 let folded = names_number(typing, &form.body, fold).then(|| ClosedForm {
205 body: folded(typing, &form.body, fold),
206 ..*form
207 });
208 fold.open.pop();
209 let number = normalize_closed_form(folded.as_ref().unwrap_or(form))
210 .ok()
211 .and_then(|sum| sole_constant(&sum));
212 if !fold.cut {
213 typing.fold_number(node, number);
214 }
215 fold.cut |= outer;
216 number
217}
218
219fn compose(
222 typing: &Typing,
223 body: &Body,
224 var: Var,
225 open: &mut Vec<NodeId>,
226) -> Result<SpectralSum, EngineError> {
227 let here = *open
228 .last()
229 .expect("compose runs inside the node it composes");
230 if !holds_node(body) {
231 return normalize_here(typing, body, var);
232 }
233 match body {
234 Body::Node(id) => composed(typing, *id, var, open),
235 Body::Add(parts) => {
236 let mut lanes: Vec<Lane> = Vec::new();
237 for part in parts {
238 add_into(&mut lanes, compose(typing, &part.body, var, open)?);
239 }
240 Ok(sum(var, lanes))
241 }
242 Body::Mul(parts) => {
243 let mut acc: Option<SpectralSum> = None;
244 for part in parts {
245 let next = compose(typing, &part.body, var, open)?;
246 acc = Some(match acc {
247 None => next,
248 Some(held) => multiply(typing, &held, &next, var)?,
249 });
250 }
251 Ok(acc.unwrap_or_else(|| sum(var, Vec::new())))
252 }
253 Body::Shift { by, of } => {
254 let held = compose(typing, &of.body, var, open)?;
255 image::shift(held, *by).map_err(|left| left_of(typing, left))
256 }
257 Body::Crop {
258 of,
259 l,
260 r,
261 rise,
262 fall,
263 } if *rise > 0.0 || *fall > 0.0 => {
264 let held = compose(typing, &of.body, var, open)?;
265 let window = image::crop_window(*l, *r, *rise, *fall, of.origin, var);
266 multiply(typing, &held, &window, var)
267 }
268 Body::Crop { of, l, r, .. } => {
269 let held = compose(typing, &of.body, var, open)?;
270 image::crop(held, Indicator { l: *l, r: *r }).map_err(|left| left_of(typing, left))
271 }
272 Body::Div(num, den) => {
273 let over = compose(typing, &den.body, var, open)?;
274 let numerator = compose(typing, &num.body, var, open)?;
275 multiply(typing, &numerator, &reciprocal(typing, here, &over)?, var)
276 }
277 Body::Join(parts) => {
278 let mut lanes = Vec::new();
279 for part in parts {
280 lanes.extend(compose(typing, &part.body, var, open)?.lanes);
281 }
282 Ok(sum(var, lanes))
283 }
284 Body::Channel(of, k) => {
285 let held = compose(typing, &of.body, var, open)?;
286 match held.lanes.into_iter().nth(usize::from(*k)) {
287 Some(lane) => Ok(sum(var, vec![lane])),
288 None => Err(unsubstituted(typing, here, body)),
289 }
290 }
291 other => match inlined(typing, other, var, &mut open.clone()) {
293 Some(written) => normalize_here(typing, &written, var),
294 None => Err(unsubstituted(typing, here, other)),
295 },
296 }
297}
298
299fn reciprocal(
301 typing: &Typing,
302 node: NodeId,
303 over: &SpectralSum,
304) -> Result<SpectralSum, EngineError> {
305 let divided = || no_spectral_sum(typing.name(node), "a division by a closed form");
306 let [lane] = over.lanes.as_slice() else {
307 return Err(divided());
308 };
309 match lane.atoms.as_slice() {
310 [atom] if atom.is_bare() => Ok(SpectralSum::mono(
311 over.var,
312 vec![sva_formula::spectral_sum::atom::SpectralAtom::constant(
313 atom.c.inv(),
314 atom.origin,
315 )],
316 )),
317 _ => Err(divided()),
318 }
319}
320
321fn left_of(typing: &Typing, left: Left) -> EngineError {
322 EngineError::of_closed_form(
323 &left.refusal(),
324 typing.locate(left.origin),
325 "write the subterm inside sample(...) to leave A deliberately",
326 )
327}
328
329fn normalize_here(typing: &Typing, body: &Body, var: Var) -> Result<SpectralSum, EngineError> {
330 normalize_closed_form(&ClosedForm {
331 var,
332 body: body.clone(),
333 origin: sva_formula::Origin::UNKNOWN,
334 })
335 .map_err(|left| {
336 EngineError::of_closed_form(
337 &left.refusal(),
338 typing.locate(left.origin),
339 "write the subterm inside sample(...) to leave A deliberately",
340 )
341 })
342}
343
344fn unsubstituted(typing: &Typing, node: NodeId, body: &Body) -> EngineError {
347 no_spectral_sum(typing.name(node), named(body))
348}
349
350fn named(body: &Body) -> &'static str {
351 match body {
352 Body::Apply(op, _) => op.name(),
353 Body::Pow(..) => "pow",
354 Body::Fold(..) => "max, min or mod",
355 Body::Join(_) => "join",
356 Body::Channel(..) => "ch",
357 Body::Series(_) => "sum",
358 Body::Delta { .. } => "delta",
359 Body::Pv(_) => "pv",
360 Body::Deriv { .. } => "a derivative",
361 Body::Warp { .. } => "a warped time",
362 _ => "a construct",
363 }
364}
365
366pub fn nodes_in(f: &Body) -> Vec<NodeId> {
368 let mut out = Vec::new();
369 collect_nodes(f, &mut out);
370 out
371}
372
373fn collect_nodes(f: &Body, out: &mut Vec<NodeId>) {
374 if let Body::Node(id) = f {
375 if !out.contains(id) {
376 out.push(*id);
377 }
378 return;
379 }
380 for part in children(f) {
381 collect_nodes(&part.body, out);
382 }
383}
384
385fn holds_node(f: &Body) -> bool {
386 matches!(f, Body::Node(_)) || children(f).iter().any(|p| holds_node(&p.body))
387}
388
389fn sum(var: Var, mut lanes: Vec<Lane>) -> SpectralSum {
390 for lane in &mut lanes {
391 simplify(lane);
392 }
393 SpectralSum::of(var, lanes)
394}
395
396fn add_into(lanes: &mut Vec<Lane>, other: SpectralSum) {
398 if other.lanes.is_empty() {
399 return;
400 }
401 let width = lanes.len().max(other.lanes.len());
402 if lanes.len() == 1 {
403 let held = lanes[0].clone();
404 lanes.resize(width, held);
405 }
406 for at in 0..width {
407 let lane = lane_at(&other, at).clone();
408 match lanes.get_mut(at) {
409 Some(held) => {
410 held.atoms.extend(lane.atoms);
411 held.series.extend(lane.series);
412 held.modal.extend(lane.modal);
413 }
414 None => lanes.push(lane),
415 }
416 }
417}
418
419fn multiply(
420 typing: &Typing,
421 a: &SpectralSum,
422 b: &SpectralSum,
423 var: Var,
424) -> Result<SpectralSum, EngineError> {
425 let width = a.lanes.len().max(b.lanes.len());
426 let mut lanes = Vec::with_capacity(width);
427 for at in 0..width {
428 let held = multiply_lanes(lane_at(a, at).clone(), lane_at(b, at).clone());
429 lanes.push(held.map_err(|left| {
430 EngineError::of_closed_form(
431 &left.refusal(),
432 typing.locate(left.origin),
433 "write one of the factors inside sample(...)",
434 )
435 })?);
436 }
437 Ok(sum(var, lanes))
438}
439
440fn lane_at(n: &SpectralSum, at: usize) -> &Lane {
441 n.lanes.get(at).unwrap_or(&n.lanes[0])
442}
443
444pub fn substituted_closed_form(typing: &Typing, node: NodeId) -> Option<ClosedForm> {
446 let Value::ClosedForm(form) = typing.value(node) else {
447 return None;
448 };
449 Some(ClosedForm {
450 var: form.var,
451 body: substituted_body(typing, node, &form.body)?,
452 origin: form.origin,
453 })
454}
455
456pub(crate) fn substituted_body(typing: &Typing, node: NodeId, body: &Body) -> Option<Body> {
458 let Value::ClosedForm(form) = typing.value(node) else {
459 return None;
460 };
461 inlined(typing, body, form.var, &mut vec![node])
462}
463
464fn inlined(typing: &Typing, f: &Body, var: Var, open: &mut Vec<NodeId>) -> Option<Body> {
465 match f {
466 Body::Node(id) if open.contains(id) => None,
467 Body::Node(id) => match typing.value(*id) {
468 Value::ClosedForm(form) if form.var == var => {
469 open.push(*id);
470 let out = inlined(typing, &form.body, var, open);
471 open.pop();
472 out
473 }
474 _ => None,
475 },
476 other => {
477 let mut ok = true;
478 let out = sva_formula::closed_form::map_children(other, |p| {
479 match inlined(typing, &p.body, var, open) {
480 Some(body) => sva_formula::Part::new(p.origin, body),
481 None => {
482 ok = false;
483 p.clone()
484 }
485 }
486 });
487 ok.then_some(out)
488 }
489 }
490}