Skip to main content

openjd_expr/
error.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! Expression language error types.
6
7use std::fmt;
8
9/// Structured error kinds for expression evaluation.
10///
11/// Callers can match on specific variants to handle errors programmatically
12/// without parsing error message strings.
13#[derive(Debug, Clone, thiserror::Error)]
14#[non_exhaustive]
15pub enum ExpressionErrorKind {
16    /// A referenced variable was not found in any symbol table.
17    #[error("Undefined variable: '{name}'.{suggestion}")]
18    UndefinedVariable { name: String, suggestion: String },
19
20    /// A referenced function was not found in the function library.
21    #[error("Unknown function: '{name}'")]
22    UnknownFunction { name: String },
23
24    /// Argument types did not match any overload signature.
25    #[error("{message}")]
26    TypeError { message: String },
27
28    /// An integer operation overflowed the 64-bit signed range.
29    #[error("Integer overflow: result is outside the 64-bit signed range")]
30    IntegerOverflow,
31
32    /// Division or modulo by zero.
33    #[error("{op} by zero")]
34    DivisionByZero { op: &'static str },
35
36    /// A float operation produced infinity or NaN.
37    #[error("{message}")]
38    FloatError { message: String },
39
40    /// An index was out of bounds for a list, string, or range expression.
41    #[error("{message}")]
42    IndexOutOfBounds { message: String },
43
44    /// Expression memory usage exceeded the configured limit.
45    #[error("Expression memory usage ({used} bytes) exceeded limit ({limit} bytes)")]
46    MemoryLimitExceeded { used: usize, limit: usize },
47
48    /// Expression operation count exceeded the configured limit.
49    #[error("Expression operation count ({count}) exceeded limit ({limit})")]
50    OperationLimitExceeded { count: usize, limit: usize },
51
52    /// Expression AST nesting depth exceeded the configured limit.
53    ///
54    /// Raised by the parser's structural validator and by the evaluator to
55    /// prevent stack exhaustion on pathological inputs such as
56    /// `((((...1...))))` or a ~1000-term left-associative binop chain.
57    /// The limit is [`MAX_EXPRESSION_DEPTH`](crate::MAX_EXPRESSION_DEPTH).
58    #[error("Expression nesting depth ({depth}) exceeded limit ({limit})")]
59    ExpressionTooDeep { depth: usize, limit: usize },
60
61    /// A Python syntax feature that is not supported in the expression language.
62    #[error("{feature}")]
63    UnsupportedSyntax { feature: String },
64
65    /// The `fail()` function was called explicitly.
66    #[error("{0}")]
67    ExplicitFail(String),
68
69    /// A parse error from the underlying Python parser.
70    #[error("{0}")]
71    ParseError(String),
72
73    /// Any other error that doesn't fit a structured variant.
74    #[error("{0}")]
75    Other(String),
76}
77
78/// Base error for expression evaluation.
79///
80/// Wraps an [`ExpressionErrorKind`] with optional source location context
81/// for caret-style error formatting.
82///
83/// Internally boxed (8 bytes on stack) to keep `Result<ExprValue, ExpressionError>`
84/// compact. Same pattern as `serde_json::Error`.
85#[derive(Debug, Clone)]
86pub struct ExpressionError {
87    inner: Box<ExpressionErrorInner>,
88}
89
90#[derive(Debug, Clone)]
91struct ExpressionErrorInner {
92    kind: ExpressionErrorKind,
93    expr: Option<String>,
94    col_offset: Option<usize>,
95    end_col_offset: Option<usize>,
96    caret_offset: Option<usize>,
97    sub_errors: Option<Vec<ExpressionError>>,
98}
99
100impl ExpressionError {
101    /// Create an error from a structured kind.
102    pub fn from_kind(kind: ExpressionErrorKind) -> Self {
103        Self {
104            inner: Box::new(ExpressionErrorInner {
105                kind,
106                expr: None,
107                col_offset: None,
108                end_col_offset: None,
109                caret_offset: None,
110                sub_errors: None,
111            }),
112        }
113    }
114
115    /// Create an error with a plain message string.
116    ///
117    /// This is a convenience for call sites that don't need a structured kind.
118    /// Prefer [`ExpressionError::from_kind`] with a specific variant when the
119    /// error category is known.
120    pub fn new(message: impl Into<String>) -> Self {
121        Self::from_kind(ExpressionErrorKind::Other(message.into()))
122    }
123
124    // ── Convenience constructors for common error kinds ──
125
126    /// Integer overflow error.
127    pub fn integer_overflow() -> Self {
128        Self::from_kind(ExpressionErrorKind::IntegerOverflow)
129    }
130
131    /// Division or modulo by zero.
132    pub fn division_by_zero(op: &'static str) -> Self {
133        Self::from_kind(ExpressionErrorKind::DivisionByZero { op })
134    }
135
136    /// Float produced infinity or NaN.
137    pub fn float_error(message: impl Into<String>) -> Self {
138        Self::from_kind(ExpressionErrorKind::FloatError {
139            message: message.into(),
140        })
141    }
142
143    /// Type mismatch error.
144    pub fn type_error(message: impl Into<String>) -> Self {
145        Self::from_kind(ExpressionErrorKind::TypeError {
146            message: message.into(),
147        })
148    }
149
150    /// Index out of bounds.
151    pub fn index_out_of_bounds(message: impl Into<String>) -> Self {
152        Self::from_kind(ExpressionErrorKind::IndexOutOfBounds {
153            message: message.into(),
154        })
155    }
156
157    /// Unsupported Python syntax feature.
158    pub fn unsupported(feature: impl Into<String>) -> Self {
159        Self::from_kind(ExpressionErrorKind::UnsupportedSyntax {
160            feature: feature.into(),
161        })
162    }
163
164    /// Explicit fail() call.
165    pub fn explicit_fail(message: impl Into<String>) -> Self {
166        Self::from_kind(ExpressionErrorKind::ExplicitFail(message.into()))
167    }
168
169    /// A parse-stage error (underlying parser, range expression parser, etc.).
170    pub fn parse_error(message: impl Into<String>) -> Self {
171        Self::from_kind(ExpressionErrorKind::ParseError(message.into()))
172    }
173
174    /// AST nesting depth exceeded the configured limit.
175    pub fn expression_too_deep(depth: usize, limit: usize) -> Self {
176        Self::from_kind(ExpressionErrorKind::ExpressionTooDeep { depth, limit })
177    }
178
179    /// The structured error kind.
180    pub fn kind(&self) -> &ExpressionErrorKind {
181        &self.inner.kind
182    }
183
184    /// The human-readable error message (the Display output of the kind).
185    pub fn message(&self) -> String {
186        self.inner.kind.to_string()
187    }
188
189    /// Sub-errors for compound failures (e.g., both branches of an if/else).
190    pub fn sub_errors(&self) -> &[ExpressionError] {
191        match &self.inner.sub_errors {
192            Some(v) => v.as_slice(),
193            None => &[],
194        }
195    }
196
197    /// Attach sub-errors (consumes and returns self for chaining).
198    pub fn with_sub_errors(mut self, sub_errors: Vec<ExpressionError>) -> Self {
199        if !sub_errors.is_empty() {
200            self.inner.sub_errors = Some(sub_errors);
201        }
202        self
203    }
204
205    /// The expression source text, if attached.
206    pub fn expr(&self) -> Option<&str> {
207        self.inner.expr.as_deref()
208    }
209
210    /// The start column offset within the expression, if attached.
211    pub fn col_offset(&self) -> Option<usize> {
212        self.inner.col_offset
213    }
214
215    /// The end column offset within the expression, if attached.
216    pub fn end_col_offset(&self) -> Option<usize> {
217        self.inner.end_col_offset
218    }
219
220    /// The caret offset relative to col_offset, if attached.
221    pub fn caret_offset(&self) -> Option<usize> {
222        self.inner.caret_offset
223    }
224
225    /// Attach expression source and AST node span for caret formatting.
226    #[must_use]
227    pub fn with_node(mut self, expr_source: &str, node: &ruff_python_ast::Expr) -> Self {
228        use ruff_text_size::Ranged;
229        if self.inner.expr.is_some() {
230            return self;
231        }
232        self.inner.expr = Some(expr_source.to_string());
233        let range = node.range();
234        self.inner.col_offset = Some(range.start().to_usize());
235        self.inner.end_col_offset = Some(range.end().to_usize());
236        self.inner.caret_offset = Some(compute_caret_offset(expr_source, node));
237        self
238    }
239
240    /// Attach expression source with explicit span (no AST node).
241    #[must_use]
242    pub fn with_span(mut self, expr_source: &str, col: usize, end_col: usize) -> Self {
243        if self.inner.expr.is_some() {
244            return self;
245        }
246        self.inner.expr = Some(expr_source.to_string());
247        self.inner.col_offset = Some(col);
248        self.inner.end_col_offset = Some(end_col);
249        self
250    }
251
252    /// Set the expression source and span directly (for cases where `with_node`
253    /// cannot be used because the span is computed manually).
254    pub fn set_source_span(
255        &mut self,
256        expr_source: &str,
257        col: usize,
258        end_col: usize,
259        caret_offset: usize,
260    ) {
261        self.inner.expr = Some(expr_source.to_string());
262        self.inner.col_offset = Some(col);
263        self.inner.end_col_offset = Some(end_col);
264        self.inner.caret_offset = Some(caret_offset);
265    }
266
267    /// Format the error with a prefix prepended to the expression line.
268    ///
269    /// The caret position is shifted right by `prefix.len()` so it still
270    /// points at the correct column in the combined `prefix + expr` string.
271    /// Used for let-binding errors where the expression is part of a larger
272    /// construct like `"x = Param.Frame + \"oops\""`.
273    ///
274    /// Only applies to single-line expressions with caret indicators.
275    /// Falls back to the normal `Display` output for multi-line or
276    /// context-free errors.
277    pub fn message_with_expr_prefix(&self, prefix: &str) -> String {
278        let (Some(expr), Some(col), Some(end_col)) = (
279            &self.inner.expr,
280            self.inner.col_offset,
281            self.inner.end_col_offset,
282        ) else {
283            return self.to_string();
284        };
285        if expr.contains('\n') {
286            return self.to_string();
287        }
288        let msg = self.message();
289        let mut out = msg;
290        out.push_str("\n  ");
291        out.push_str(prefix);
292        out.push_str(expr);
293        out.push_str("\n  ");
294        let _ = write_caret_line(
295            &mut out,
296            col + prefix.len(),
297            end_col + prefix.len(),
298            self.inner.caret_offset.unwrap_or(0),
299        );
300        out
301    }
302}
303
304impl fmt::Display for ExpressionError {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        write!(f, "{}", self.inner.kind)?;
307        if let (Some(expr), Some(col), Some(end_col)) = (
308            &self.inner.expr,
309            self.inner.col_offset,
310            self.inner.end_col_offset,
311        ) {
312            let is_multiline = expr.contains('\n');
313            // For multi-line, the parser wraps in parens shifting offsets by 1
314            let (col, end_col) = if is_multiline {
315                (col.saturating_sub(1), end_col.saturating_sub(1))
316            } else {
317                (col, end_col)
318            };
319
320            // Find the line containing col_offset and adjust to line-relative offsets
321            let (expr_line, line_col, line_end_col) = if is_multiline {
322                let mut pos = 0;
323                let mut found_line = expr.as_str();
324                let mut line_start = 0;
325                for line in expr.split('\n') {
326                    if pos + line.len() >= col {
327                        found_line = line;
328                        line_start = pos;
329                        break;
330                    }
331                    pos += line.len() + 1; // +1 for \n
332                }
333                let lc = col - line_start;
334                let lec = if end_col > line_start {
335                    (end_col - line_start).min(found_line.len())
336                } else {
337                    lc + 1
338                };
339                (found_line, lc, lec)
340            } else {
341                (expr.as_str(), col, end_col)
342            };
343
344            write!(f, "\n  {expr_line}\n  ")?;
345            write_caret_line(
346                f,
347                line_col,
348                line_end_col,
349                self.inner.caret_offset.unwrap_or(0),
350            )?;
351        }
352        Ok(())
353    }
354}
355
356impl std::error::Error for ExpressionError {
357    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
358        Some(&self.inner.kind)
359    }
360}
361
362/// Render a caret-annotation line matching the format used by
363/// [`ExpressionError`]'s `Display` impl: `col` leading spaces, then
364/// `caret_idx` tildes, then `^`, then trailing tildes to cover
365/// `end_col - col`.
366///
367/// `col`, `end_col`, and `caret_idx` are all zero-based column offsets
368/// measured within the displayed line (so callers that add indentation
369/// should add it into `col` before calling).
370///
371/// If the span is zero- or one-wide, only `^` is drawn (no tildes).
372///
373/// This is the single source of truth for caret rendering across the
374/// crate — `Display for ExpressionError`, `message_with_expr_prefix`,
375/// and the if/else both-branches-fail renderer in `eval_ifexp` all call
376/// it to guarantee identical output.
377pub(crate) fn write_caret_line(
378    w: &mut dyn std::fmt::Write,
379    col: usize,
380    end_col: usize,
381    caret_offset: usize,
382) -> std::fmt::Result {
383    let span_len = end_col.saturating_sub(col);
384    for _ in 0..col {
385        w.write_char(' ')?;
386    }
387    if span_len > 1 {
388        let caret_idx = caret_offset.min(span_len.saturating_sub(1));
389        for _ in 0..caret_idx {
390            w.write_char('~')?;
391        }
392        w.write_char('^')?;
393        for _ in 0..span_len.saturating_sub(caret_idx + 1) {
394            w.write_char('~')?;
395        }
396    } else {
397        w.write_char('^')?;
398    }
399    Ok(())
400}
401
402/// Compute the caret offset within a node's span based on node type.
403fn compute_caret_offset(expr: &str, node: &ruff_python_ast::Expr) -> usize {
404    use ruff_python_ast as ast;
405    use ruff_text_size::Ranged;
406    match node {
407        ast::Expr::BinOp(b) => {
408            let left_end = b.left.range().end().to_usize();
409            let right_start = b.right.range().start().to_usize();
410            let node_start = node.range().start().to_usize();
411            // Scan backwards from right operand to find operator
412            let bytes = expr.as_bytes();
413            let mut i = right_start.saturating_sub(1);
414            while i > left_end
415                && i < bytes.len()
416                && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'(')
417            {
418                i -= 1;
419            }
420            // Check for two-char operators (**, //)
421            if i > left_end
422                && i < bytes.len()
423                && i >= 1
424                && (bytes[i - 1..=i] == *b"**" || bytes[i - 1..=i] == *b"//")
425            {
426                return (i - 1) - node_start;
427            }
428            if i >= left_end && i < bytes.len() {
429                i - node_start
430            } else {
431                0
432            }
433        }
434        ast::Expr::Attribute(a) => {
435            let value_end = a.value.range().end().to_usize();
436            let node_start = node.range().start().to_usize();
437            (value_end + 1).saturating_sub(node_start) // +1 for the dot
438        }
439        ast::Expr::Call(c) => {
440            if let ast::Expr::Attribute(a) = &*c.func {
441                let value_end = a.value.range().end().to_usize();
442                let node_start = node.range().start().to_usize();
443                (value_end + 1).saturating_sub(node_start)
444            } else {
445                0
446            }
447        }
448        ast::Expr::Subscript(s) => {
449            let value_end = s.value.range().end().to_usize();
450            let node_start = node.range().start().to_usize();
451            value_end.saturating_sub(node_start)
452        }
453        _ => 0,
454    }
455}