1use sva_formula::spectral_sum::atom::{Singular, SpectralAtom};
4use sva_formula::{Line, SpectralSum, Var, d_dt, envelope_read, line_atoms_read};
5use sva_samples::{
6 AliasScore, Buffer, Consumes, Extent, Peak, PitchFrame, Profile, Source, measure::bands,
7 measure::crest, measure::envelope, measure::formants, measure::loudness, measure::onsets,
8 measure::pitch, measure::spectrum, measure::stereo, measure_alias,
9};
10
11use crate::error::{Diagnostic, EngineError, Located};
12use crate::query::{Answer, DEFAULT_FRAME_SECS, Output, Representation};
13use crate::refs;
14use crate::render::Render;
15use crate::typing::Value;
16
17pub fn answer(
20 render: &Render,
21 node: sva_formula::NodeId,
22 representation: Representation,
23) -> Result<Answer, EngineError> {
24 let closed = render.tys.ty(node).is_closed_form();
25 let profile = render.config.profile.name;
26 if representation == Representation::Flops {
27 return Ok(Answer::whole(
28 Output::Flops(Box::new(crate::flops::tree_at(render, node))),
29 Source::Exact,
30 profile,
31 None,
32 ));
33 }
34 if representation == Representation::Arguments {
35 return Ok(Answer::whole(
36 Output::Arguments(arguments_under(render, node)),
37 Source::Exact,
38 profile,
39 None,
40 ));
41 }
42 if representation == Representation::Bindings {
43 return Ok(Answer::whole(
44 Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
45 Source::Exact,
46 profile,
47 None,
48 ));
49 }
50 match representation.consumes(closed) {
51 Consumes::ClosedForm => {
52 let found;
53 let sum = match render.symbolic.get(&node) {
54 Some(held) => held,
55 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
56 Ok(held) => {
57 found = held;
58 &found
59 }
60 Err(left) => return measured_instead(render, node, representation, left),
61 },
62 };
63 let (value, listed, source) = match exact(render, node, representation, sum) {
64 Err(left) => return measured_instead(render, node, representation, left),
65 Ok(held) => held,
66 };
67 Ok(Answer {
68 value,
69 source,
70 profile,
71 rate: None,
72 tail_db: listed.tail_db(),
73 dropped: listed.dropped,
74 })
75 }
76 _ => {
77 if render.buffer(node).is_none() {
78 return Err(unmaterialized(render, node, representation));
79 }
80 let buffer = render.output(node)?;
81 Ok(Answer::whole(
82 measured(render, node, representation, &buffer)?,
83 Source::Measured,
84 profile,
85 Some(render.config.rate),
86 ))
87 }
88 }
89}
90
91fn measured_instead(
94 render: &Render,
95 node: sva_formula::NodeId,
96 representation: Representation,
97 left: EngineError,
98) -> Result<Answer, EngineError> {
99 match representation {
100 Representation::Envelope { frame_secs } if left.code() == "cast.left_algebra" => {
101 off_the_grid(render, node, frame_secs)
102 }
103 _ => Err(left),
104 }
105}
106
107pub(super) fn on_the_grid(
110 render: &Render,
111 node: sva_formula::NodeId,
112) -> Result<Buffer, EngineError> {
113 if render.buffer(node).is_some() {
114 return render.output(node);
115 }
116 let extent = render.range.ok_or_else(|| {
117 render.unranged.clone().unwrap_or_else(|| {
118 refused(
119 render,
120 node,
121 "engine.not_materialized",
122 "a measured envelope reads samples, and this render read none".to_string(),
123 )
124 })
125 })?;
126 collapsed_over(render, node, extent)
127}
128
129fn collapsed_over(
130 render: &Render,
131 node: sva_formula::NodeId,
132 extent: sva_samples::Extent,
133) -> Result<Buffer, EngineError> {
134 super::sampled(render, node, extent)
135}
136
137fn off_the_grid(
138 render: &Render,
139 node: sva_formula::NodeId,
140 frame_secs: Option<f64>,
141) -> Result<Answer, EngineError> {
142 let rate = render.config.rate;
143 let buffer = on_the_grid(render, node)?;
144 Ok(Answer::whole(
145 Output::Envelope(envelope::trace(
146 &buffer.slices(),
147 f64::from(rate),
148 buffer.origin_secs(),
149 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
150 )),
151 Source::Measured,
152 render.config.profile.name,
153 Some(rate),
154 ))
155}
156
157fn exact(
158 render: &Render,
159 node: sva_formula::NodeId,
160 representation: Representation,
161 sum: &SpectralSum,
162) -> Result<(Output, Listed, Source), EngineError> {
163 let source = Source::Exact;
164 let enumerated = || listed(render, node, sum, &render.config.profile);
165 let (value, listed) = match representation {
166 Representation::Lines => {
167 let listed = enumerated()?;
168 let held = lines(render, node, sum.var, &listed.atoms)?;
169 (Output::Lines(held), listed)
170 }
171 Representation::Spectrum {
172 frame_secs: Some(_),
173 ..
174 } => {
175 return Err(EngineError::refused(Diagnostic {
176 code: "engine.observation_needs_samples".to_string(),
177 message: "a closed form's spectrum is its lines, and it has no frames to \
178 take a spectrum across"
179 .to_string(),
180 location: Located::at(render.tys.name(node), None),
181 help: "drop `frame` for the lines, or read `sample(...)` of it to measure \
182 frames"
183 .to_string(),
184 }));
185 }
186 Representation::Spectrum {
188 frame_secs: None, ..
189 } => {
190 let listed = enumerated()?;
191 let mut held = lines(render, node, sum.var, &listed.atoms)?;
192 held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
193 (Output::Lines(held), listed)
194 }
195 Representation::Atoms => {
196 let listed = enumerated()?;
197 let found = listed.atoms.iter().map(sketch_atom).collect();
198 (Output::Atoms(found), listed)
199 }
200 Representation::Pitch { max_notes, .. } => {
202 let listed = enumerated()?;
203 let held = lines(render, node, sum.var, &listed.atoms)?;
204 let mut found: Vec<Peak> = held
205 .iter()
206 .filter(|l| l.hz > 0.0)
207 .map(|l| Peak {
208 hz: l.hz,
209 db: 20.0 * amplitude_at(&held, l.hz).log10(),
210 })
211 .collect();
212 found.sort_by(|a, b| b.db.total_cmp(&a.db));
213 let frame = PitchFrame {
214 t_secs: render
215 .range
216 .map_or(0.0, |range| range.start_secs(render.config.rate)),
217 notes: pitch::name_peaks(&found, max_notes),
218 };
219 (Output::Pitch(vec![frame]), listed)
220 }
221 Representation::Derivative => (Output::Symbolic(Box::new(d_dt(sum))), Listed::NONE),
222 Representation::Envelope { .. } => {
223 let held =
224 refs::read_through(&render.tys, |t| envelope_read(sum, t)).map_err(|left| {
225 EngineError::of_closed_form(
226 &left.refusal(),
227 render.tys.locate(left.origin),
228 "read the envelope off sample(...) for a measured one",
229 )
230 })?;
231 (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
232 }
233 other => return Err(not_a_closed_form(render, node, other)),
234 };
235 Ok((value, listed, source))
236}
237
238struct Listed {
241 atoms: Vec<SpectralAtom>,
242 dropped: Vec<Line>,
243}
244
245impl Listed {
246 const NONE: Listed = Listed {
248 atoms: Vec::new(),
249 dropped: Vec::new(),
250 };
251
252 fn tail_db(&self) -> Option<f64> {
255 let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
256 let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
257 let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
258 (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
259 }
260}
261
262fn listed(
265 render: &Render,
266 node: sva_formula::NodeId,
267 sum: &SpectralSum,
268 profile: &Profile,
269) -> Result<Listed, EngineError> {
270 let ceiling = profile.ceiling_hz;
271 let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
272 let mut held = Listed::NONE;
273 for lane in &sum.lanes {
274 held.atoms.extend(lane.clone().expanded().atoms);
275 for series in &lane.series {
276 let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
277 let Some(found) = found else {
278 return Err(unenumerable(render, node));
279 };
280 held.atoms.extend(found.atoms);
281 held.dropped.extend(found.dropped);
282 }
283 }
284 Ok(held)
285}
286
287fn lines(
290 render: &Render,
291 node: sva_formula::NodeId,
292 var: Var,
293 atoms: &[SpectralAtom],
294) -> Result<Vec<Line>, EngineError> {
295 atoms
296 .iter()
297 .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
298 .collect()
299}
300
301fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
302 match (var, a.sing) {
303 (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
304 (Var::T, Singular::Regular) => {
305 if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
306 return None;
307 }
308 match a.exp {
309 None => Some(Line::bare(0.0, a.c)),
311 Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
312 Some(_) => None,
313 }
314 }
315 _ => None,
316 }
317}
318
319fn widening(a: &SpectralAtom) -> Vec<&'static str> {
321 let mut held = Vec::new();
322 if !a.poly.is_one() {
323 held.push("a polynomial");
324 }
325 if a.exp.is_some_and(|e| e.sigma != 0.0) {
326 held.push("a decaying exponential");
327 }
328 if a.gauss.is_some() {
329 held.push("a Gaussian");
330 }
331 if a.pole.is_some() {
332 held.push("a pole");
333 }
334 if held.is_empty() {
335 held.push("a delta");
336 }
337 held
338}
339
340fn not_a_line(
342 render: &Render,
343 node: sva_formula::NodeId,
344 var: Var,
345 atom: &SpectralAtom,
346) -> EngineError {
347 let mut at = render.tys.locate(atom.origin);
348 if at.node.is_empty() {
349 at = Located::at(render.tys.name(node), None);
350 }
351 let message = match atom.ind {
352 _ if var == Var::F => "this term spreads over `f` rather than standing at one \
353 frequency, and a line in `f` is a delta"
354 .to_string(),
355 Some(window) => format!(
356 "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
357 with the window's transform, which has a width and is no line",
358 window.l.value(),
359 window.r.value()
360 ),
361 None => format!(
362 "this term carries {}, and a line is a bare turning exponential: each of those \
363 convolves the line with a shape of its own width",
364 widening(atom).join(", ")
365 ),
366 };
367 EngineError::refused(Diagnostic {
368 code: "read.lines_need_unwindowed_lines".to_string(),
369 message,
370 location: at,
371 help: "`atoms` states each term as it stands, `lines` of the node under the window \
372 or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
373 `sample(...)` of it measures it frame by frame"
374 .to_string(),
375 })
376}
377
378fn amplitude_at(held: &[Line], hz: f64) -> f64 {
381 held.iter()
382 .filter(|l| (l.hz - hz).abs() <= f64::EPSILON * hz.abs() || l.hz == -hz)
383 .map(|l| l.amp.abs())
384 .sum()
385}
386
387pub fn sketch_atom(a: &SpectralAtom) -> String {
389 let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
390 format!("{} x {}", a.c.abs(), factors.join(" times "))
391}
392
393fn measured(
394 render: &Render,
395 node: sva_formula::NodeId,
396 representation: Representation,
397 buffer: &Buffer,
398) -> Result<Output, EngineError> {
399 Ok(match representation {
400 Representation::Alias { oversample } => {
401 let reference = oversampled(render, behind(render, node)?, oversample)?;
402 Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
403 }
404 Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
405 other => {
406 return off_buffer(buffer, other).map_err(|fault| match fault {
407 NoReading::NeedsAGraph => not_a_reading(render, node, other),
408 NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
409 });
410 }
411 })
412}
413
414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub enum NoReading {
418 NeedsAGraph,
419 TooNarrow { need: usize, held: usize },
420}
421
422pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
424 let sr = f64::from(buffer.rate);
425 let start = buffer.origin_secs();
426 let plane = buffer.plane(0);
427 Ok(match representation {
428 Representation::Samples => Output::Samples(Box::new(buffer.clone())),
429 Representation::Spectrum {
430 max_peaks,
431 frame_secs,
432 } => Output::Spectrum(Box::new(spectrum::analyze(
433 plane, sr, max_peaks, frame_secs,
434 ))),
435 Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
436 &buffer.slices(),
437 sr,
438 start,
439 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
440 )),
441 Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
442 Representation::Pitch {
443 max_notes,
444 frame_secs,
445 } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
446 Representation::Formants {
447 max_formants,
448 frame_secs,
449 } => Output::Formants(formants::track(
450 plane,
451 sr,
452 start,
453 frame_secs,
454 formants::default_order(sr),
455 max_formants,
456 )),
457 Representation::Stereo { frame_secs } => {
458 if buffer.width < 2 {
459 return Err(NoReading::TooNarrow {
460 need: 2,
461 held: buffer.width,
462 });
463 }
464 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
465 Output::Stereo(Box::new(stereo::analyze(
466 &planes,
467 buffer.width,
468 sr,
469 start,
470 frame_secs,
471 )))
472 }
473 Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
474 Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
475 Representation::Onsets => Output::Onsets(Box::new(onsets::detect(plane, sr, start))),
476 Representation::Loudness => {
477 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
478 Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
479 }
480 _ => return Err(NoReading::NeedsAGraph),
481 })
482}
483
484fn attributed(
489 render: &Render,
490 node: sva_formula::NodeId,
491 depth: usize,
492) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
493 let holds = |id: sva_formula::NodeId| render.buffers.contains_key(&id);
494 let edges = edges_under(render, node, depth, &holds)?;
495 let under: std::collections::BTreeSet<sva_formula::NodeId> = std::iter::once(node)
496 .chain(edges.iter().map(|(_, child)| *child))
497 .collect();
498 let mut deps = std::collections::BTreeMap::new();
499 let mut kinds = std::collections::BTreeMap::new();
500 for id in render.buffers.keys().filter(|id| under.contains(id)) {
501 let name = render.tys.name(*id).to_string();
502 let read = refs_read(render, *id, &holds)?
503 .into_iter()
504 .map(|op| render.tys.name(op).to_string())
505 .collect();
506 deps.insert(name.clone(), read);
507 kinds.insert(name, sva_samples::SignalKind::Audio);
508 }
509 let mut contributed_by = std::collections::BTreeMap::new();
510 for (parent, child) in &edges {
511 if !crate::render::slots::reads_held(render, *parent)? {
512 return Err(unmaterialized(
513 render,
514 node,
515 Representation::Ledger { depth },
516 ));
517 }
518 if let Some(held) = contributed(render, *parent, *child)? {
519 contributed_by.insert(render.tys.name(*child).to_string(), held);
520 }
521 }
522 let readers: std::collections::BTreeSet<_> = edges.iter().map(|(parent, _)| *parent).collect();
524 let mut buffers = std::collections::BTreeMap::new();
525 for id in render.buffers.keys().filter(|id| under.contains(id)) {
526 let name = render.tys.name(*id);
527 if *id == node || readers.contains(id) || !contributed_by.contains_key(name) {
528 buffers.insert(name.to_string(), own_samples(render, *id)?);
529 }
530 }
531 let len = render.range.map_or(0, |range| range.len());
532 Ok(sva_samples::measure::ledger::attribute(
533 &buffers,
534 &contributed_by,
535 &deps,
536 &kinds,
537 render.tys.name(node),
538 0..len,
539 depth,
540 ))
541}
542
543fn own_samples(render: &Render, id: sva_formula::NodeId) -> Result<Buffer, EngineError> {
545 let range = render.range.expect("a ledger reads a decided range");
546 match render.buffer(id) {
547 Some(_) => render.output(id),
548 None => collapsed_over(render, id, range),
549 }
550}
551
552fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
555 let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
556 match render.tys.value(id) {
557 Value::ClosedForm(form) => refs::nodes_in(&form.body),
558 Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
559 Value::Op { args, .. } => args.clone(),
560 Value::Filter {
561 x, cutoff, q, gain, ..
562 } => vec![*x, *cutoff, *q, *gain],
563 Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
564 Value::SelfAt { .. } | Value::Noise(_) | Value::Stored(_) => Vec::new(),
565 }
566 };
567 let mut names: Vec<&str> = Vec::new();
568 let mut seen = std::collections::BTreeSet::from([node]);
569 let mut level = vec![node];
570 while !level.is_empty() {
571 let mut next = Vec::new();
572 for at in level {
573 let name = render.tys.name(at);
574 if !names.contains(&name) {
575 names.push(name);
576 }
577 next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
578 }
579 level = next;
580 }
581 names
582 .into_iter()
583 .filter_map(|name| render.tys.arguments(name).cloned())
584 .collect()
585}
586
587fn edges_under(
589 render: &Render,
590 node: sva_formula::NodeId,
591 depth: usize,
592 holds: &dyn Fn(sva_formula::NodeId) -> bool,
593) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
594 let mut seen = std::collections::BTreeSet::from([node]);
595 let (mut level, mut out) = (vec![node], Vec::new());
596 for _ in 0..depth {
597 let mut next = Vec::new();
598 for parent in level {
599 for child in refs_read(render, parent, holds)? {
600 if seen.insert(child) {
601 out.push((parent, child));
602 next.push(child);
603 }
604 }
605 }
606 level = next;
607 }
608 Ok(out)
609}
610
611fn refs_read(
613 render: &Render,
614 node: sva_formula::NodeId,
615 holds: &dyn Fn(sva_formula::NodeId) -> bool,
616) -> Result<Vec<sva_formula::NodeId>, EngineError> {
617 crate::render::slots::refs_read(render, node, holds)
618}
619
620fn contributed(
624 render: &Render,
625 parent: sva_formula::NodeId,
626 child: sva_formula::NodeId,
627) -> Result<Option<Buffer>, EngineError> {
628 let range = render.range.expect("a ledger reads a decided range");
629 Ok(crate::render::slots::contributed(render, parent, child)?
630 .map(|held| held.over(range, held.extent())))
631}
632
633fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
636 if render.tys.ty(node).is_closed_form() {
637 return Ok(node);
638 }
639 match render.tys.value(node) {
640 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
641 _ => Err(refused(
642 render,
643 node,
644 "engine.alias_needs_a_closed_form",
645 "an alias score is a render against the same closed form oversampled, and this node \
646 is samples with no closed form behind it"
647 .to_string(),
648 )),
649 }
650}
651
652pub(super) fn alias_db(
655 render: &Render,
656 node: sva_formula::NodeId,
657 buffer: &Buffer,
658) -> Result<f64, EngineError> {
659 let oversample = sva_samples::ALIAS_OVERSAMPLE as u32;
660 let reference = oversampled(render, behind(render, node)?, oversample)?;
661 Ok(worst_alias(buffer, &reference, oversample).asr_db)
662}
663
664fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
666 debug_assert_eq!(
667 buffer.width, reference.width,
668 "the reference is the same form at another rate"
669 );
670 let sr = f64::from(buffer.rate);
671 let width = buffer.width.min(reference.width);
672 sva_samples::worst_alias((0..width).map(|c| {
673 measure_alias(
674 buffer.plane(c),
675 reference.plane(c),
676 oversample as usize,
677 sr,
678 buffer.origin_secs(),
679 )
680 }))
681 .expect("a buffer holds at least one component")
682}
683
684fn oversampled(
686 render: &Render,
687 node: sva_formula::NodeId,
688 oversample: u32,
689) -> Result<Buffer, EngineError> {
690 let rate = render.config.rate * oversample;
691 let range = render.range.expect("an alias score reads a decided range");
692 let k = i64::from(oversample);
693 let extent = Extent::new(range.start * k, range.end * k);
694 let taken = match refs::spectral_sum_of(&render.tys, node, Var::T) {
695 Ok(sum) => refs::read_through(&render.tys, |t| {
696 sva_samples::of_spectral_sum_read(
697 &sum,
698 (rate, extent),
699 &render.config.profile,
700 AliasScore::NotAsked,
701 t,
702 )
703 }),
704 Err(_) => return super::finer(render, node, oversample, extent),
705 };
706 taken.map(|(buffer, _)| buffer).map_err(|e| {
707 EngineError::refused(Diagnostic {
708 code: e.code().to_string(),
709 message: e.to_string(),
710 location: Located::at(render.tys.name(node), None),
711 help: "an alias score needs a closed form to oversample".to_string(),
712 })
713 })
714}
715
716fn difference(buffer: &Buffer) -> Buffer {
718 let step = f64::from(buffer.rate);
719 let planes = (0..buffer.width)
720 .map(|c| {
721 let plane = buffer.plane(c);
722 plane
723 .iter()
724 .enumerate()
725 .map(|(i, x)| match i {
726 0 => 0.0,
727 _ => (x - plane[i - 1]) * step,
728 })
729 .collect()
730 })
731 .collect();
732 let mut out = Buffer::of_planes(buffer.rate, planes);
733 out.start = buffer.start;
734 out
735}
736
737fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
738 EngineError::refused(Diagnostic {
739 code: code.to_string(),
740 message,
741 location: Located::at(render.tys.name(node), None),
742 help: "ask for a reading this representation answers".to_string(),
743 })
744}
745
746fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
747 refused(
748 render,
749 node,
750 "read.series_not_enumerable",
751 "a series whose term is no line lists no lines, and an empty list would read as \
752 a node with none"
753 .to_string(),
754 )
755}
756
757fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
758 refused(
759 render,
760 node,
761 "engine.observation_needs_samples",
762 format!(
763 "`{}` reads samples, and this node is a closed form",
764 r.name()
765 ),
766 )
767}
768
769fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
770 refused(
771 render,
772 node,
773 "engine.observation_not_wired",
774 format!("`{}` takes no reading off a buffer here", r.name()),
775 )
776}
777
778fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
779 refused(
780 render,
781 node,
782 "type.width_mismatch",
783 format!("this reading needs {need} components and the node holds {held}"),
784 )
785}
786
787pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
788 refused(
789 render,
790 node,
791 "engine.not_materialized",
792 "this render held no samples of it".to_string(),
793 )
794}
795
796fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
797 refused(
798 render,
799 node,
800 "engine.not_materialized",
801 format!("`{}` reads samples this render never held", r.name()),
802 )
803}
804
805pub fn answer_buffer(
808 name: &str,
809 buffer: &Buffer,
810 representation: Representation,
811 profile: &'static str,
812) -> Result<Answer, EngineError> {
813 let refused = |code: &str, message: String| {
814 EngineError::refused(Diagnostic {
815 code: code.to_string(),
816 message,
817 location: Located::at(name, None),
818 help: "ask for a reading a buffer answers on its own".to_string(),
819 })
820 };
821 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
822 NoReading::NeedsAGraph => refused(
823 "engine.observation_needs_a_graph",
824 format!(
825 "`{}` reads the tree a node was built from, and a file has none",
826 representation.name()
827 ),
828 ),
829 NoReading::TooNarrow { need, held } => refused(
830 "type.width_mismatch",
831 format!("this reading needs {need} components and the file holds {held}"),
832 ),
833 })?;
834 Ok(Answer::whole(
835 value,
836 Source::Measured,
837 profile,
838 Some(buffer.rate),
839 ))
840}