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); 66] = [
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.over_budget",
281        "a render counting more operations than the caller allowed",
282    ),
283    (
284        "samples.zero_delay_loop",
285        "a sampled loop reads no written sample",
286    ),
287    (
288        "samples.hop_outside_cola",
289        "an stft hop the window does not overlap-add across",
290    ),
291    (
292        "samples.window_not_power_of_two",
293        "an stft window that is not a power of two",
294    ),
295    (
296        "samples.channel_out_of_range",
297        "a component past the width of the value it was asked of",
298    ),
299    (
300        "samples.grid_too_large",
301        "a finite-difference grid past what one call may hold at this rate",
302    ),
303    (
304        "samples.string_past_rate",
305        "a string whose first two partials no stable grid rings at this rate",
306    ),
307    (
308        "grammar.unknown_name",
309        "an identifier that is no builtin, unit or note name",
310    ),
311    ("grammar.arity", "a call with the wrong argument count"),
312    (
313        "grammar.unknown_named_argument",
314        "a named argument the builtin does not read",
315    ),
316    ("grammar.inf_out_of_place", "inf outside a sum bound"),
317    (
318        "ref.fractional_shift_on_samples",
319        "a sampled read offset that is not a whole sample",
320    ),
321    (
322        "engine.unknown_node",
323        "a target no node of this composition answers for",
324    ),
325    (
326        "engine.ambiguous_node",
327        "a path more than one node answers for",
328    ),
329    (
330        "engine.ref_above_root",
331        "a ref reaching outside the composition",
332    ),
333    ("engine.unknown_builtin", "a call no builtin table names"),
334    (
335        "engine.physics_out_of_range",
336        "a finite-difference argument outside the range its model admits",
337    ),
338    (
339        "engine.bad_arity",
340        "a builtin whose own lowering wanted other arguments",
341    ),
342    (
343        "engine.unresolved_bar_literal",
344        "a bar literal with no bpm and meter behind it",
345    ),
346    (
347        "engine.unbound_variable",
348        "a body reading a name no invocation bound",
349    ),
350    ("engine.unused_argument", "an argument the body never reads"),
351    (
352        "engine.reserved_parameter",
353        "a parameter named t, f, pi, i or self",
354    ),
355    ("engine.duplicate_argument", "one argument given twice"),
356    ("engine.self_in_argument", "self written inside an argument"),
357    (
358        "engine.shifted_parameter_read",
359        "a parameter read at a shifted time",
360    ),
361    (
362        "engine.too_many_instances",
363        "more argument tuples than one node may hold",
364    ),
365    (
366        "engine.default_reads_buffer",
367        "a default naming what settles to no one number",
368    ),
369    (
370        "engine.cyclic_substitution",
371        "a closed form reaching itself around a loop of refs",
372    ),
373    (
374        "read.no_spectral_sum",
375        "an exact reading of a term no atom sum reaches",
376    ),
377    (
378        "read.series_not_enumerable",
379        "a line reading of a series whose term is no line",
380    ),
381    (
382        "read.lines_need_unwindowed_lines",
383        "a line reading of a term a window gave a width",
384    ),
385    (
386        "engine.bad_crop_shoulder",
387        "a crop shoulder outside the window it belongs to",
388    ),
389    (
390        "engine.non_constant_argument",
391        "a number-valued argument that moves",
392    ),
393    (
394        "engine.varying_delay",
395        "a delay with no whole-sample offset on the grid",
396    ),
397    (
398        "engine.unreadable_shift",
399        "a ref read at a time this engine cannot settle",
400    ),
401    (
402        "engine.per_lane_read_on_samples",
403        "a buffer read at one time per component",
404    ),
405    (
406        "engine.forward_self_read",
407        "a self-reference reaching forward in time",
408    ),
409    (
410        "engine.series_body_not_inlinable",
411        "a closed loop whose body holds a value no term carries",
412    ),
413    (
414        "engine.unshadered_operation",
415        "an operation the shader builder has no arm for",
416    ),
417    (
418        "engine.not_materialized",
419        "a reading off samples this render never held",
420    ),
421    (
422        "engine.observation_needs_samples",
423        "a sample-consuming reading asked of a closed form",
424    ),
425    (
426        "engine.observation_needs_a_graph",
427        "a reading asked of a buffer with no node behind it",
428    ),
429    (
430        "engine.observation_not_wired",
431        "a reading no buffer answers",
432    ),
433    (
434        "engine.alias_needs_a_closed_form",
435        "an alias score asked of samples with no closed form behind them",
436    ),
437];