use miette::Diagnostic;
use relon_parser::TokenRange;
use thiserror::Error;
fn format_chain(chain: &[String]) -> String {
chain.join(" \u{2192} ")
}
#[derive(Error, Debug, Diagnostic, Clone)]
pub enum RuntimeError {
#[error("Variable not found: {0}")]
#[diagnostic(
code(relon::eval::variable_not_found),
help("Check that the name is spelled correctly and is in scope at this point.")
)]
VariableNotFound(String, #[label("undefined")] TokenRange),
#[error("Type mismatch: expected {expected}, found {found}")]
#[diagnostic(code(relon::eval::type_mismatch))]
TypeMismatch {
expected: String,
found: String,
#[label("expected {expected}, got {found}")]
range: TokenRange,
},
#[error("Validation failed: {0}")]
#[diagnostic(code(relon::eval::validation_failed))]
ValidationError(String, #[label("validation failed here")] TokenRange),
#[error("Division by zero")]
#[diagnostic(
code(relon::eval::division_by_zero),
help("The right-hand operand of `/` or `%` evaluated to 0.")
)]
DivisionByZero(#[label("divisor is zero")] TokenRange),
#[error("Function not found: {0}")]
#[diagnostic(code(relon::eval::function_not_found))]
FunctionNotFound(String, #[label("called here")] TokenRange),
#[error("Circular reference detected: {}", format_chain(.cycle))]
#[diagnostic(
code(relon::eval::circular_reference),
help("Each entry depends on a later one in the cycle. Break the loop or replace one of the references with a literal value.")
)]
CircularReference {
cycle: Vec<String>,
#[label("triggers the cycle")]
range: TokenRange,
},
#[error("Unsupported operator {0:?}")]
#[diagnostic(code(relon::eval::unsupported_operator))]
UnsupportedOperator(String, #[label("not supported here")] TokenRange),
#[error("Invalid identifier: {0}")]
#[diagnostic(
code(relon::eval::invalid_identifier),
help("Function/decorator names must start with a letter or underscore and contain only alphanumeric characters or underscores.")
)]
InvalidIdentifier(String, #[label("invalid identifier")] TokenRange),
#[error("IO error: {0}")]
#[diagnostic(code(relon::eval::io_error))]
IoError(String),
#[error("Module not found at path: {0}")]
#[diagnostic(
code(relon::eval::module_not_found),
help("Check the path is relative to the importing file (or absolute) and that the file exists.")
)]
ModuleNotFound(String, #[label("import target missing")] miette::SourceSpan),
#[error("Parse error in module {path}: {message}")]
#[diagnostic(code(relon::eval::module_parse_error))]
ModuleParseError {
path: String,
message: String,
#[label("imported here")]
range: miette::SourceSpan,
},
#[error("Circular import detected: {}", format_chain(.0))]
#[diagnostic(
code(relon::eval::circular_import),
help("Two or more modules import each other. Restructure so the dependency is one-way.")
)]
CircularImport(
Vec<String>,
#[label("import that closes the cycle")] miette::SourceSpan,
),
#[error("Numeric overflow")]
#[diagnostic(code(relon::eval::numeric_overflow))]
NumericOverflow(#[label("overflowed here")] TokenRange),
#[error("Step limit exceeded")]
#[diagnostic(
code(relon::eval::step_limit_exceeded),
help("The script ran longer than the configured `max_steps` / deadline budget. Raise `Capabilities::max_steps` or refactor recursive / iterative work.")
)]
StepLimitExceeded {
limit: Option<u64>,
#[label("budget exhausted here")]
range: TokenRange,
},
#[error("Recursion limit exceeded ({limit} levels)")]
#[diagnostic(
code(relon::eval::recursion_limit_exceeded),
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.")
)]
RecursionLimitExceeded {
limit: usize,
#[label("depth limit reached here")]
range: TokenRange,
},
#[error("Value too large: {actual} elements exceeds limit of {limit}")]
#[diagnostic(
code(relon::eval::value_too_large),
help("A list/tuple/dict grew past `Capabilities::max_value_elements`. Raise the limit or shrink the value.")
)]
ValueTooLarge {
limit: usize,
actual: usize,
#[label("constructed here")]
range: TokenRange,
},
#[error("Index out of bounds")]
#[diagnostic(
code(relon::eval::index_out_of_bounds),
help("Inspect the receiver's length before indexing, or clamp the offset / length arguments so the slice stays inside the value.")
)]
IndexOutOfBounds {
#[label("index walked past the receiver length")]
range: TokenRange,
},
#[error("Operation on empty list has no defined result")]
#[diagnostic(
code(relon::eval::empty_list),
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.")
)]
EmptyList {
#[label("called on an empty list here")]
range: TokenRange,
},
#[error("Capability denied: {reason}")]
#[diagnostic(
code(relon::eval::capability_denied),
help("This Context is sandboxed. Grant the capability declared on the fn's gate (e.g. `caps.reads_fs = true`) to permit it.")
)]
CapabilityDenied {
cap_bit: Option<u32>,
reason: String,
#[label("call rejected by sandbox")]
range: TokenRange,
},
#[error("file has no `#main(...)` signature; cannot run as entry program")]
#[diagnostic(
code(relon::eval::no_main_signature),
help(
"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`."
)
)]
NoMainSignature {
#[label("no #main here")]
range: TokenRange,
},
#[error("missing argument `{name}` for `#main(...)`")]
#[diagnostic(
code(relon::eval::missing_main_arg),
help("The host must push a value for every parameter declared by `#main(...)`.")
)]
MissingMainArg {
name: String,
#[label("expected here")]
range: TokenRange,
},
#[error("unexpected argument `{name}`: not declared by `#main(...)`")]
#[diagnostic(
code(relon::eval::unexpected_main_arg),
help("Only parameters listed in `#main(...)` may be pushed; remove the extra entry or add it to the signature.")
)]
UnexpectedMainArg {
name: String,
#[label("not in signature")]
range: TokenRange,
},
#[error("type mismatch for `#main` arg `{name}`: expected {expected}, found {found}")]
#[diagnostic(code(relon::eval::main_arg_type_mismatch))]
MainArgTypeMismatch {
name: String,
expected: String,
found: String,
#[label("type mismatch")]
range: TokenRange,
},
#[error("type mismatch for `#main` return value: expected {expected}, found {found}")]
#[diagnostic(code(relon::eval::main_return_type_mismatch))]
MainReturnTypeMismatch {
expected: String,
found: String,
#[label("declared here")]
range: TokenRange,
},
#[error("operation not supported by this backend: {reason}")]
#[diagnostic(
code(relon::eval::unsupported),
help("This backend lacks the runtime structures the operation needs. Switch to the tree-walking backend, or restrict the call to `run_main`.")
)]
Unsupported {
reason: String,
},
#[error("remote import {}: {}", payload.url, payload.cause)]
#[diagnostic(
code(relon::eval::remote_import_failed),
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.")
)]
RemoteImportFailed {
payload: Box<RemoteImportFailure>,
#[label("remote import failed")]
range: TokenRange,
},
#[error("remote import {} denied: {}", payload.url, payload.reason)]
#[diagnostic(
code(relon::eval::remote_import_denied),
help("Remote `#import` is a network operation. Run the host with `--trust` (CLI) or grant `Capabilities::network` to allow it.")
)]
RemoteImportDenied {
payload: Box<RemoteImportDenial>,
#[label("remote import rejected by sandbox")]
range: TokenRange,
},
#[error(
"remote import {} hash mismatch: expected {}, got {}",
payload.url,
payload.expected,
payload.got
)]
#[diagnostic(
code(relon::eval::remote_import_hash_mismatch),
help("The remote source's sha256 differs from the pinned hash. Either update the pin or refuse to load the module.")
)]
RemoteImportHashMismatch {
payload: Box<RemoteImportHashMismatchDetail>,
#[label("hash mismatch on remote import")]
range: TokenRange,
},
#[error(
"import {} hash mismatch: expected {}:{}, got {}",
payload.path,
payload.algorithm,
payload.expected,
payload.got
)]
#[diagnostic(
code(relon::eval::import_hash_mismatch),
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.")
)]
ImportHashMismatch {
payload: Box<ImportHashMismatchDetail>,
#[label("import body does not match pinned digest")]
range: TokenRange,
},
#[error("import {path} pinned with unsupported hash algorithm `{algorithm}`")]
#[diagnostic(
code(relon::eval::import_hash_unknown_algorithm),
help("Use a supported algorithm (currently `sha256:`). The evaluator refuses to load an `#import` it cannot verify against the pin.")
)]
ImportHashUnknownAlgorithm {
path: String,
algorithm: String,
#[label("unsupported integrity algorithm")]
range: TokenRange,
},
#[error(
"import {path} pinned with invalid {algorithm} hex (expected {expected_len} chars, got {got_len})"
)]
#[diagnostic(
code(relon::eval::import_hash_invalid_hex),
help("The pin's hex digest is not the expected length or contains non-hex characters. Re-encode the digest as lowercase hex.")
)]
ImportHashInvalidHex {
path: String,
algorithm: String,
expected_len: usize,
got_len: usize,
#[label("invalid integrity hex")]
range: TokenRange,
},
}
#[derive(Debug, Clone)]
pub struct RemoteImportFailure {
pub url: String,
pub cause: String,
}
#[derive(Debug, Clone)]
pub struct RemoteImportDenial {
pub url: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct RemoteImportHashMismatchDetail {
pub url: String,
pub expected: String,
pub got: String,
}
#[derive(Debug, Clone)]
pub struct ImportHashMismatchDetail {
pub path: String,
pub algorithm: String,
pub expected: String,
pub got: String,
}