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