1use std::fmt;
4
5use sva_ast::ByteSpan;
6use sva_formula::Refusal;
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum BindingFault {
11 Unbound(String, String),
12 Unused(String, String),
13 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 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#[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#[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 Refused(Box<Diagnostic>),
142 UnresolvedBars(Located),
144 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 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 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
219pub 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];