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                 depends on the sample being written rather than on that time; 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); 77] = [
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.bars_in_frequency",
245        "a bar unit inside a closed form in f",
246    ),
247    (
248        "type.non_integer_power",
249        "a real exponent over a base that is not a positive constant",
250    ),
251    ("cast.left_algebra", "fourier or ifourier on a non-pair"),
252    (
253        "cast.samples_are_terminal",
254        "fourier or ifourier on samples or frames",
255    ),
256    ("cast.stft_needs_samples", "stft on anything but samples"),
257    ("cast.istft_needs_frames", "istft on anything but frames"),
258    ("cast.missing_window", "stft without window= or hop="),
259    (
260        "collapse.no_horizon",
261        "a collapse with no window to run against",
262    ),
263    (
264        "collapse.empty_band",
265        "every line sits at or above the ceiling",
266    ),
267    (
268        "collapse.singular_in_ct",
269        "a delta or pv atom reaches point sampling",
270    ),
271    (
272        "collapse.not_evaluable",
273        "a subterm with no value at a point",
274    ),
275    (
276        "collapse.series_nesting",
277        "series nested past what one instant expands",
278    ),
279    (
280        "collapse.no_block_row",
281        "a closed form in f read span by span, which one transform over a horizon places",
282    ),
283    (
284        "collapse.over_budget",
285        "a render counting more operations than the caller allowed",
286    ),
287    (
288        "samples.zero_delay_loop",
289        "a sampled loop reads no written sample",
290    ),
291    (
292        "samples.hop_outside_cola",
293        "an stft hop the window does not overlap-add across",
294    ),
295    (
296        "samples.window_not_power_of_two",
297        "an stft window that is not a power of two",
298    ),
299    (
300        "samples.channel_out_of_range",
301        "a component past the width of the value it was asked of",
302    ),
303    (
304        "samples.grid_too_large",
305        "a finite-difference grid past what one call may hold at this rate",
306    ),
307    (
308        "samples.string_past_rate",
309        "a string whose first two partials no stable grid rings at this rate",
310    ),
311    (
312        "samples.bridge_unstable",
313        "a unison whose bridge leaves its scheme not provably stable at this rate",
314    ),
315    (
316        "samples.contact_unsettled",
317        "a contact force whose solve reached no value at some sample",
318    ),
319    (
320        "samples.state_mismatch",
321        "a held state handed to call sites other than the ones it was held for",
322    ),
323    (
324        "grammar.unknown_name",
325        "an identifier that is no builtin, unit or note name",
326    ),
327    ("grammar.arity", "a call with the wrong argument count"),
328    (
329        "grammar.unknown_named_argument",
330        "a named argument the builtin does not read",
331    ),
332    ("grammar.inf_out_of_place", "inf outside a sum bound"),
333    (
334        "ref.fractional_shift_on_samples",
335        "a sampled read offset that is not a whole sample",
336    ),
337    (
338        "engine.unknown_node",
339        "a target no node of this composition answers for",
340    ),
341    (
342        "engine.ambiguous_node",
343        "a path more than one node answers for",
344    ),
345    (
346        "engine.ref_above_root",
347        "a ref reaching outside the composition",
348    ),
349    ("engine.unknown_builtin", "a call no builtin table names"),
350    (
351        "engine.physics_out_of_range",
352        "a finite-difference argument outside the range its model admits",
353    ),
354    (
355        "engine.bad_arity",
356        "a builtin whose own lowering wanted other arguments",
357    ),
358    (
359        "engine.unresolved_bar_literal",
360        "a bar literal with no bpm and meter behind it",
361    ),
362    (
363        "engine.unbound_variable",
364        "a body reading a name no invocation bound",
365    ),
366    ("engine.unused_argument", "an argument the body never reads"),
367    (
368        "engine.reserved_parameter",
369        "a parameter named t, f, pi, i or self",
370    ),
371    ("engine.duplicate_argument", "one argument given twice"),
372    ("engine.self_in_argument", "self written inside an argument"),
373    (
374        "engine.shifted_parameter_read",
375        "a parameter read at a shifted time",
376    ),
377    (
378        "engine.too_many_instances",
379        "more argument tuples than one node may hold",
380    ),
381    (
382        "engine.default_reads_buffer",
383        "a default naming what settles to no one number",
384    ),
385    (
386        "engine.cyclic_substitution",
387        "a closed form reaching itself around a loop of refs",
388    ),
389    (
390        "read.no_spectral_sum",
391        "an exact reading of a term no atom sum reaches",
392    ),
393    (
394        "read.series_not_enumerable",
395        "a line reading of a series whose term is no line",
396    ),
397    (
398        "read.lines_need_unwindowed_lines",
399        "a line reading of a term a window gave a width",
400    ),
401    (
402        "engine.bad_crop_shoulder",
403        "a crop shoulder outside the window it belongs to",
404    ),
405    (
406        "engine.non_constant_argument",
407        "a number-valued argument that moves",
408    ),
409    (
410        "engine.varying_delay",
411        "a delay with no whole-sample offset on the grid",
412    ),
413    (
414        "engine.unreadable_shift",
415        "a ref read at a time this engine cannot settle",
416    ),
417    (
418        "engine.per_lane_read_on_samples",
419        "a buffer read at one time per component",
420    ),
421    (
422        "engine.forward_self_read",
423        "a self-reference reaching forward in time",
424    ),
425    (
426        "engine.series_body_not_inlinable",
427        "a closed loop whose body holds a value no term carries",
428    ),
429    (
430        "engine.unshadered_operation",
431        "an operation the shader builder has no arm for",
432    ),
433    (
434        "engine.not_materialized",
435        "a reading off samples this render never held",
436    ),
437    (
438        "engine.observation_needs_samples",
439        "a sample-consuming reading asked of a closed form",
440    ),
441    (
442        "engine.observation_needs_a_graph",
443        "a reading asked of a buffer with no node behind it",
444    ),
445    (
446        "engine.observation_not_wired",
447        "a reading no buffer answers",
448    ),
449    (
450        "engine.alias_needs_a_closed_form",
451        "an alias score asked of samples with no closed form behind them",
452    ),
453    (
454        "engine.release_not_causal",
455        "a sample before key-up that would depend on release",
456    ),
457    (
458        "engine.no_stream",
459        "a stream over a node no block reads alone, or a target or binding it cannot open",
460    ),
461    (
462        "engine.binding_not_causal",
463        "a binding moved at a checkpoint where a sample before it could hear the move",
464    ),
465    (
466        "engine.checkpoint_mismatch",
467        "a checkpoint resumed on a stream or node other than the one it was taken of",
468    ),
469    (
470        "engine.never_silent",
471        "a render until silent of a node that holds a level forever",
472    ),
473    (
474        "engine.not_silent_by",
475        "a render until silent whose bound is not under the floor by the latest time",
476    ),
477    (
478        "engine.no_tail_bound",
479        "a render until silent through a node class no tail bound is derived for",
480    ),
481];