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    AmbiguousNode(String, Vec<String>),
137    RefAboveRoot(String, String),
138    UnknownBuiltin(String),
139    BadArity(String),
140    /// A type, cast or ref refusal, already written in the shape section 16 states.
141    Refused(Box<Diagnostic>),
142    /// A bar is a duration this graph never resolved; the tempo pass runs above the engine.
143    UnresolvedBars(Located),
144    /// Its caller let the render go before it ended.
145    Abandoned,
146}
147
148impl EngineError {
149    pub fn code(&self) -> &str {
150        match self {
151            EngineError::Binding { fault, .. } => fault.code(),
152            EngineError::UnknownNode(_) => "engine.unknown_node",
153            EngineError::AmbiguousNode(..) => "engine.ambiguous_node",
154            EngineError::RefAboveRoot(..) => "engine.ref_above_root",
155            EngineError::UnknownBuiltin(_) => "engine.unknown_builtin",
156            EngineError::BadArity(_) => "engine.bad_arity",
157            EngineError::Refused(d) => &d.code,
158            EngineError::UnresolvedBars(_) => "engine.unresolved_bar_literal",
159            EngineError::Abandoned => "render.abandoned",
160        }
161    }
162
163    /// Which node a refusal points at, where one is known.
164    pub fn at(&self) -> Option<&Located> {
165        match self {
166            EngineError::Refused(d) => Some(&d.location),
167            EngineError::UnresolvedBars(at) => Some(at),
168            _ => None,
169        }
170    }
171
172    pub fn refused(d: Diagnostic) -> EngineError {
173        EngineError::Refused(Box::new(d))
174    }
175
176    /// A closed form refusal already names its blocking subterm by `Origin`; the caller resolves that
177    /// to a node and a span before it reaches here.
178    pub fn of_closed_form(r: &Refusal, at: Located, help: impl Into<String>) -> EngineError {
179        EngineError::refused(Diagnostic {
180            code: r.code.as_str().to_string(),
181            message: r.message.clone(),
182            location: at,
183            help: help.into(),
184        })
185    }
186}
187
188impl fmt::Display for EngineError {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            EngineError::Binding { node, fault, .. } => write!(f, "`{node}`: {fault}"),
192            EngineError::UnknownNode(p) => write!(f, "no such node: `{p}`"),
193            EngineError::AmbiguousNode(file, names) => write!(
194                f,
195                "`{file}` is a file with {} instances; name one: {}",
196                names.len(),
197                names.join(", ")
198            ),
199            EngineError::RefAboveRoot(node, path) => {
200                write!(f, "`@{path}` in `{node}` walks above the composition root")
201            }
202            EngineError::UnknownBuiltin(n) => write!(
203                f,
204                "unresolved function `{n}` (no builtin or sugar implementation)"
205            ),
206            EngineError::BadArity(n) => write!(f, "wrong argument shape for `{n}`"),
207            EngineError::Refused(d) => write!(f, "{d}"),
208            EngineError::UnresolvedBars(at) => write!(
209                f,
210                "`{at}` holds a bar literal and this composition declares no bpm/meter"
211            ),
212            EngineError::Abandoned => write!(f, "the render was let go before it ended"),
213        }
214    }
215}
216
217impl std::error::Error for EngineError {}
218
219/// Every code a judgment in this engine can refuse under, with what trips it. The prefix
220/// says which judgment refused, per FORMAT 16.
221pub static REGISTRY: [(&str, &str); 86] = [
222    (
223        "type.no_overload",
224        "a builtin applied to operand types no row names",
225    ),
226    ("type.domain_mismatch", "a closed form in t meets one in f"),
227    (
228        "type.samples_in_closed_form",
229        "a closed form meets samples or frames",
230    ),
231    (
232        "type.samples_in_series",
233        "a sum to inf whose term is samples, which only a finite sum writes out",
234    ),
235    (
236        "engine.series_too_long",
237        "a finite sum written out a term at a time past the terms one lowering holds",
238    ),
239    (
240        "type.rate_conflict",
241        "two sampled operands at different rates",
242    ),
243    (
244        "type.frame_mismatch",
245        "two framed operands at different windows or hops",
246    ),
247    ("type.width_mismatch", "widths differ and neither is one"),
248    (
249        "type.filter_needs_a_dual",
250        "a filter needs a closed form with a dual, and this one has none",
251    ),
252    (
253        "type.non_affine_singular",
254        "delta or pv with a non-affine argument",
255    ),
256    (
257        "type.series_not_summable",
258        "a sum to inf whose dual does not converge",
259    ),
260    (
261        "type.self_gain_unbounded",
262        "a linear self whose loop gain does not settle",
263    ),
264    (
265        "type.discrete_self_at_time",
266        "a discrete loop reading its own past at an instant rather than by index",
267    ),
268    (
269        "type.stateful_warp",
270        "a node that holds state read at a time that moves",
271    ),
272    (
273        "type.bars_in_frequency",
274        "a bar unit inside a closed form in f",
275    ),
276    (
277        "type.non_integer_power",
278        "a real exponent over a base that is not a positive constant",
279    ),
280    (
281        "type.non_integer_index",
282        "an index that is no integer: a time, a fraction, or a ref",
283    ),
284    (
285        "type.index_outside_read",
286        "idx(...) anywhere but inside an index read",
287    ),
288    ("cast.left_algebra", "fourier or ifourier on a non-pair"),
289    (
290        "cast.samples_are_terminal",
291        "fourier or ifourier on samples or frames",
292    ),
293    ("cast.stft_needs_samples", "stft on anything but samples"),
294    ("cast.istft_needs_frames", "istft on anything but frames"),
295    ("cast.missing_window", "stft without window= or hop="),
296    (
297        "collapse.not_finite",
298        "a closed form past the largest double where it is read",
299    ),
300    (
301        "cast.window_off_the_lattice",
302        "an stft window or hop that is no whole number of steps of the rate in use",
303    ),
304    (
305        "collapse.empty_band",
306        "every line sits at or above the ceiling",
307    ),
308    (
309        "collapse.singular_in_ct",
310        "a delta or pv atom reaches point sampling",
311    ),
312    (
313        "collapse.not_evaluable",
314        "a subterm with no value at a point",
315    ),
316    (
317        "collapse.series_nesting",
318        "series nested past what one instant expands",
319    ),
320    (
321        "collapse.no_block_row",
322        "a closed form in f read block by block, which one transform over an extent places",
323    ),
324    (
325        "samples.zero_delay_loop",
326        "a sampled loop reads no written sample",
327    ),
328    (
329        "samples.hop_outside_cola",
330        "an stft hop the window does not overlap-add across",
331    ),
332    (
333        "samples.window_not_power_of_two",
334        "an stft window that is not a power of two",
335    ),
336    (
337        "samples.channel_out_of_range",
338        "a component past the width of the value it was asked of",
339    ),
340    (
341        "samples.grid_too_large",
342        "a finite-difference grid past what one call may hold at this rate",
343    ),
344    (
345        "samples.string_past_rate",
346        "a string whose first two partials no stable grid rings at this rate",
347    ),
348    (
349        "samples.bridge_unstable",
350        "a unison whose bridge leaves its scheme not provably stable at this rate",
351    ),
352    (
353        "samples.contact_unsettled",
354        "a contact force whose solve reached no value at some sample",
355    ),
356    (
357        "samples.argument_out_of_range",
358        "a solver's varying parameter outside its range at some sample",
359    ),
360    (
361        "engine.moving_energy_parameter",
362        "a stored-energy coefficient that moves between its jumps",
363    ),
364    (
365        "grammar.unknown_name",
366        "an identifier that is no builtin, unit or note name",
367    ),
368    ("grammar.arity", "a call with the wrong argument count"),
369    (
370        "grammar.unknown_named_argument",
371        "a named argument the builtin does not read",
372    ),
373    (
374        "engine.infinite_value",
375        "inf where it names no number: inf - inf, 0*inf, or in a term that moves",
376    ),
377    (
378        "engine.unknown_node",
379        "a target no node of this composition answers for",
380    ),
381    (
382        "engine.ambiguous_node",
383        "a path more than one node answers for",
384    ),
385    (
386        "engine.ref_above_root",
387        "a ref reaching outside the composition",
388    ),
389    ("engine.unknown_builtin", "a call no builtin table names"),
390    (
391        "engine.physics_out_of_range",
392        "a finite-difference argument outside the range its model admits",
393    ),
394    (
395        "engine.bad_arity",
396        "a builtin whose own lowering wanted other arguments",
397    ),
398    (
399        "engine.unresolved_bar_literal",
400        "a bar literal with no bpm and meter behind it",
401    ),
402    (
403        "engine.unbound_variable",
404        "a body reading a name no invocation bound",
405    ),
406    ("engine.unused_argument", "an argument the body never reads"),
407    (
408        "engine.reserved_parameter",
409        "a parameter named t, f, pi, i or self",
410    ),
411    ("engine.duplicate_argument", "one argument given twice"),
412    ("engine.self_in_argument", "self written inside an argument"),
413    (
414        "engine.shifted_parameter_read",
415        "a parameter read at a shifted time",
416    ),
417    (
418        "engine.too_many_instances",
419        "more argument tuples than one node may hold",
420    ),
421    (
422        "engine.default_reads_buffer",
423        "a default naming what settles to no one number",
424    ),
425    (
426        "engine.cyclic_substitution",
427        "a closed form reaching itself around a loop of refs",
428    ),
429    (
430        "read.no_spectral_sum",
431        "an exact reading of a term no atom sum reaches",
432    ),
433    (
434        "read.series_not_enumerable",
435        "a line reading of a series whose term is no line",
436    ),
437    (
438        "read.lines_need_unwindowed_lines",
439        "a line reading of a term a window gave a width",
440    ),
441    (
442        "engine.bad_crop_shoulder",
443        "a crop shoulder outside the window it belongs to",
444    ),
445    (
446        "engine.non_constant_argument",
447        "a number-valued argument that moves",
448    ),
449    (
450        "engine.reads_ahead",
451        "a stream reading a sample its source has not computed yet",
452    ),
453    (
454        "engine.unreadable_position",
455        "an exactly computed time past what its integers hold",
456    ),
457    (
458        "engine.per_lane_read_on_samples",
459        "a buffer read at one time per component",
460    ),
461    (
462        "engine.forward_self_read",
463        "a self-reference reaching forward in time",
464    ),
465    (
466        "engine.series_body_not_inlinable",
467        "a closed loop whose body holds a value no term carries",
468    ),
469    (
470        "engine.unshadered_operation",
471        "an operation the shader builder has no arm for",
472    ),
473    (
474        "engine.not_materialized",
475        "a reading off samples this render never held",
476    ),
477    (
478        "engine.observation_needs_samples",
479        "a sample-consuming reading asked of a closed form",
480    ),
481    (
482        "engine.observation_needs_a_graph",
483        "a reading asked of a buffer with no node behind it",
484    ),
485    (
486        "engine.observation_not_wired",
487        "a reading no buffer answers",
488    ),
489    (
490        "engine.alias_needs_a_closed_form",
491        "an alias score asked of samples with no closed form behind them",
492    ),
493    (
494        "engine.no_stream",
495        "a stream over a node no block reads alone, or a target it cannot open",
496    ),
497    (
498        "engine.stream_behind",
499        "a stream read from a sample before where it stands",
500    ),
501    ("engine.empty_read", "a stream read of no samples"),
502    (
503        "engine.stream_width",
504        "a stream, or an edit of one, wider than the channels it plays",
505    ),
506    (
507        "engine.unbounded_extent",
508        "a node read whole whose input never ends",
509    ),
510    (
511        "engine.unwritten_series",
512        "a sum written out one node per index whose bounds name no whole number in this instance",
513    ),
514    (
515        "engine.unbounded_read",
516        "a value asked for every sample of a support that never starts or ends",
517    ),
518    (
519        "render.no_end",
520        "an interval with no end over a root whose support never ends",
521    ),
522    (
523        "render.unrepresentable_sample",
524        "a sample past what the output's float or integer PCM encoding holds",
525    ),
526    (
527        "render.abandoned",
528        "a render its caller let go before it ended",
529    ),
530];