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