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 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 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#[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#[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 Refused(Box<Diagnostic>),
133 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 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 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
206pub static REGISTRY: [(&str, &str); 82] = [
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.stream_behind",
482 "a stream read from a sample before where it stands",
483 ),
484 ("engine.empty_read", "a stream read of no samples"),
485 (
486 "engine.stream_width",
487 "a stream, or an edit of one, wider than the channels it plays",
488 ),
489 (
490 "engine.unbounded_extent",
491 "a node read whole whose input never ends",
492 ),
493 (
494 "render.no_end",
495 "an interval with no end over a root whose support never ends",
496 ),
497 (
498 "render.unrepresentable_sample",
499 "a sample past what the output's float or integer PCM encoding holds",
500 ),
501];