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 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#[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); 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];