Skip to main content

lua_types/
error.rs

1//! `LuaError` and its canonical constructors. PORT_STRATEGY §3.7, PORTING.md §6.
2
3use crate::status::LuaStatus;
4use crate::value::LuaValue;
5use std::fmt;
6
7/// Internal control-flow payload used by the standalone CLI to implement
8/// `os.exit` without making Lua protected calls catch it as an ordinary error.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct LuaExit(pub i32);
11
12/// Internal control-flow payload for Lua 5.5 `coroutine.close()` self-close.
13/// It is caught at the coroutine resume boundary; panic hooks should suppress it
14/// like [`LuaExit`] because it is not a Rust runtime panic.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct LuaThreadClose(pub LuaStatus);
17
18/// The Lua error type. Carries a `LuaValue` payload because Lua errors can
19/// be any value (typically a string).
20///
21/// The `*Msg` variants carry the message as owned bytes instead of a GC
22/// string: host-side code builds errors with no VM (and no active heap) in
23/// scope, and allocating the payload eagerly either required an active
24/// `HeapGuard` or fell back to a detached, never-freed box (the issue #249
25/// leak class — see #253). The bytes are materialized into a real GC string
26/// by [`into_value`](LuaError::into_value) at the moment the error actually
27/// enters a VM, where a heap is active by construction.
28#[derive(Debug, Clone)]
29pub enum LuaError {
30    Runtime(LuaValue),
31    Syntax(LuaValue),
32    RuntimeMsg(Box<[u8]>),
33    SyntaxMsg(Box<[u8]>),
34    Memory,
35    Error,
36    Yield,
37    File,
38    Gc,
39}
40
41impl LuaError {
42    // ── Generic message constructors ─────────────────────────────────────
43
44    /// Build a runtime error whose message is owned bytes — no GC
45    /// allocation happens here; [`into_value`](LuaError::into_value)
46    /// materializes the string when the error enters a VM (#253).
47    pub fn runtime(args: fmt::Arguments<'_>) -> Self {
48        LuaError::RuntimeMsg(format!("{}", args).into_bytes().into_boxed_slice())
49    }
50    pub fn syntax(args: fmt::Arguments<'_>) -> Self {
51        LuaError::SyntaxMsg(format!("{}", args).into_bytes().into_boxed_slice())
52    }
53    pub fn syntax_at(args: fmt::Arguments<'_>, source: &[u8], line: i32) -> Self {
54        LuaError::SyntaxMsg(
55            format!("{}:{}: {}", String::from_utf8_lossy(source), line, args)
56                .into_bytes()
57                .into_boxed_slice(),
58        )
59    }
60    pub fn syntax_raw(msg: &[u8]) -> Self {
61        LuaError::SyntaxMsg(msg.to_vec().into_boxed_slice())
62    }
63
64    // ── Standard-shape constructors ──────────────────────────────────────
65    pub fn type_error(v: &LuaValue, op: &str) -> Self {
66        LuaError::runtime(format_args!("attempt to {} a {} value", op, v.type_name()))
67    }
68    pub fn call_error(v: &LuaValue) -> Self {
69        Self::type_error(v, "call")
70    }
71    pub fn concat_error(p1: &LuaValue, p2: &LuaValue) -> Self {
72        let bad = if matches!(p1, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_)) {
73            p2
74        } else {
75            p1
76        };
77        LuaError::runtime(format_args!(
78            "attempt to concatenate a {} value",
79            bad.type_name()
80        ))
81    }
82    pub fn arith_error(p1: &LuaValue, p2: &LuaValue, _msg: &str) -> Self {
83        let bad = if matches!(p1, LuaValue::Int(_) | LuaValue::Float(_)) {
84            p2
85        } else {
86            p1
87        };
88        LuaError::runtime(format_args!(
89            "attempt to perform arithmetic on a {} value",
90            bad.type_name()
91        ))
92    }
93    pub fn int_overflow(_p1: &LuaValue, _p2: &LuaValue) -> Self {
94        LuaError::runtime(format_args!("number has no integer representation"))
95    }
96    pub fn order_error(p1: &LuaValue, p2: &LuaValue) -> Self {
97        LuaError::runtime(format_args!(
98            "attempt to compare {} with {}",
99            p1.type_name(),
100            p2.type_name()
101        ))
102    }
103    pub fn for_error(v: &LuaValue, what: &str) -> Self {
104        LuaError::runtime(format_args!(
105            "bad 'for' {} (number expected, got {})",
106            what,
107            v.type_name()
108        ))
109    }
110    pub fn arg_error(narg: i32, msg: &str) -> Self {
111        LuaError::runtime(format_args!("bad argument #{} ({})", narg, msg))
112    }
113    pub fn type_arg_error(narg: i32, expected: &str, got: &LuaValue) -> Self {
114        LuaError::runtime(format_args!(
115            "bad argument #{} ({} expected, got {})",
116            narg,
117            expected,
118            got.type_name()
119        ))
120    }
121
122    // ── Pass-through constructors ────────────────────────────────────────
123    pub fn from_value(v: LuaValue) -> Self {
124        // Special-case: the global "not enough memory" string becomes Memory.
125        // The real impl checks ptr equality against G(L)->memerrmsg.
126        LuaError::Runtime(v)
127    }
128    pub fn with_status(status: LuaStatus) -> Self {
129        match status {
130            LuaStatus::Ok => LuaError::Error,
131            LuaStatus::Yield => LuaError::Yield,
132            LuaStatus::ErrRun => LuaError::Runtime(LuaValue::Nil),
133            LuaStatus::ErrSyntax => LuaError::Syntax(LuaValue::Nil),
134            LuaStatus::ErrMem => LuaError::Memory,
135            LuaStatus::ErrErr => LuaError::Error,
136            LuaStatus::ErrFile => LuaError::File,
137            LuaStatus::ErrGc => LuaError::Gc,
138        }
139    }
140
141    pub fn to_status(&self) -> LuaStatus {
142        match self {
143            LuaError::Runtime(_) | LuaError::RuntimeMsg(_) => LuaStatus::ErrRun,
144            LuaError::Syntax(_) | LuaError::SyntaxMsg(_) => LuaStatus::ErrSyntax,
145            LuaError::Memory => LuaStatus::ErrMem,
146            LuaError::Error => LuaStatus::ErrErr,
147            LuaError::Yield => LuaStatus::Yield,
148            LuaError::File => LuaStatus::ErrFile,
149            LuaError::Gc => LuaStatus::ErrGc,
150        }
151    }
152
153    /// The message bytes, when this error carries a textual message —
154    /// either a GC string payload or the owned bytes of a not-yet-
155    /// materialized `*Msg` variant. The canonical accessor for host-side
156    /// message extraction; callers needing a fallback keep their own.
157    /// Never allocates, never panics.
158    pub fn message_bytes(&self) -> Option<&[u8]> {
159        match self {
160            LuaError::Runtime(LuaValue::Str(s)) | LuaError::Syntax(LuaValue::Str(s)) => {
161                Some(s.as_bytes())
162            }
163            LuaError::RuntimeMsg(b) | LuaError::SyntaxMsg(b) => Some(b),
164            _ => None,
165        }
166    }
167
168    /// Convert the error into the Lua value that gets pushed/propagated.
169    ///
170    /// This is the VM-entry boundary for the `*Msg` variants: the owned
171    /// bytes become a real GC string here, via `GcRef::new`, which requires
172    /// an active `HeapGuard`. Every in-tree call site that pushes an error
173    /// object runs inside a guarded region (`pcall_k`, `api::load`,
174    /// `with_state`, `lua_resume`, `reset_thread`), so the allocation is
175    /// always collector-owned — this is what lets the guard-less detached
176    /// allocation arm be removed entirely (#253).
177    ///
178    /// # Panics
179    ///
180    /// Panics for `RuntimeMsg`/`SyntaxMsg` payloads when no `HeapGuard` is
181    /// active. Host code that only needs the message text should use
182    /// [`message_bytes`](LuaError::message_bytes) or
183    /// [`message_lossy`](LuaError::message_lossy) instead — those never
184    /// allocate and never panic.
185    pub fn into_value(self) -> LuaValue {
186        match self {
187            LuaError::Runtime(v) | LuaError::Syntax(v) => v,
188            LuaError::RuntimeMsg(b) | LuaError::SyntaxMsg(b) => LuaValue::Str(
189                crate::gc::GcRef::new(crate::string::LuaString::from_bytes(b.into_vec())),
190            ),
191            _ => LuaValue::Nil,
192        }
193    }
194
195    /// Human-readable error payload for embedders.
196    ///
197    /// Lua errors can carry any Lua value. When the payload is a byte string,
198    /// this returns it using lossy UTF-8 conversion; other payloads fall back to
199    /// the Lua type name so host integrations do not have to parse `Debug`.
200    pub fn message_lossy(&self) -> String {
201        match self {
202            LuaError::Runtime(v) | LuaError::Syntax(v) => lua_value_message_lossy(v),
203            LuaError::RuntimeMsg(b) | LuaError::SyntaxMsg(b) => {
204                String::from_utf8_lossy(b).into_owned()
205            }
206            LuaError::Memory => "not enough memory".to_string(),
207            LuaError::Error => "error in error handling".to_string(),
208            LuaError::Yield => "attempt to yield across a C-call boundary".to_string(),
209            LuaError::File => "file error".to_string(),
210            LuaError::Gc => "garbage collector error".to_string(),
211        }
212    }
213}
214
215fn lua_value_message_lossy(value: &LuaValue) -> String {
216    match value {
217        LuaValue::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
218        LuaValue::Nil => "nil".to_string(),
219        LuaValue::Bool(v) => v.to_string(),
220        LuaValue::Int(v) => v.to_string(),
221        LuaValue::Float(v) => v.to_string(),
222        other => format!("{} error", other.type_name()),
223    }
224}
225
226impl fmt::Display for LuaError {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        match self {
229            LuaError::RuntimeMsg(b) | LuaError::SyntaxMsg(b) => {
230                write!(f, "{}", String::from_utf8_lossy(b))
231            }
232            other => write!(f, "{:?}", other),
233        }
234    }
235}
236impl std::error::Error for LuaError {}
237
238// ──────────────────────────────────────────────────────────────────────────────
239// PORT STATUS
240//   source:        n/a (Rust-native error enum; no C analogue)
241//   target_crate:  lua-types
242//   confidence:    high
243//   todos:         0
244//   port_notes:    0
245//   unsafe_blocks: 0
246//   notes:         LuaError + supporting variants. Pure Rust idiom (Result<T, LuaError>) replacing
247//                  C's setjmp/longjmp error propagation. No direct C-source mapping.
248// ──────────────────────────────────────────────────────────────────────────────