1use miette::Diagnostic;
2use relon_parser::TokenRange;
3use thiserror::Error;
4
5fn format_chain(chain: &[String]) -> String {
9 chain.join(" \u{2192} ")
10}
11
12#[derive(Error, Debug, Diagnostic, Clone)]
13pub enum RuntimeError {
14 #[error("Variable not found: {0}")]
15 #[diagnostic(
16 code(relon::eval::variable_not_found),
17 help("Check that the name is spelled correctly and is in scope at this point.")
18 )]
19 VariableNotFound(String, #[label("undefined")] TokenRange),
20
21 #[error("Type mismatch: expected {expected}, found {found}")]
22 #[diagnostic(code(relon::eval::type_mismatch))]
23 TypeMismatch {
24 expected: String,
25 found: String,
26 #[label("expected {expected}, got {found}")]
27 range: TokenRange,
28 },
29
30 #[error("Validation failed: {0}")]
31 #[diagnostic(code(relon::eval::validation_failed))]
32 ValidationError(String, #[label("validation failed here")] TokenRange),
33
34 #[error("Division by zero")]
35 #[diagnostic(
36 code(relon::eval::division_by_zero),
37 help("The right-hand operand of `/` or `%` evaluated to 0.")
38 )]
39 DivisionByZero(#[label("divisor is zero")] TokenRange),
40
41 #[error("Function not found: {0}")]
42 #[diagnostic(code(relon::eval::function_not_found))]
43 FunctionNotFound(String, #[label("called here")] TokenRange),
44
45 #[error("Circular reference detected: {}", format_chain(.cycle))]
46 #[diagnostic(
47 code(relon::eval::circular_reference),
48 help("Each entry depends on a later one in the cycle. Break the loop or replace one of the references with a literal value.")
49 )]
50 CircularReference {
51 cycle: Vec<String>,
53 #[label("triggers the cycle")]
54 range: TokenRange,
55 },
56
57 #[error("Unsupported operator {0:?}")]
58 #[diagnostic(code(relon::eval::unsupported_operator))]
59 UnsupportedOperator(String, #[label("not supported here")] TokenRange),
60
61 #[error("Invalid identifier: {0}")]
62 #[diagnostic(
63 code(relon::eval::invalid_identifier),
64 help("Function/decorator names must start with a letter or underscore and contain only alphanumeric characters or underscores.")
65 )]
66 InvalidIdentifier(String, #[label("invalid identifier")] TokenRange),
67
68 #[error("IO error: {0}")]
69 #[diagnostic(code(relon::eval::io_error))]
70 IoError(String),
71
72 #[error("Module not found at path: {0}")]
73 #[diagnostic(
74 code(relon::eval::module_not_found),
75 help("Check the path is relative to the importing file (or absolute) and that the file exists.")
76 )]
77 ModuleNotFound(String, #[label("import target missing")] miette::SourceSpan),
78
79 #[error("Parse error in module {path}: {message}")]
80 #[diagnostic(code(relon::eval::module_parse_error))]
81 ModuleParseError {
82 path: String,
83 message: String,
84 #[label("imported here")]
85 range: miette::SourceSpan,
86 },
87
88 #[error("Circular import detected: {}", format_chain(.0))]
89 #[diagnostic(
90 code(relon::eval::circular_import),
91 help("Two or more modules import each other. Restructure so the dependency is one-way.")
92 )]
93 CircularImport(
94 Vec<String>,
95 #[label("import that closes the cycle")] miette::SourceSpan,
96 ),
97
98 #[error("Numeric overflow")]
99 #[diagnostic(code(relon::eval::numeric_overflow))]
100 NumericOverflow(#[label("overflowed here")] TokenRange),
101
102 #[error("Step limit exceeded")]
106 #[diagnostic(
107 code(relon::eval::step_limit_exceeded),
108 help("The script ran longer than the configured `max_steps` / deadline budget. Raise `Capabilities::max_steps` or refactor recursive / iterative work.")
109 )]
110 StepLimitExceeded {
111 limit: Option<u64>,
115 #[label("budget exhausted here")]
116 range: TokenRange,
117 },
118
119 #[error("Recursion limit exceeded ({limit} levels)")]
120 #[diagnostic(
121 code(relon::eval::recursion_limit_exceeded),
122 help("A type-check or schema-validation pass nested deeper than the runtime's safety bound. Restructure the recursive type or value so it doesn't self-reference past this depth.")
123 )]
124 RecursionLimitExceeded {
125 limit: usize,
126 #[label("depth limit reached here")]
127 range: TokenRange,
128 },
129
130 #[error("Value too large: {actual} elements exceeds limit of {limit}")]
131 #[diagnostic(
132 code(relon::eval::value_too_large),
133 help("A list/tuple/dict grew past `Capabilities::max_value_elements`. Raise the limit or shrink the value.")
134 )]
135 ValueTooLarge {
136 limit: usize,
137 actual: usize,
138 #[label("constructed here")]
139 range: TokenRange,
140 },
141
142 #[error("Index out of bounds")]
149 #[diagnostic(
150 code(relon::eval::index_out_of_bounds),
151 help("Inspect the receiver's length before indexing, or clamp the offset / length arguments so the slice stays inside the value.")
152 )]
153 IndexOutOfBounds {
154 #[label("index walked past the receiver length")]
155 range: TokenRange,
156 },
157
158 #[error("Operation on empty list has no defined result")]
164 #[diagnostic(
165 code(relon::eval::empty_list),
166 help("Reducers like `list_int_max` need at least one element. Check the list isn't empty before calling, or supply an explicit fallback value.")
167 )]
168 EmptyList {
169 #[label("called on an empty list here")]
170 range: TokenRange,
171 },
172
173 #[error("Capability denied: {reason}")]
179 #[diagnostic(
180 code(relon::eval::capability_denied),
181 help("This Context is sandboxed. Grant the capability declared on the fn's gate (e.g. `caps.reads_fs = true`) to permit it.")
182 )]
183 CapabilityDenied {
184 cap_bit: Option<u32>,
189 reason: String,
193 #[label("call rejected by sandbox")]
194 range: TokenRange,
195 },
196
197 #[error("file has no `#main(...)` signature; cannot run as entry program")]
198 #[diagnostic(
199 code(relon::eval::no_main_signature),
200 help(
201 "Add `#main(Type arg, ...)` to declare the file as an entry program, or evaluate it as a static config via `eval_root` instead of `run_main`."
202 )
203 )]
204 NoMainSignature {
205 #[label("no #main here")]
206 range: TokenRange,
207 },
208
209 #[error("missing argument `{name}` for `#main(...)`")]
210 #[diagnostic(
211 code(relon::eval::missing_main_arg),
212 help("The host must push a value for every parameter declared by `#main(...)`.")
213 )]
214 MissingMainArg {
215 name: String,
216 #[label("expected here")]
217 range: TokenRange,
218 },
219
220 #[error("unexpected argument `{name}`: not declared by `#main(...)`")]
221 #[diagnostic(
222 code(relon::eval::unexpected_main_arg),
223 help("Only parameters listed in `#main(...)` may be pushed; remove the extra entry or add it to the signature.")
224 )]
225 UnexpectedMainArg {
226 name: String,
227 #[label("not in signature")]
228 range: TokenRange,
229 },
230
231 #[error("type mismatch for `#main` arg `{name}`: expected {expected}, found {found}")]
232 #[diagnostic(code(relon::eval::main_arg_type_mismatch))]
233 MainArgTypeMismatch {
234 name: String,
235 expected: String,
236 found: String,
237 #[label("type mismatch")]
238 range: TokenRange,
239 },
240
241 #[error("type mismatch for `#main` return value: expected {expected}, found {found}")]
242 #[diagnostic(code(relon::eval::main_return_type_mismatch))]
243 MainReturnTypeMismatch {
244 expected: String,
245 found: String,
246 #[label("declared here")]
247 range: TokenRange,
248 },
249
250 #[error("operation not supported by this backend: {reason}")]
258 #[diagnostic(
259 code(relon::eval::unsupported),
260 help("This backend lacks the runtime structures the operation needs. Switch to the tree-walking backend, or restrict the call to `run_main`.")
261 )]
262 Unsupported {
263 reason: String,
267 },
268
269 #[error("remote import {}: {}", payload.url, payload.cause)]
276 #[diagnostic(
277 code(relon::eval::remote_import_failed),
278 help("The host could not retrieve the remote module. Check connectivity, the URL, and that the server returns a 2xx response with a Relon source body.")
279 )]
280 RemoteImportFailed {
281 payload: Box<RemoteImportFailure>,
282 #[label("remote import failed")]
283 range: TokenRange,
284 },
285
286 #[error("remote import {} denied: {}", payload.url, payload.reason)]
290 #[diagnostic(
291 code(relon::eval::remote_import_denied),
292 help("Remote `#import` is a network operation. Run the host with `--trust` (CLI) or grant `Capabilities::network` to allow it.")
293 )]
294 RemoteImportDenied {
295 payload: Box<RemoteImportDenial>,
296 #[label("remote import rejected by sandbox")]
297 range: TokenRange,
298 },
299
300 #[error(
307 "remote import {} hash mismatch: expected {}, got {}",
308 payload.url,
309 payload.expected,
310 payload.got
311 )]
312 #[diagnostic(
313 code(relon::eval::remote_import_hash_mismatch),
314 help("The remote source's sha256 differs from the pinned hash. Either update the pin or refuse to load the module.")
315 )]
316 RemoteImportHashMismatch {
317 payload: Box<RemoteImportHashMismatchDetail>,
318 #[label("hash mismatch on remote import")]
319 range: TokenRange,
320 },
321
322 #[error(
332 "import {} hash mismatch: expected {}:{}, got {}",
333 payload.path,
334 payload.algorithm,
335 payload.expected,
336 payload.got
337 )]
338 #[diagnostic(
339 code(relon::eval::import_hash_mismatch),
340 help("The module body the evaluator loaded does not match the inline integrity pin on this `#import`. Either update the pin to the new digest or refuse to trust the source.")
341 )]
342 ImportHashMismatch {
343 payload: Box<ImportHashMismatchDetail>,
344 #[label("import body does not match pinned digest")]
345 range: TokenRange,
346 },
347
348 #[error("import {path} pinned with unsupported hash algorithm `{algorithm}`")]
355 #[diagnostic(
356 code(relon::eval::import_hash_unknown_algorithm),
357 help("Use a supported algorithm (currently `sha256:`). The evaluator refuses to load an `#import` it cannot verify against the pin.")
358 )]
359 ImportHashUnknownAlgorithm {
360 path: String,
361 algorithm: String,
362 #[label("unsupported integrity algorithm")]
363 range: TokenRange,
364 },
365
366 #[error(
372 "import {path} pinned with invalid {algorithm} hex (expected {expected_len} chars, got {got_len})"
373 )]
374 #[diagnostic(
375 code(relon::eval::import_hash_invalid_hex),
376 help("The pin's hex digest is not the expected length or contains non-hex characters. Re-encode the digest as lowercase hex.")
377 )]
378 ImportHashInvalidHex {
379 path: String,
380 algorithm: String,
381 expected_len: usize,
382 got_len: usize,
383 #[label("invalid integrity hex")]
384 range: TokenRange,
385 },
386}
387
388#[derive(Debug, Clone)]
392pub struct RemoteImportFailure {
393 pub url: String,
394 pub cause: String,
395}
396
397#[derive(Debug, Clone)]
401pub struct RemoteImportDenial {
402 pub url: String,
403 pub reason: String,
404}
405
406#[derive(Debug, Clone)]
411pub struct RemoteImportHashMismatchDetail {
412 pub url: String,
413 pub expected: String,
414 pub got: String,
415}
416
417#[derive(Debug, Clone)]
422pub struct ImportHashMismatchDetail {
423 pub path: String,
428 pub algorithm: String,
430 pub expected: String,
432 pub got: String,
435}