Skip to main content

weaver_lang/
error.rs

1//! Error types for parsing and evaluation.
2//!
3//! [`ParseError`] is produced during template parsing and carries source
4//! spans for diagnostic formatting. [`EvalError`] is produced during
5//! evaluation and can originate from the evaluator, the registry, or
6//! the host's [`EvalContext`](crate::eval::EvalContext) implementation.
7
8use crate::ast::span::Span;
9use std::sync::Arc;
10use thiserror::Error;
11
12// ── Parse errors ────────────────────────────────────────────────────────
13
14#[derive(Debug, Clone, Error)]
15#[error("{message}")]
16pub struct ParseError {
17    pub span: Span,
18    pub message: String,
19    pub hint: Option<String>,
20}
21
22impl ParseError {
23    pub fn new(span: Span, message: impl Into<String>) -> Self {
24        Self {
25            span,
26            message: message.into(),
27            hint: None,
28        }
29    }
30
31    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
32        self.hint = Some(hint.into());
33        self
34    }
35
36    /// Format the error with source context for display
37    pub fn format_with_source(&self, source: &str, entry_name: Option<&str>) -> String {
38        let (line, col) = offset_to_line_col(source, self.span.start);
39        let source_line = source.lines().nth(line.saturating_sub(1)).unwrap_or("");
40
41        let location = if let Some(name) = entry_name {
42            format!(" --> {name}:{line}:{col}")
43        } else {
44            format!(" --> {line}:{col}")
45        };
46
47        let pointer = " ".repeat(col.saturating_sub(1))
48            + &"^".repeat((self.span.end - self.span.start).max(1));
49
50        let mut output = format!("Error: {}\n{location}\n  |\n{line:>3} | {source_line}\n    | {pointer}", self.message);
51
52        if let Some(hint) = &self.hint {
53            output.push_str(&format!("\n  = hint: {hint}"));
54        }
55
56        output
57    }
58}
59
60fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
61    let mut line = 1;
62    let mut col = 1;
63    for (i, ch) in source.char_indices() {
64        if i >= offset {
65            break;
66        }
67        if ch == '\n' {
68            line += 1;
69            col = 1;
70        } else {
71            col += 1;
72        }
73    }
74    (line, col)
75}
76
77// ── Eval errors ─────────────────────────────────────────────────────────
78
79/// An error that occurs during template evaluation.
80///
81/// Carries a structured [`EvalErrorKind`], a human-readable message,
82/// an optional source [`Span`], and an optional underlying error cause.
83///
84/// # Error chaining
85///
86/// When a host's [`EvalContext`](crate::eval::EvalContext) implementation
87/// catches an underlying error (database, I/O, etc.), it can preserve the
88/// original error chain using [`with_source`](EvalError::with_source):
89///
90/// ```rust
91/// use weaver_lang::EvalError;
92///
93/// fn example() -> Result<(), EvalError> {
94///     let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
95///     Err(EvalError::host_error("failed to load entry").with_source(io_err))
96/// }
97/// ```
98#[derive(Debug, Clone, Error)]
99#[error("{message}")]
100pub struct EvalError {
101    pub kind: EvalErrorKind,
102    pub span: Option<Span>,
103    pub message: String,
104    /// The underlying error that caused this evaluation error, if any.
105    ///
106    /// Wrapped in `Arc` so that `EvalError` remains `Clone`.
107    #[source]
108    pub source: Option<Arc<dyn std::error::Error + Send + Sync>>,
109}
110
111impl EvalError {
112    pub fn new(kind: EvalErrorKind, message: impl Into<String>) -> Self {
113        Self {
114            kind,
115            span: None,
116            message: message.into(),
117            source: None,
118        }
119    }
120
121    pub fn with_span(mut self, span: Span) -> Self {
122        self.span = Some(span);
123        self
124    }
125
126    /// Attach an underlying error cause to this evaluation error.
127    ///
128    /// This preserves the full error chain for production logging and
129    /// debugging. The source is wrapped in an `Arc` so that `EvalError`
130    /// remains `Clone`.
131    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
132        self.source = Some(Arc::new(source));
133        self
134    }
135
136    // Convenience constructors for common error types
137
138    pub fn undefined_variable(scope: &str, name: &str) -> Self {
139        Self::new(
140            EvalErrorKind::UndefinedVariable,
141            format!("undefined variable: {scope}:{name}"),
142        )
143    }
144
145    pub fn undefined_processor(namespace: &str, name: &str) -> Self {
146        Self::new(
147            EvalErrorKind::UndefinedCallable,
148            format!("undefined processor: {namespace}.{name}"),
149        )
150    }
151
152    pub fn undefined_command(name: &str) -> Self {
153        Self::new(
154            EvalErrorKind::UndefinedCallable,
155            format!("undefined command: {name}"),
156        )
157    }
158
159    pub fn type_error(expected: &str, got: &str) -> Self {
160        Self::new(
161            EvalErrorKind::TypeError,
162            format!("expected {expected}, got {got}"),
163        )
164    }
165
166    pub fn not_iterable(got: &str) -> Self {
167        Self::new(
168            EvalErrorKind::NotIterable,
169            format!("foreach requires an array, got {got}"),
170        )
171    }
172
173    pub fn host_error(message: impl Into<String>) -> Self {
174        Self::new(EvalErrorKind::HostError, message)
175    }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum EvalErrorKind {
180    UndefinedVariable,
181    UndefinedCallable,
182    TypeError,
183    NotIterable,
184    ArithmeticError,
185    TriggerNotFound,
186    DocumentNotFound,
187    HostError,
188    RecursionLimit,
189    /// The evaluation exceeded a configured resource limit (node count
190    /// or iteration cap).
191    ResourceLimit,
192    /// The evaluation was cancelled via an external cancellation token.
193    Cancelled,
194}