Skip to main content

quickjs_oxide/engine/api/
error.rs

1use crate::source::SourceSpan;
2use std::error::Error as StdError;
3use std::fmt;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum ErrorKind {
7    Syntax,
8    Type,
9    Reference,
10    Range,
11    /// A JavaScript-visible QuickJS `InternalError`, distinct from an engine
12    /// invariant or implementation fault.
13    JsInternal,
14    Internal,
15    Io,
16    /// A conformance decision which cannot yet be made because compilation
17    /// reached the current implementation frontier.
18    ///
19    /// This is deliberately not a JavaScript-visible native error kind. Public
20    /// compiler and runtime APIs preserve this provenance so ordinary callers
21    /// and conformance tooling cannot mistake an implementation gap for a
22    /// conforming early error.
23    Unsupported,
24}
25
26/// QuickJS native Error subclasses, in the same stable order as
27/// `JSErrorEnum` in the 2026-06-04 baseline.
28#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
29#[repr(u8)]
30pub enum NativeErrorKind {
31    Eval,
32    Range,
33    Reference,
34    Syntax,
35    Type,
36    Uri,
37    Internal,
38    Aggregate,
39}
40
41/// Raw bytes produced by QuickJS's stack-local `char buf[256]` native-error
42/// formatter. The payload silently stops at 255 bytes; JavaScript String
43/// construction observes only the prefix before the first formatted NUL.
44#[derive(Clone, Eq, PartialEq)]
45pub struct NativeErrorMessage {
46    bytes: [u8; 256],
47    len: usize,
48}
49
50impl fmt::Debug for NativeErrorMessage {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        formatter
53            .debug_tuple("NativeErrorMessage")
54            .field(&&self.bytes[..self.len])
55            .finish()
56    }
57}
58
59impl Default for NativeErrorMessage {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl NativeErrorMessage {
66    #[must_use]
67    pub fn new() -> Self {
68        Self {
69            bytes: [0; 256],
70            len: 0,
71        }
72    }
73
74    #[must_use]
75    pub fn from_utf8(value: &str) -> Self {
76        let mut message = Self::new();
77        message.push_bytes(value.bytes());
78        message
79    }
80
81    pub fn push_bytes(&mut self, bytes: impl IntoIterator<Item = u8>) {
82        for byte in bytes {
83            if self.len == self.bytes.len() - 1 {
84                break;
85            }
86            self.bytes[self.len] = byte;
87            self.len += 1;
88        }
89    }
90
91    pub fn push_c_string_bytes(&mut self, bytes: impl IntoIterator<Item = u8>) {
92        for byte in bytes {
93            if byte == 0 || self.len == self.bytes.len() - 1 {
94                break;
95            }
96            self.bytes[self.len] = byte;
97            self.len += 1;
98        }
99    }
100
101    pub fn push_utf8(&mut self, value: &str) {
102        self.push_bytes(value.bytes());
103    }
104
105    #[must_use]
106    pub fn visible_bytes(&self) -> &[u8] {
107        let visible_len = self.bytes[..self.len]
108            .iter()
109            .position(|byte| *byte == 0)
110            .unwrap_or(self.len);
111        &self.bytes[..visible_len]
112    }
113}
114
115impl NativeErrorKind {
116    pub const ALL: [Self; 8] = [
117        Self::Eval,
118        Self::Range,
119        Self::Reference,
120        Self::Syntax,
121        Self::Type,
122        Self::Uri,
123        Self::Internal,
124        Self::Aggregate,
125    ];
126    pub const COUNT: usize = Self::ALL.len();
127
128    #[must_use]
129    pub const fn index(self) -> usize {
130        self as usize
131    }
132
133    #[must_use]
134    pub const fn name(self) -> &'static str {
135        match self {
136            Self::Eval => "EvalError",
137            Self::Range => "RangeError",
138            Self::Reference => "ReferenceError",
139            Self::Syntax => "SyntaxError",
140            Self::Type => "TypeError",
141            Self::Uri => "URIError",
142            Self::Internal => "InternalError",
143            Self::Aggregate => "AggregateError",
144        }
145    }
146
147    /// Classify an engine-facing error kind which has JavaScript throw
148    /// semantics. `Unsupported`, `Internal`, and `Io` are deliberately
149    /// excluded: implementation frontiers and engine faults must not become
150    /// catchable JavaScript exceptions through this conversion.
151    #[must_use]
152    pub const fn from_javascript_error(kind: ErrorKind) -> Option<Self> {
153        match kind {
154            ErrorKind::Syntax => Some(Self::Syntax),
155            ErrorKind::Type => Some(Self::Type),
156            ErrorKind::Reference => Some(Self::Reference),
157            ErrorKind::Range => Some(Self::Range),
158            ErrorKind::JsInternal => Some(Self::Internal),
159            ErrorKind::Unsupported | ErrorKind::Internal | ErrorKind::Io => None,
160        }
161    }
162}
163
164/// Cold error payload. Boxing it keeps [`Error`] at one word so the common
165/// `Result<(), _>`/`Result<bool, _>`/`Result<&T, _>` helper returns used the
166/// register ABI in the measured x86_64 fat-LTO build instead of a hidden
167/// 80-byte memory slot; that codegen result is target-specific.
168#[derive(Clone, Eq, PartialEq)]
169struct ErrorData {
170    kind: ErrorKind,
171    message: String,
172    native_message: Option<Box<NativeErrorMessage>>,
173    span: Option<SourceSpan>,
174}
175
176/// An engine error. JavaScript exceptions will eventually carry a heap value;
177/// this type is also usable before a context exists (lexer and decoder errors).
178#[derive(Clone, Eq, PartialEq)]
179pub struct Error(Box<ErrorData>);
180
181impl fmt::Debug for Error {
182    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183        formatter
184            .debug_struct("Error")
185            .field("kind", &self.0.kind)
186            .field("message", &self.0.message)
187            .field("native_message", &self.0.native_message)
188            .field("span", &self.0.span)
189            .finish()
190    }
191}
192
193impl Error {
194    #[must_use]
195    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
196        Self(Box::new(ErrorData {
197            kind,
198            message: message.into(),
199            native_message: None,
200            span: None,
201        }))
202    }
203
204    #[must_use]
205    pub fn from_native_message(kind: ErrorKind, native_message: NativeErrorMessage) -> Self {
206        let message = native_message.to_utf8_lossy();
207        Self(Box::new(ErrorData {
208            kind,
209            message,
210            native_message: Some(Box::new(native_message)),
211            span: None,
212        }))
213    }
214
215    #[must_use]
216    pub fn syntax(message: impl Into<String>, span: SourceSpan) -> Self {
217        Self::new(ErrorKind::Syntax, message).with_span(span)
218    }
219
220    #[must_use]
221    pub fn unsupported(message: impl Into<String>, span: SourceSpan) -> Self {
222        Self::new(ErrorKind::Unsupported, message).with_span(span)
223    }
224
225    #[must_use]
226    pub fn internal(message: impl Into<String>) -> Self {
227        Self::new(ErrorKind::Internal, message)
228    }
229
230    #[must_use]
231    pub const fn kind(&self) -> ErrorKind {
232        self.0.kind
233    }
234
235    #[must_use]
236    pub fn message(&self) -> &str {
237        &self.0.message
238    }
239
240    #[must_use]
241    pub fn native_message(&self) -> Option<&NativeErrorMessage> {
242        self.0.native_message.as_deref()
243    }
244
245    #[must_use]
246    pub const fn span(&self) -> Option<SourceSpan> {
247        self.0.span
248    }
249
250    #[must_use]
251    pub const fn with_span(mut self, span: SourceSpan) -> Self {
252        self.0.span = Some(span);
253        self
254    }
255}
256
257impl fmt::Display for Error {
258    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
259        if let Some(span) = self.0.span {
260            if self.0.kind == ErrorKind::JsInternal {
261                write!(
262                    formatter,
263                    "InternalError at {}:{}: {}",
264                    span.start.line, span.start.column, self.0.message
265                )
266            } else {
267                write!(
268                    formatter,
269                    "{:?}Error at {}:{}: {}",
270                    self.0.kind, span.start.line, span.start.column, self.0.message
271                )
272            }
273        } else {
274            match self.0.kind {
275                ErrorKind::JsInternal => write!(formatter, "InternalError: {}", self.0.message),
276                _ => write!(formatter, "{:?}Error: {}", self.0.kind, self.0.message),
277            }
278        }
279    }
280}
281
282impl StdError for Error {}
283
284#[cfg(test)]
285mod tests {
286    use super::{Error, ErrorKind, NativeErrorMessage};
287    use crate::engine::api::core::RuntimeError;
288    use crate::source::{SourceLocation, SourceSpan};
289
290    #[test]
291    fn error_equality_and_debug_preserve_exact_native_payload() {
292        let mut raw = NativeErrorMessage::new();
293        raw.push_bytes([0x80, b'A']);
294        let exact = Error::from_native_message(ErrorKind::Type, raw);
295        let public = Error::new(ErrorKind::Type, "\u{fffd}");
296
297        assert_eq!(exact.message(), "\u{fffd}");
298        assert_ne!(exact, public);
299        assert_ne!(format!("{exact:?}"), format!("{public:?}"));
300        assert_eq!(
301            format!("{exact:?}"),
302            "Error { kind: Type, message: \"�\", native_message: Some(NativeErrorMessage([128, 65])), span: None }"
303        );
304        assert_eq!(format!("{exact}"), "TypeError: �");
305        assert_eq!(exact.clone(), exact);
306        assert_eq!(
307            exact.native_message().unwrap().visible_bytes(),
308            [0x80, b'A']
309        );
310        assert!(public.native_message().is_none());
311    }
312
313    #[test]
314    fn error_channel_stays_one_register() {
315        use std::mem::size_of;
316        let word = size_of::<usize>();
317        assert_eq!(size_of::<Error>(), word);
318        assert_eq!(size_of::<Result<(), Error>>(), word);
319        assert!(size_of::<Result<bool, Error>>() <= 2 * word);
320        assert!(size_of::<Result<&u8, Error>>() <= 2 * word);
321    }
322
323    #[test]
324    fn error_span_and_debug_survive_runtime_wrapping_and_clone() {
325        let span = SourceSpan::new(
326            SourceLocation::new(12, 3, 5),
327            SourceLocation::new(18, 3, 11),
328        );
329        let original = Error::syntax("unexpected token", span);
330        assert_eq!(original.kind(), ErrorKind::Syntax);
331        assert_eq!(original.message(), "unexpected token");
332        assert_eq!(original.span(), Some(span));
333        assert_eq!(
334            format!("{original}"),
335            "SyntaxError at 3:5: unexpected token"
336        );
337        assert_eq!(
338            format!("{original:?}"),
339            "Error { kind: Syntax, message: \"unexpected token\", native_message: None, span: Some(SourceSpan { start: SourceLocation { byte_offset: 12, line: 3, column: 5 }, end: SourceLocation { byte_offset: 18, line: 3, column: 11 } }) }"
340        );
341
342        let wrapped = RuntimeError::from(original.clone());
343        assert_eq!(wrapped, RuntimeError::Compile(original.clone()));
344        assert_eq!(format!("{wrapped}"), format!("{original}"));
345        assert_eq!(format!("{wrapped:?}"), format!("Compile({original:?})"));
346        assert_eq!(original.clone(), original);
347
348        let relocated = original.clone().with_span(SourceSpan::default());
349        assert_eq!(relocated.span(), Some(SourceSpan::default()));
350        assert_ne!(relocated, original);
351        assert_eq!(original.span(), Some(span));
352    }
353
354    #[test]
355    fn with_span_remains_usable_from_a_const_function() {
356        const fn attach_span(error: Error, span: SourceSpan) -> Error {
357            error.with_span(span)
358        }
359
360        let span = SourceSpan::new(SourceLocation::new(2, 1, 3), SourceLocation::new(5, 1, 6));
361        let error = attach_span(Error::new(ErrorKind::Unsupported, "frontier"), span);
362        assert_eq!(error.kind(), ErrorKind::Unsupported);
363        assert_eq!(error.message(), "frontier");
364        assert_eq!(error.span(), Some(span));
365    }
366
367    #[test]
368    fn native_error_payload_remains_exact_after_clone_and_span_change() {
369        let mut raw = NativeErrorMessage::new();
370        raw.push_bytes([b'A', 0, 0x80]);
371        let error = Error::from_native_message(ErrorKind::JsInternal, raw);
372        let span = SourceSpan::new(SourceLocation::new(4, 2, 1), SourceLocation::new(7, 2, 4));
373        let error = error.with_span(span);
374        let cloned = error.clone();
375
376        assert_eq!(cloned, error);
377        assert_eq!(cloned.kind(), ErrorKind::JsInternal);
378        assert_eq!(cloned.span(), Some(span));
379        assert_eq!(cloned.message(), "A");
380        assert_eq!(cloned.native_message().unwrap().visible_bytes(), b"A");
381        assert_eq!(format!("{cloned}"), "InternalError at 2:1: A");
382        assert!(format!("{cloned:?}").contains("NativeErrorMessage([65, 0, 128])"));
383    }
384}