Skip to main content

sva_engine/
error.rs

1// Concern: declares every refusal the engine reaches and the diagnostic shape each is written in | Non-concern: sva-ast's parse-time codes, sva-formula's own type codes | IO: none
2
3use std::fmt;
4
5use sva_ast::ByteSpan;
6use sva_formula::Refusal;
7
8/// What went wrong binding one invocation's arguments to one file's free variables.
9#[derive(Clone, Debug, PartialEq)]
10pub enum BindingFault {
11    Unbound(String, String),
12    Unused(String, String),
13    /// A callee that reads no signal, and what its positional argument was written as.
14    UnusedPositional(String, String),
15    Reserved(String),
16    Duplicate(String),
17    SelfInArgument(String),
18    ShiftedRead(String, String),
19    TooManyInstances(usize),
20    DefaultNamesNoNumber(String, String),
21}
22
23impl BindingFault {
24    pub fn code(&self) -> &'static str {
25        match self {
26            BindingFault::Unbound(..) => "engine.unbound_variable",
27            BindingFault::Unused(..) | BindingFault::UnusedPositional(..) => {
28                "engine.unused_argument"
29            }
30            BindingFault::Reserved(_) => "engine.reserved_parameter",
31            BindingFault::Duplicate(_) => "engine.duplicate_argument",
32            BindingFault::SelfInArgument(_) => "engine.self_in_argument",
33            BindingFault::ShiftedRead(..) => "engine.shifted_parameter_read",
34            BindingFault::TooManyInstances(_) => "engine.too_many_instances",
35            // The code an agent branches on stays whatever it was first published as.
36            BindingFault::DefaultNamesNoNumber(..) => "engine.default_reads_buffer",
37        }
38    }
39}
40
41impl fmt::Display for BindingFault {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            BindingFault::Unbound(callee, v) => write!(
45                f,
46                "`{v}` is free in `{callee}` and this invocation binds no `{v}=`"
47            ),
48            BindingFault::Unused(callee, v) => {
49                write!(f, "`{v}=` binds nothing — `{callee}` leaves no `{v}` free")
50            }
51            BindingFault::UnusedPositional(callee, arg) => write!(
52                f,
53                "the positional argument `{arg}` binds nothing — `{callee}` reads no signal \
54                 passed to it; name each argument, or read it at a time as `@{callee}({arg}, ...)`"
55            ),
56            BindingFault::Reserved(v) => {
57                write!(
58                    f,
59                    "`{v}` is already bound by the language and cannot be a parameter"
60                )
61            }
62            BindingFault::Duplicate(v) => {
63                write!(f, "`{v}=` is given twice in one invocation")
64            }
65            BindingFault::SelfInArgument(v) => write!(
66                f,
67                "`{v}=` passes a `self(...)`, which would read the callee's own output, not \
68                 this node's; give it a file to reference instead"
69            ),
70            BindingFault::ShiftedRead(v, what) => write!(
71                f,
72                "`{v}` is read at a shifted time but its argument holds `{what}`, whose value \
73                 follows a time of its own rather than the `t` handed to it; pass a file"
74            ),
75            BindingFault::TooManyInstances(n) => {
76                write!(f, "this composition expands past {n} function invocations")
77            }
78            BindingFault::DefaultNamesNoNumber(file, what) => write!(
79                f,
80                "`{file}`'s default names `{what}`, which settles to no one number; a default \
81                 resolves with no invocation behind it, so a signal is what a caller passes"
82            ),
83        }
84    }
85}
86
87/// Which node a refusal points at, and where inside its file. A graph holds no source text,
88/// so a byte span is as far as the engine can place a subterm.
89#[derive(Clone, Debug, PartialEq, Eq, Default)]
90pub struct Located {
91    pub node: String,
92    pub span: Option<ByteSpan>,
93}
94
95impl Located {
96    pub fn at(node: impl Into<String>, span: Option<ByteSpan>) -> Located {
97        Located {
98            node: node.into(),
99            span,
100        }
101    }
102}
103
104impl fmt::Display for Located {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self.span {
107            Some(span) => write!(f, "{}:{}", self.node, span.start),
108            None => write!(f, "{}", self.node),
109        }
110    }
111}
112
113/// The three parts of FORMAT 7.1 and 8.3, which every engine refusal is written in.
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct Diagnostic {
116    pub code: String,
117    pub message: String,
118    pub location: Located,
119    pub help: String,
120}
121
122impl fmt::Display for Diagnostic {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{} ({}). {}", self.message, self.location, self.help)
125    }
126}
127
128#[derive(Clone, Debug, PartialEq)]
129pub enum EngineError {
130    Binding {
131        node: String,
132        span: Option<ByteSpan>,
133        fault: BindingFault,
134    },
135    UnknownNode(String),
136    /// A written instance no instance of its file is spelt as; the second names every one.
137    UnknownInstance(String, Vec<String>),
138    AmbiguousNode(String, Vec<String>),
139    RefAboveRoot(String, String),
140    UnknownBuiltin(String),
141    BadArity(String),
142    /// A type, cast or ref refusal, already written in the shape section 16 states.
143    Refused(Box<Diagnostic>),
144    /// A bar is a duration this graph never resolved; the tempo pass runs above the engine.
145    UnresolvedBars(Located),
146    /// Its caller let the render go before it ended.
147    Abandoned,
148}
149
150impl EngineError {
151    pub fn code(&self) -> &str {
152        match self {
153            EngineError::Binding { fault, .. } => fault.code(),
154            EngineError::UnknownNode(_) | EngineError::UnknownInstance(..) => "engine.unknown_node",
155            EngineError::AmbiguousNode(..) => "engine.ambiguous_node",
156            EngineError::RefAboveRoot(..) => "engine.ref_above_root",
157            EngineError::UnknownBuiltin(_) => "engine.unknown_builtin",
158            EngineError::BadArity(_) => "engine.bad_arity",
159            EngineError::Refused(d) => &d.code,
160            EngineError::UnresolvedBars(_) => "engine.unresolved_bar_literal",
161            EngineError::Abandoned => "render.abandoned",
162        }
163    }
164
165    /// Which node a refusal points at, where one is known.
166    pub fn at(&self) -> Option<&Located> {
167        match self {
168            EngineError::Refused(d) => Some(&d.location),
169            EngineError::UnresolvedBars(at) => Some(at),
170            _ => None,
171        }
172    }
173
174    pub fn refused(d: Diagnostic) -> EngineError {
175        EngineError::Refused(Box::new(d))
176    }
177
178    /// A closed form refusal already names its blocking subterm by `Origin`; the caller resolves that
179    /// to a node and a span before it reaches here.
180    pub fn of_closed_form(r: &Refusal, at: Located, help: impl Into<String>) -> EngineError {
181        EngineError::refused(Diagnostic {
182            code: r.code.as_str().to_string(),
183            message: r.message.clone(),
184            location: at,
185            help: help.into(),
186        })
187    }
188}
189
190impl fmt::Display for EngineError {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self {
193            EngineError::Binding { node, fault, .. } => write!(f, "`{node}`: {fault}"),
194            EngineError::UnknownNode(p) => write!(f, "no such node: `{p}`"),
195            EngineError::UnknownInstance(p, names) => write!(
196                f,
197                "no such node: `{p}`; this render's instances of its file are named {}, a \
198                 reading names one exactly as listed",
199                names
200                    .iter()
201                    .map(|n| format!("`@{n}`"))
202                    .collect::<Vec<_>>()
203                    .join(", ")
204            ),
205            EngineError::AmbiguousNode(file, names) => write!(
206                f,
207                "`{file}` is a file with {} instances; name one: {}",
208                names.len(),
209                names.join(", ")
210            ),
211            EngineError::RefAboveRoot(node, path) => {
212                write!(f, "`@{path}` in `{node}` walks above the composition root")
213            }
214            EngineError::UnknownBuiltin(n) => write!(
215                f,
216                "unresolved function `{n}` (no builtin or sugar implementation)"
217            ),
218            EngineError::BadArity(n) => write!(f, "wrong argument shape for `{n}`"),
219            EngineError::Refused(d) => write!(f, "{d}"),
220            EngineError::UnresolvedBars(at) => write!(
221                f,
222                "`{at}` holds a bar literal and this composition declares no bpm/meter"
223            ),
224            EngineError::Abandoned => write!(f, "the render was let go before it ended"),
225        }
226    }
227}
228
229impl std::error::Error for EngineError {}
230
231/// Every code a judgment in this engine can refuse under, with what trips it. The prefix
232/// says which judgment refused, per FORMAT 16.
233pub static REGISTRY: [(&str, &str); 101] = [
234    (
235        "type.no_overload",
236        "a builtin applied to operand types no row names",
237    ),
238    ("type.domain_mismatch", "a closed form in t meets one in f"),
239    (
240        "type.samples_in_closed_form",
241        "a closed form meets samples or frames",
242    ),
243    (
244        "type.samples_in_series",
245        "a sum to inf whose term is samples, which only a finite sum writes out",
246    ),
247    (
248        "engine.series_too_long",
249        "a finite sum written out a term at a time past the terms one lowering holds",
250    ),
251    (
252        "type.rate_conflict",
253        "two sampled operands at different rates",
254    ),
255    (
256        "type.frame_mismatch",
257        "two framed operands at different windows or hops",
258    ),
259    ("type.width_mismatch", "widths differ and neither is one"),
260    (
261        "type.filter_needs_a_dual",
262        "a filter needs a closed form with a dual, and this one has none",
263    ),
264    (
265        "type.non_affine_singular",
266        "delta or pv with a non-affine argument",
267    ),
268    (
269        "type.series_not_summable",
270        "a sum to inf whose dual does not converge",
271    ),
272    (
273        "type.self_gain_unbounded",
274        "a linear self whose loop gain does not settle",
275    ),
276    (
277        "type.discrete_self_at_time",
278        "a discrete loop reading its own past at an instant rather than by index",
279    ),
280    (
281        "type.join_over_self",
282        "a join operand reading its own loop whole, so no width holds the node",
283    ),
284    (
285        "type.stateful_warp",
286        "a node that holds state read at a time that moves",
287    ),
288    (
289        "type.bars_in_frequency",
290        "a bar unit inside a closed form in f",
291    ),
292    (
293        "type.non_integer_power",
294        "a real exponent over a base that is not a positive constant",
295    ),
296    (
297        "type.non_integer_index",
298        "an index that is no integer: a time, a fraction, or a ref",
299    ),
300    (
301        "type.index_outside_read",
302        "idx(...) anywhere but inside an index read",
303    ),
304    ("cast.left_algebra", "fourier or ifourier on a non-pair"),
305    (
306        "cast.samples_are_terminal",
307        "fourier or ifourier on samples or frames",
308    ),
309    ("cast.stft_needs_samples", "stft on anything but samples"),
310    ("cast.istft_needs_frames", "istft on anything but frames"),
311    ("cast.missing_window", "stft without window= or hop="),
312    (
313        "collapse.not_finite",
314        "a closed form past the largest double where it is read",
315    ),
316    (
317        "cast.window_off_the_lattice",
318        "an stft window or hop that is no whole number of steps of the rate in use",
319    ),
320    (
321        "collapse.empty_band",
322        "every line sits at or above the ceiling",
323    ),
324    (
325        "collapse.singular_in_ct",
326        "a delta or pv atom reaches point sampling",
327    ),
328    (
329        "collapse.not_evaluable",
330        "a subterm with no value at a point",
331    ),
332    (
333        "collapse.series_nesting",
334        "series nested past what one instant expands",
335    ),
336    (
337        "collapse.no_block_row",
338        "a closed form in f read block by block, which one transform over an extent places",
339    ),
340    (
341        "samples.zero_delay_loop",
342        "a sampled loop reads no written sample",
343    ),
344    (
345        "samples.hop_outside_cola",
346        "an stft hop the window does not overlap-add across",
347    ),
348    (
349        "samples.window_not_power_of_two",
350        "an stft window that is not a power of two",
351    ),
352    (
353        "samples.channel_out_of_range",
354        "a component past the width of the value it was asked of",
355    ),
356    (
357        "samples.own_width",
358        "a node's program writing another width than its samples hold, an engine defect",
359    ),
360    (
361        "samples.grid_too_large",
362        "a finite-difference grid past what one call may hold at this rate",
363    ),
364    (
365        "samples.string_past_rate",
366        "a string whose first two partials no stable grid rings at this rate",
367    ),
368    (
369        "samples.bridge_unstable",
370        "a unison whose bridge leaves its scheme not provably stable at this rate",
371    ),
372    (
373        "samples.contact_unsettled",
374        "a contact force whose solve reached no value at some sample",
375    ),
376    (
377        "samples.argument_out_of_range",
378        "a solver's varying parameter outside its range at some sample",
379    ),
380    (
381        "engine.moving_energy_parameter",
382        "a stored-energy coefficient that moves between its jumps",
383    ),
384    (
385        "grammar.unknown_name",
386        "an identifier that is no builtin, unit or note name",
387    ),
388    ("grammar.arity", "a call with the wrong argument count"),
389    (
390        "grammar.unknown_named_argument",
391        "a named argument the builtin does not read",
392    ),
393    (
394        "engine.infinite_value",
395        "inf where it names no number: inf - inf, 0*inf, or in a term that moves",
396    ),
397    (
398        "engine.unknown_node",
399        "a target no node of this composition answers for",
400    ),
401    (
402        "engine.ambiguous_node",
403        "a path more than one node answers for",
404    ),
405    (
406        "engine.ref_above_root",
407        "a ref reaching outside the composition",
408    ),
409    ("engine.unknown_builtin", "a call no builtin table names"),
410    (
411        "engine.physics_out_of_range",
412        "a finite-difference argument outside the range its model admits",
413    ),
414    (
415        "engine.bad_arity",
416        "a builtin whose own lowering wanted other arguments",
417    ),
418    (
419        "engine.unresolved_bar_literal",
420        "a bar literal with no bpm and meter behind it",
421    ),
422    (
423        "engine.unbound_variable",
424        "a body reading a name no invocation bound",
425    ),
426    ("engine.unused_argument", "an argument the body never reads"),
427    (
428        "engine.reserved_parameter",
429        "a parameter named t, f, pi, i or self",
430    ),
431    ("engine.duplicate_argument", "one argument given twice"),
432    ("engine.self_in_argument", "self written inside an argument"),
433    (
434        "engine.shifted_parameter_read",
435        "a parameter read at a shifted time",
436    ),
437    (
438        "engine.too_many_instances",
439        "more argument tuples than one node may hold",
440    ),
441    (
442        "engine.default_reads_buffer",
443        "a default naming what settles to no one number",
444    ),
445    (
446        "engine.cyclic_substitution",
447        "a closed form reaching itself around a loop of refs",
448    ),
449    (
450        "read.no_spectral_sum",
451        "an exact reading of a term no atom sum reaches",
452    ),
453    (
454        "read.series_not_enumerable",
455        "a line reading of a series whose term is no line",
456    ),
457    (
458        "read.lines_need_unwindowed_lines",
459        "a line reading of a term a window gave a width",
460    ),
461    (
462        "read.no_partial_at_instant",
463        "a harmonic reading of a term with no partial at an instant: an impulse, or a pole there",
464    ),
465    (
466        "read.too_many_partial_pairs",
467        "a harmonic reading of a frame whose partials over the floor pair past the cap",
468    ),
469    (
470        "read.window_under_two_periods",
471        "a sliding-window reading's window under twice the shortest period it reads",
472    ),
473    (
474        "read.target_under_window",
475        "a sliding-window reading of a target shorter than its window",
476    ),
477    (
478        "read.rate_under_bands",
479        "a band-envelope reading of samples at a rate that places no ERB band",
480    ),
481    (
482        "read.too_many_cells",
483        "a self-similarity matrix whose cells over the target pass the cap",
484    ),
485    (
486        "read.kernel_past_cap",
487        "a self-similarity novelty kernel wider a side than the cells a matrix holds",
488    ),
489    (
490        "read.cell_under_band_step",
491        "a self-similarity cell holding no sample of the band envelopes",
492    ),
493    (
494        "read.target_under_frame",
495        "an alias score of a target shorter than the one frame it reads",
496    ),
497    (
498        "read.frame_under_order",
499        "a formant frame holding fewer samples than its all-pole fit's lags",
500    ),
501    (
502        "read.target_under_order",
503        "a formant reading of a target holding fewer samples than its all-pole fit's lags",
504    ),
505    (
506        "read.rate_under_weighting",
507        "a loudness of samples at a rate whose Nyquist the K-weighting shelf stands at or past",
508    ),
509    (
510        "engine.bad_crop_shoulder",
511        "a crop shoulder outside the window it belongs to",
512    ),
513    (
514        "engine.filter_without_response",
515        "a filter whose q or gain leaves its response no finite pole, zero or gain",
516    ),
517    (
518        "engine.non_constant_argument",
519        "a number-valued argument that moves",
520    ),
521    (
522        "engine.reads_ahead",
523        "a stream reading a sample its source has not computed yet",
524    ),
525    (
526        "engine.unreadable_position",
527        "an exactly computed time past what its integers hold",
528    ),
529    (
530        "engine.per_lane_read_on_samples",
531        "a buffer read at one time per component",
532    ),
533    (
534        "engine.forward_self_read",
535        "a self-reference reaching forward in time",
536    ),
537    (
538        "engine.series_body_not_inlinable",
539        "a closed loop whose body holds a value no term carries",
540    ),
541    (
542        "engine.unshadered_operation",
543        "an operation the shader builder has no arm for",
544    ),
545    (
546        "engine.not_materialized",
547        "a reading off samples this render never held",
548    ),
549    (
550        "engine.observation_needs_samples",
551        "a sample-consuming reading asked of a closed form",
552    ),
553    (
554        "engine.observation_needs_a_graph",
555        "a reading asked of a buffer with no node behind it",
556    ),
557    (
558        "engine.observation_not_wired",
559        "a reading no buffer answers",
560    ),
561    (
562        "engine.alias_needs_a_closed_form",
563        "an alias score asked of samples with no closed form behind them",
564    ),
565    (
566        "engine.no_stream",
567        "a stream over a node no block reads alone, or a target it cannot open",
568    ),
569    (
570        "engine.stream_behind",
571        "a stream read from a sample before where it stands",
572    ),
573    ("engine.empty_read", "a stream read of no samples"),
574    (
575        "engine.stream_width",
576        "a stream, or an edit of one, wider than the channels it plays",
577    ),
578    (
579        "engine.unbounded_extent",
580        "a node read whole whose input never ends",
581    ),
582    (
583        "engine.unwritten_series",
584        "a sum written out one node per index whose bounds name no whole number in this instance",
585    ),
586    (
587        "engine.unbounded_read",
588        "a value asked for every sample of a support that never starts or ends",
589    ),
590    (
591        "render.no_end",
592        "an interval with no end over a root whose support never ends",
593    ),
594    (
595        "render.unrepresentable_sample",
596        "a sample past what the output's float or integer PCM encoding holds",
597    ),
598    (
599        "render.abandoned",
600        "a render its caller let go before it ended",
601    ),
602];