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