lua_vm/state.rs
1//! Global State — port of `lstate.c` (445 lines, 25 functions) + `lstate.h` (merged).
2//!
3//! Manages per-thread ([`LuaState`]) and process-wide ([`GlobalState`]) Lua state:
4//! creation, initialization, teardown, and coroutine lifecycle helpers.
5//!
6//! The `lstate.h` header is merged into this module per PORTING.md §1.
7//!
8//! # C source files
9//! - `reference/lua-5.4.7/src/lstate.c` (445 lines, 25 functions)
10//! - `reference/lua-5.4.7/src/lstate.h` (408 lines; struct + macro definitions merged)
11
12
13// PORT NOTE: The C `LX` (thread + extra space) and `LG` (LX + global state) layout
14// wrappers are C-only pointer-arithmetic helpers for allocating the main thread and
15// GlobalState as one contiguous block. In Rust, `GlobalState` and `LuaState` are
16// separate heap-allocated values linked via `Rc<RefCell<GlobalState>>`. No LX/LG
17// equivalents are needed.
18
19// PORT NOTE: C macro `fromstate(L)` (cast LX* from lua_State*) is C-only pointer
20// arithmetic and is not translated. Rust owns the allocations via Rc/Box.
21
22use std::cell::RefCell;
23use std::rc::Rc;
24
25use crate::string::StringPool;
26pub use lua_types::error::LuaError;
27pub use lua_types::{CallInfoIdx, StackIdx};
28
29/// Internal: a thin wrapper used so stubbed methods can accept either
30/// `StackIdx` or `u32` (Phase A code mixes both). Phase B will normalise.
31pub struct StackIdxConv(pub StackIdx);
32
33/// Phase-A code casts `StackIdx as i32`; provide a `From` so it compiles.
34/// TODO(phase-b): expressions like `state.top_idx().0 as i32` should become
35/// `state.top_idx().raw() as i32`. The non-primitive-cast error is silenced
36/// here by promoting the StackIdx through a free-function conversion.
37#[inline(always)]
38pub fn stack_idx_to_i32(i: StackIdx) -> i32 { i.0 as i32 }
39
40impl From<u32> for StackIdxConv {
41 #[inline(always)]
42 fn from(v: u32) -> Self { StackIdxConv(StackIdx(v)) }
43}
44impl From<i32> for StackIdxConv {
45 #[inline(always)]
46 fn from(v: i32) -> Self { StackIdxConv(StackIdx(v.max(0) as u32)) }
47}
48impl From<usize> for StackIdxConv {
49 #[inline(always)]
50 fn from(v: usize) -> Self { StackIdxConv(StackIdx(v as u32)) }
51}
52impl From<StackIdx> for StackIdxConv {
53 #[inline(always)]
54 fn from(v: StackIdx) -> Self { StackIdxConv(v) }
55}
56pub use lua_types::value::{LuaTable, LuaValue, F2Imod};
57pub use lua_types::string::LuaString;
58pub use lua_types::userdata::LuaUserData;
59pub use lua_types::closure::{LuaCFnPtr, LuaClosure, LuaLClosure as LuaClosureLua, LuaCClosure as LuaClosureC};
60pub use lua_types::proto::LuaProto;
61pub use lua_types::upval::{UpVal, UpValState};
62pub use lua_types::gc::GcRef;
63
64/// A Lua-callable function pointer. C: `lua_CFunction`.
65///
66/// TODO(phase-b): the lua-types crate uses a placeholder
67/// `LuaCFnPtr = fn() -> i32` since it can't reference `LuaState` without a
68/// circular dep. The real signature is `fn(&mut LuaState) -> Result<usize, LuaError>`,
69/// kept here as the lua-vm-facing type alias.
70pub type LuaCFunction = fn(&mut LuaState) -> Result<usize, LuaError>;
71
72pub type LuaRustFunction = Rc<dyn Fn(&mut LuaState) -> Result<usize, LuaError>>;
73
74#[derive(Clone)]
75pub enum LuaCallable {
76 Bare(LuaCFunction),
77 Rust(LuaRustFunction),
78}
79
80impl std::fmt::Debug for LuaCallable {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 LuaCallable::Bare(_) => f.write_str("LuaCallable::Bare(..)"),
84 LuaCallable::Rust(_) => f.write_str("LuaCallable::Rust(..)"),
85 }
86 }
87}
88
89impl LuaCallable {
90 pub fn bare(f: LuaCFunction) -> Self {
91 LuaCallable::Bare(f)
92 }
93
94 pub fn rust(f: LuaRustFunction) -> Self {
95 LuaCallable::Rust(f)
96 }
97
98 pub fn as_bare(&self) -> Option<LuaCFunction> {
99 match self {
100 LuaCallable::Bare(f) => Some(*f),
101 LuaCallable::Rust(_) => None,
102 }
103 }
104
105 pub fn call(&self, state: &mut LuaState) -> Result<usize, LuaError> {
106 match self {
107 LuaCallable::Bare(f) => f(state),
108 LuaCallable::Rust(f) => f(state),
109 }
110 }
111}
112
113// ─── Constants (from macros.tsv) ──────────────────────────────────────────────
114
115// macros.tsv: EXTRA_STACK → const EXTRA_STACK: u32 = 5
116pub(crate) const EXTRA_STACK: usize = 5;
117
118// macros.tsv: LUA_MINSTACK → const LUA_MINSTACK: u32 = 20
119pub(crate) const LUA_MINSTACK: usize = 20;
120
121// macros.tsv: BASIC_STACK_SIZE → const BASIC_STACK_SIZE: u32 = 2 * LUA_MINSTACK
122pub(crate) const BASIC_STACK_SIZE: usize = 2 * LUA_MINSTACK;
123
124/// Maximum nested non-yielding C-call recursion depth — the single source of
125/// truth for the call-depth guard (also used by `do_::ccall_inner` and
126/// `do_::lua_resume`).
127///
128/// This is the structural defense that keeps a recursive interpreter sound for
129/// untrusted code: a recursive Rust interpreter consumes host (Rust) stack per
130/// nested Lua→Lua call, so unbounded Lua recursion would otherwise overflow the
131/// OS thread stack and crash the process. Tripping this limit instead raises a
132/// catchable `"stack overflow"` / `"C stack overflow"` Lua error.
133///
134/// Safe margin: each nested call frame consumes a bounded amount of Rust stack,
135/// so `MAXCCALLS` frames fit within the default ~8 MiB thread stack with room to
136/// spare — verified on macOS/Linux release builds against deep non-tail
137/// recursion, infinite `__index`/`__concat`/`__tostring` metamethod chains, and
138/// nested-coroutine `__close` cascades, all of which error cleanly rather than
139/// SIGSEGV (see the `recursion_*` sandbox tests). Embedders that run the VM on a
140/// smaller thread stack should lower this constant proportionally (roughly
141/// `stack_bytes / 40_000`).
142
143pub(crate) const LUAI_MAXCCALLS: u32 = 200;
144
145// macros.tsv: CIST_C → const CIST_C: u16 = 1 << 1
146pub(crate) const CIST_C: u16 = 1 << 1;
147
148// Remaining CIST_* bits from macros.tsv
149pub(crate) const CIST_OAH: u16 = 1 << 0;
150pub(crate) const CIST_FRESH: u16 = 1 << 2;
151pub(crate) const CIST_HOOKED: u16 = 1 << 3;
152pub(crate) const CIST_YPCALL: u16 = 1 << 4;
153pub(crate) const CIST_TAIL: u16 = 1 << 5;
154pub(crate) const CIST_HOOKYIELD: u16 = 1 << 6;
155pub(crate) const CIST_FIN: u16 = 1 << 7;
156pub(crate) const CIST_TRAN: u16 = 1 << 8;
157pub(crate) const CIST_RECST: u32 = 10;
158
159// macros.tsv: LUA_NUMTYPES → const LUA_NUMTYPES: usize = 9
160const LUA_NUMTYPES: usize = 9;
161
162// TODO(port): import from crate::gc (lgc.c → gc.rs) once it exists in Phase D
163const GCSTPUSR: u8 = 1;
164const GCSTPGC: u8 = 2;
165
166// TODO(port): import from crate::gc in Phase D
167const GCS_PAUSE: u8 = 0;
168
169const LUAI_GCPAUSE: u32 = 200;
170const LUAI_GCMUL: u32 = 100;
171const LUAI_GCSTEPSIZE: u8 = 13;
172const LUAI_GENMAJORMUL: u32 = 100;
173const LUAI_GENMINORMUL: u8 = 20;
174
175const WHITE0BIT: u8 = 0;
176
177const STRCACHE_N: usize = 53;
178const STRCACHE_M: usize = 2;
179
180// ─── GcKind enum ─────────────────────────────────────────────────────────────
181
182/// Garbage collector operating mode.
183///
184/// macros.tsv: `KGC_INC → GcKind::Incremental`, `KGC_GEN → GcKind::Generational`
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum GcKind {
187 Incremental = 0,
188 Generational = 1,
189}
190
191// ─── LuaStatus enum ──────────────────────────────────────────────────────────
192
193/// Thread / call status codes.
194///
195pub use lua_types::status::LuaStatus;
196
197// ─── StackValue ───────────────────────────────────────────────────────────────
198
199/// One slot on the Lua value stack. Wraps a `LuaValue` and an optional
200/// to-be-closed delta (for the `tbclist` mechanism).
201///
202/// types.tsv: `StackValue → StackValue { val: LuaValue, tbclist.delta: u16 }`
203#[derive(Clone)]
204pub struct StackValue {
205 pub val: LuaValue,
206 pub tbc_delta: u16,
207}
208
209impl Default for StackValue {
210 fn default() -> Self {
211 StackValue {
212 val: LuaValue::Nil,
213 tbc_delta: 0,
214 }
215 }
216}
217
218// ─── CallInfo ────────────────────────────────────────────────────────────────
219
220/// Saved state for a Lua or C call frame.
221///
222/// types.tsv: CallInfo → CallInfo (several fields renamed / adapted).
223///
224/// The C intrusive doubly-linked list (`previous`, `next` as raw pointers) is
225/// replaced by `Option<CallInfoIdx>` indices into `LuaState::call_info`.
226#[derive(Clone)]
227pub struct CallInfo {
228 // types.tsv: CallInfo.func → StackIdx
229 pub func: StackIdx,
230
231 // types.tsv: CallInfo.top → StackIdx
232 pub top: StackIdx,
233
234 // types.tsv: CallInfo.previous → CallInfoIdx (Option at boundary)
235 pub previous: Option<CallInfoIdx>,
236
237 // types.tsv: CallInfo.next → CallInfoIdx (Option at tail)
238 pub next: Option<CallInfoIdx>,
239
240 pub u: CallInfoFrame,
241
242 pub u2: CallInfoExtra,
243
244 // types.tsv: CallInfo.nresults → i16
245 pub nresults: i16,
246
247 // types.tsv: CallInfo.callstatus → u16 (bit-packed CIST_* flags)
248 pub callstatus: u16,
249}
250
251/// Payload of `CallInfo.u`.
252///
253#[derive(Clone, Copy)]
254pub enum CallInfoFrame {
255 Lua {
256 // types.tsv: CallInfo.u.l.savedpc → u32
257 savedpc: u32,
258 // types.tsv: CallInfo.u.l.trap → bool
259 trap: bool,
260 // types.tsv: CallInfo.u.l.nextraargs → i32
261 nextraargs: i32,
262 },
263 C {
264 // types.tsv: CallInfo.u.c.k → Option<lua_KFunction>
265 k: Option<LuaKFunction>,
266 // types.tsv: CallInfo.u.c.old_errfunc → isize
267 old_errfunc: isize,
268 // types.tsv: CallInfo.u.c.ctx → isize
269 ctx: isize,
270 },
271}
272
273/// Continuation function for yieldable C calls. C: `lua_KFunction`.
274pub type LuaKFunction = fn(&mut LuaState, status: i32, ctx: isize) -> Result<usize, LuaError>;
275
276/// Payload of `CallInfo.u2`.
277///
278/// types.tsv: CallInfo.u2 → CallInfoExtra (Rust: struct with all fields, interpretation by context)
279#[derive(Default, Clone, Copy)]
280pub struct CallInfoExtra {
281 pub value: i32,
282 pub ftransfer: u16,
283 pub ntransfer: u16,
284}
285
286impl CallInfoFrame {
287 /// Default C-call frame (no continuation, zero context).
288 pub fn c_default() -> Self {
289 CallInfoFrame::C {
290 k: None,
291 old_errfunc: 0,
292 ctx: 0,
293 }
294 }
295
296 /// Default Lua-call frame (pc=0, no trap, no extra args).
297 pub fn lua_default() -> Self {
298 CallInfoFrame::Lua {
299 savedpc: 0,
300 trap: false,
301 nextraargs: 0,
302 }
303 }
304}
305
306impl Default for CallInfo {
307 fn default() -> Self {
308 CallInfo {
309 func: StackIdx(0),
310 top: StackIdx(0),
311 previous: None,
312 next: None,
313 u: CallInfoFrame::c_default(),
314 u2: CallInfoExtra::default(),
315 nresults: 0,
316 callstatus: 0,
317 }
318 }
319}
320
321impl CallInfo {
322 pub fn is_lua(&self) -> bool { (self.callstatus & CIST_C) == 0 }
323 pub fn is_lua_code(&self) -> bool { self.is_lua() }
324 /// Whether the active function is a vararg function.
325 ///
326 /// Currently returns `false` unconditionally — vararg introspection via
327 /// `debug.getinfo` reports no vararg info instead of panicking.
328 ///
329 /// TODO(port): wire when CallInfo carries proto access for vararg detection.
330 pub fn is_vararg_func(&self) -> bool { false }
331 pub fn saved_pc(&self) -> u32 {
332 if let CallInfoFrame::Lua { savedpc, .. } = self.u { savedpc } else { 0 }
333 }
334 pub fn set_saved_pc(&mut self, pc: u32) {
335 if let CallInfoFrame::Lua { ref mut savedpc, .. } = self.u { *savedpc = pc; }
336 }
337 pub fn nextra_args(&self) -> i32 {
338 if let CallInfoFrame::Lua { nextraargs, .. } = self.u { nextraargs } else { 0 }
339 }
340 pub fn transfer_ftransfer(&self) -> u16 { self.u2.ftransfer }
341 pub fn transfer_ntransfer(&self) -> u16 { self.u2.ntransfer }
342 pub fn set_trap(&mut self, t: bool) {
343 if let CallInfoFrame::Lua { ref mut trap, .. } = self.u { *trap = t; }
344 }
345 /// Read the 3-bit recover-status field packed into bits 10-12 of callstatus.
346 ///
347 pub fn recover_status(&self) -> i32 {
348 ((self.callstatus >> CIST_RECST) & 7) as i32
349 }
350 /// Write the 3-bit recover-status field. `status` must fit in three bits.
351 ///
352 pub fn set_recover_status<T: Into<i32>>(&mut self, status: T) {
353 let st = (status.into() & 7) as u16;
354 self.callstatus = (self.callstatus & !(7u16 << CIST_RECST)) | (st << CIST_RECST);
355 }
356 pub fn get_oah(&self) -> bool { (self.callstatus & CIST_OAH) != 0 }
357 /// Store the current `allowhook` value into callstatus bit 0 (CIST_OAH).
358 ///
359 pub fn set_oah(&mut self, allow: bool) {
360 self.callstatus = (self.callstatus & !CIST_OAH) | (if allow { CIST_OAH } else { 0 });
361 }
362 pub fn u_c_old_errfunc(&self) -> isize {
363 if let CallInfoFrame::C { old_errfunc, .. } = self.u { old_errfunc } else { 0 }
364 }
365 pub fn u_c_ctx(&self) -> isize {
366 if let CallInfoFrame::C { ctx, .. } = self.u { ctx } else { 0 }
367 }
368 pub fn u_c_k(&self) -> Option<LuaKFunction> {
369 if let CallInfoFrame::C { k, .. } = self.u { k } else { None }
370 }
371 /// Set continuation function on a C-call frame.
372 ///
373 /// Panics if invoked on a Lua frame (callers must check `is_lua()` first).
374 pub fn set_u_c_k(&mut self, k: Option<LuaKFunction>) {
375 if let CallInfoFrame::C { k: ref mut slot, .. } = self.u {
376 *slot = k;
377 }
378 }
379 /// Set continuation context on a C-call frame.
380 pub fn set_u_c_ctx(&mut self, ctx: isize) {
381 if let CallInfoFrame::C { ctx: ref mut slot, .. } = self.u {
382 *slot = ctx;
383 }
384 }
385 /// Set saved old_errfunc on a C-call frame.
386 pub fn set_u_c_old_errfunc(&mut self, old_errfunc: isize) {
387 if let CallInfoFrame::C { old_errfunc: ref mut slot, .. } = self.u {
388 *slot = old_errfunc;
389 }
390 }
391 /// Set the `u2.funcidx` field, used by yieldable pcall for error recovery.
392 ///
393 pub fn set_u2_funcidx(&mut self, idx: i32) {
394 self.u2.value = idx;
395 }
396}
397
398// ─── Phase-B value/proto/instruction helpers ──────────────────────────────────
399
400/// Extension methods on `LuaValue`. TODO(phase-b): move these to
401/// `lua_types::value` (or wherever the canonical impl lives) once the type
402/// helpers stabilise.
403pub trait LuaValueExt {
404 fn base_type(&self) -> lua_types::LuaType;
405 fn to_number_no_strconv(&self) -> Option<f64>;
406 fn to_number_with_strconv(&self) -> Option<f64>;
407 fn to_integer_no_strconv(&self) -> Option<i64>;
408 fn to_integer_with_strconv(&self) -> Option<i64>;
409 fn full_type_tag(&self) -> u8;
410}
411
412impl LuaValueExt for LuaValue {
413 fn base_type(&self) -> lua_types::LuaType { self.type_tag() }
414 fn to_number_no_strconv(&self) -> Option<f64> {
415 match self {
416 LuaValue::Float(f) => Some(*f),
417 LuaValue::Int(i) => Some(*i as f64),
418 _ => None,
419 }
420 }
421 fn to_number_with_strconv(&self) -> Option<f64> {
422 if let Some(n) = self.to_number_no_strconv() { return Some(n); }
423 if let LuaValue::Str(s) = self {
424 let mut tmp = LuaValue::Nil;
425 let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
426 if sz == 0 { return None; }
427 return match tmp {
428 LuaValue::Int(i) => Some(i as f64),
429 LuaValue::Float(f) => Some(f),
430 _ => None,
431 };
432 }
433 None
434 }
435 fn to_integer_no_strconv(&self) -> Option<i64> {
436 match self {
437 LuaValue::Int(i) => Some(*i),
438 LuaValue::Float(f) if f.fract() == 0.0 && f.is_finite() => {
439 // d >= LUA_MININTEGER && d < -(lua_Number)LUA_MININTEGER.
440 // Without this, Rust's `as i64` saturates and silently
441 // produces i64::MAX / i64::MIN for out-of-range floats.
442 let min_f = i64::MIN as f64;
443 let max_plus1_f = -(i64::MIN as f64);
444 if *f >= min_f && *f < max_plus1_f {
445 Some(*f as i64)
446 } else {
447 None
448 }
449 }
450 _ => None,
451 }
452 }
453 fn to_integer_with_strconv(&self) -> Option<i64> {
454 if let Some(i) = self.to_integer_no_strconv() { return Some(i); }
455 if let LuaValue::Str(s) = self {
456 let mut tmp = LuaValue::Nil;
457 let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
458 if sz == 0 { return None; }
459 return tmp.to_integer_no_strconv();
460 }
461 None
462 }
463 fn full_type_tag(&self) -> u8 {
464 match self {
465 LuaValue::Nil => 0x00,
466 LuaValue::Bool(false) => 0x01,
467 LuaValue::Bool(true) => 0x11,
468 LuaValue::Int(_) => 0x03,
469 LuaValue::Float(_) => 0x13,
470 LuaValue::Str(s) if s.is_short() => 0x04,
471 LuaValue::Str(_) => 0x14,
472 LuaValue::LightUserData(_) => 0x02,
473 LuaValue::Table(_) => 0x05,
474 LuaValue::Function(LuaClosure::Lua(_)) => 0x06,
475 LuaValue::Function(LuaClosure::LightC(_)) => 0x16,
476 LuaValue::Function(LuaClosure::C(_)) => 0x26,
477 LuaValue::UserData(_) => 0x07,
478 LuaValue::Thread(_) => 0x08,
479 }
480 }
481}
482
483/// Extension methods on `lua_types::LuaType`.
484pub trait LuaTypeExt {
485 fn type_name(&self) -> &'static [u8];
486}
487
488impl LuaTypeExt for lua_types::LuaType {
489 fn type_name(&self) -> &'static [u8] {
490 use lua_types::LuaType::*;
491 match self {
492 None => b"no value",
493 Nil => b"nil",
494 Boolean => b"boolean",
495 LightUserData => b"userdata",
496 Number => b"number",
497 String => b"string",
498 Table => b"table",
499 Function => b"function",
500 UserData => b"userdata",
501 Thread => b"thread",
502 }
503 }
504}
505
506/// StackIdx checked-arithmetic helpers. Returns the raw `u32` because Phase A
507/// callers use the result in arithmetic comparisons against other `u32`
508/// quantities (stack-distance offsets).
509pub trait StackIdxExt {
510 fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32;
511 fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32;
512 fn raw(self) -> u32;
513}
514impl StackIdxExt for StackIdx {
515 #[inline(always)]
516 fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32 { self.0.saturating_sub(n.into().0.0) }
517 #[inline(always)]
518 fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32 { self.0.wrapping_sub(n.into().0.0) }
519 #[inline(always)]
520 fn raw(self) -> u32 { self.0 }
521}
522
523/// `GcRef<LuaTable>` / `GcRef<LuaUserData>` field-access helpers. These
524/// methods are needed by api.rs and tagmethods.rs but the lua-types
525/// placeholders don't yet expose them. TODO(phase-b): replace with real
526/// accessor methods on the canonical types in lua-types.
527///
528/// PORT NOTE: the historical `reject_invalid_table_key` precheck used to
529/// guard nil/NaN keys at this layer; it has moved inside
530/// [`LuaTable::try_raw_set`] (alongside the integer-fast-path match) so
531/// the lua-vm wrapper does not double-check.
532pub trait LuaTableRefExt {
533 fn metatable(&self) -> Option<GcRef<LuaTable>>;
534 fn as_ptr(&self) -> *const ();
535 fn get(&self, _k: &LuaValue) -> LuaValue;
536 fn get_int(&self, _k: i64) -> LuaValue;
537 fn get_short_str(&self, _k: &GcRef<LuaString>) -> LuaValue;
538 fn raw_set(&self, _state: &mut LuaState, _k: LuaValue, _v: LuaValue) -> Result<(), LuaError>;
539 fn raw_set_int(&self, _state: &mut LuaState, _k: i64, _v: LuaValue) -> Result<(), LuaError>;
540 fn invalidate_tm_cache(&self);
541 fn resize(&self, _state: &mut LuaState, _na: usize, _nh: usize) -> Result<(), LuaError>;
542 fn next(&self, _k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError>;
543}
544impl LuaTableRefExt for GcRef<LuaTable> {
545 #[inline]
546 fn metatable(&self) -> Option<GcRef<LuaTable>> { (**self).metatable() }
547 #[inline]
548 fn as_ptr(&self) -> *const () { GcRef::identity(self) as *const () }
549 #[inline]
550 fn get(&self, k: &LuaValue) -> LuaValue { (**self).get(k) }
551 #[inline]
552 fn get_int(&self, k: i64) -> LuaValue { (**self).get_int(k) }
553 #[inline]
554 fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue { (**self).get_short_str(k) }
555 /// Forwards to [`LuaTable::try_raw_set`], which performs the nil/NaN
556 /// key validation internally as part of its integer-fast-path match.
557 #[inline]
558 fn raw_set(&self, _state: &mut LuaState, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
559 (**self).try_raw_set(k, v)
560 }
561 #[inline]
562 fn raw_set_int(&self, _state: &mut LuaState, k: i64, v: LuaValue) -> Result<(), LuaError> {
563 (**self).try_raw_set_int(k, v)
564 }
565 fn invalidate_tm_cache(&self) {}
566 fn resize(&self, _state: &mut LuaState, na: usize, nh: usize) -> Result<(), LuaError> {
567 let na32 = na.min(u32::MAX as usize) as u32;
568 let nh32 = nh.min(u32::MAX as usize) as u32;
569 (**self).resize(na32, nh32)
570 }
571 fn next(&self, k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
572 (**self).try_next_pair(&k)
573 }
574}
575
576pub trait LuaUserDataRefExt {
577 fn metatable(&self) -> Option<GcRef<LuaTable>>;
578 fn set_metatable(&self, mt: Option<GcRef<LuaTable>>);
579 fn as_ptr(&self) -> *const ();
580 fn len(&self) -> usize;
581}
582impl LuaUserDataRefExt for GcRef<LuaUserData> {
583 fn metatable(&self) -> Option<GcRef<LuaTable>> { (**self).metatable() }
584 fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) { (**self).set_metatable(mt); }
585 fn as_ptr(&self) -> *const () { GcRef::identity(self) as *const () }
586 fn len(&self) -> usize { self.0.data.len() }
587}
588
589pub trait LuaStringRefExt {
590 fn is_white(&self) -> bool;
591 fn hash(&self) -> u32;
592 fn as_gc_ref(&self) -> GcRef<LuaString>;
593}
594impl LuaStringRefExt for GcRef<LuaString> {
595 fn is_white(&self) -> bool { false }
596 fn hash(&self) -> u32 { self.0.hash() }
597 fn as_gc_ref(&self) -> GcRef<LuaString> { self.clone() }
598}
599
600pub trait LuaLClosureRefExt {
601 fn proto(&self) -> &GcRef<LuaProto>;
602 fn nupvalues(&self) -> usize;
603}
604impl LuaLClosureRefExt for GcRef<lua_types::closure::LuaLClosure> {
605 fn proto(&self) -> &GcRef<LuaProto> { &self.0.proto }
606 fn nupvalues(&self) -> usize { self.0.upvals.len() }
607}
608
609/// `LuaClosure` accessor — `nupvalues()` reports the upvalue count uniformly.
610pub trait LuaClosureExt {
611 fn nupvalues(&self) -> usize;
612}
613impl LuaClosureExt for LuaClosure {
614 fn nupvalues(&self) -> usize {
615 match self {
616 LuaClosure::Lua(l) => l.0.upvals.len(),
617 LuaClosure::C(c) => c.0.upvalues.len(),
618 LuaClosure::LightC(_) => 0,
619 }
620 }
621}
622
623/// `LuaProto` source bytes accessor.
624pub trait LuaProtoExt {
625 fn source_bytes(&self) -> &[u8];
626 fn source_string(&self) -> Option<&GcRef<LuaString>>;
627}
628impl LuaProtoExt for LuaProto {
629 fn source_bytes(&self) -> &[u8] {
630 match &self.source { Some(s) => s.0.as_bytes(), None => &[] }
631 }
632 fn source_string(&self) -> Option<&GcRef<LuaString>> { self.source.as_ref() }
633}
634
635// ─── Collectable trait (GC interface) ────────────────────────────────────────
636
637/// Marker trait for GC-managed objects.
638///
639/// Phase D: real tracing GC.
640/// types.tsv: `GCObject → (trait Collectable; concrete = GcRef<T>)`
641pub trait Collectable: std::fmt::Debug {}
642
643impl std::fmt::Debug for LuaState {
644 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645 write!(f, "LuaState")
646 }
647}
648impl Collectable for LuaState {}
649
650// ─── GlobalState ─────────────────────────────────────────────────────────────
651
652/// Function-pointer signature for the text-source parser, installed on
653/// [`GlobalState::parser_hook`] by the embedder.
654///
655/// The implementation lives in `lua-parse`; `lua-vm` cannot depend on it
656/// directly (that would form a cycle), so the parser is reached via this
657/// function pointer registered at startup.
658pub type ParserHook = fn(
659 state: &mut LuaState,
660 source: &[u8],
661 name: &[u8],
662 firstchar: i32,
663) -> Result<GcRef<lua_types::closure::LuaLClosure>, LuaError>;
664
665/// Function-pointer signature for reading a file's full contents into memory,
666/// installed on [`GlobalState::file_loader_hook`] by the embedder.
667///
668/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `loadfile` and
669/// `searcher_lua` reach the filesystem via this hook. `None` keeps the file
670/// system unreachable, which is appropriate for embeddings where modules are
671/// served exclusively from `package.preload`.
672pub type FileLoaderHook = fn(filename: &[u8]) -> Result<Vec<u8>, LuaError>;
673
674/// Function-pointer signature for opening a file handle, installed on
675/// [`GlobalState::file_open_hook`] by the embedder.
676///
677/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s io library reaches
678/// the filesystem via this hook. `None` causes `io.open` and `io.output(name)`
679/// to return a "file system not available" error, which is appropriate for
680/// sandboxed embeddings.
681///
682/// `mode` is a Lua fopen-style mode string (e.g. `b"r"`, `b"w"`, `b"a"`,
683/// `b"r+"`, etc.). The hook must honour at least `r`, `w`, and `a`.
684pub type FileOpenHook =
685 fn(filename: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
686
687/// Function-pointer signature for writing bytes to a host-provided output
688/// stream, installed on [`GlobalState::stdout_hook`] or
689/// [`GlobalState::stderr_hook`] by the embedder.
690///
691/// Bare `wasm32-unknown-unknown` has no ambient stdout/stderr. Keeping output
692/// behind explicit hooks lets sandboxed and WASM hosts decide whether output is
693/// unavailable, buffered, or bridged to something like a browser console.
694pub type OutputHook = fn(bytes: &[u8]) -> std::io::Result<()>;
695
696/// Function-pointer signature for reading bytes from a host-provided input
697/// stream, installed on [`GlobalState::stdin_hook`] by the embedder.
698pub type InputHook = fn(buf: &mut [u8]) -> std::io::Result<usize>;
699
700/// Function-pointer signature for reading a host environment variable.
701///
702/// Returning `None` maps naturally to Lua's `os.getenv` result for a missing
703/// variable and is also the sandbox/bare-WASM default when no environment is
704/// exposed.
705pub type EnvHook = fn(name: &[u8]) -> Option<Vec<u8>>;
706
707/// Function-pointer signature for retrieving the current Unix time in seconds.
708pub type UnixTimeHook = fn() -> i64;
709
710/// Function-pointer signature for retrieving program CPU time in seconds.
711///
712/// Backs `os.clock`. C's `clock()` reads `CLOCK_PROCESS_CPUTIME_ID`, which has no
713/// `std` equivalent and is unavailable on bare WASM; the stdlib falls back to a
714/// monotonic wall-clock baseline (matching wasi-libc/Emscripten's emulation) when
715/// this hook is unset. A host wanting true CPU time can install one (e.g. via the
716/// `cpu-time` crate) without changing the sandboxed crates.
717pub type CpuClockHook = fn() -> f64;
718
719/// Function-pointer signature for the host's local timezone offset.
720///
721/// Given a Unix timestamp (seconds, UTC), returns the offset in seconds that the
722/// host's local timezone applies at that instant, such that
723/// `local_broken_down = gmtime(timestamp + offset)`. Positive east of UTC (e.g.
724/// `+3600` for CET), negative west (e.g. `-14400` for US EDT). This backs the
725/// local-time semantics of `os.date` (non-`!` formats) and `os.time`, which C
726/// implements with `localtime_r`/`mktime`. Reading the host timezone database
727/// requires `libc` FFI (`unsafe`), banned in `lua-stdlib`, so the host installs
728/// this hook. When unset the stdlib uses UTC (offset 0), keeping the
729/// `os.date`/`os.time` round-trip exact on hosts without a timezone.
730pub type LocalOffsetHook = fn(timestamp: i64) -> i64;
731
732/// Function-pointer signature for host entropy used by default PRNG seeds and
733/// table-sort pivot randomisation. Hosts without entropy may leave it unset; the
734/// stdlib then uses deterministic fallback values instead of touching OS stubs.
735pub type EntropyHook = fn() -> u64;
736
737/// Function-pointer signature for generating a host temporary filename.
738///
739/// Used by `os.tmpname` and `io.tmpfile`. The hook should return a path-like byte
740/// string that the host's `file_open_hook` can understand.
741pub type TempNameHook = fn() -> Result<Vec<u8>, LuaError>;
742
743/// Function-pointer signature for spawning a child process with a connected
744/// pipe, installed on [`GlobalState::popen_hook`] by the embedder.
745///
746/// `std::process::Command` is banned outside `lua-cli`, so `lua-stdlib`'s
747/// `io.popen` reaches the OS through this hook. `None` causes `io.popen` to
748/// raise a clean Lua error ("popen not enabled in this build"), which is
749/// appropriate for sandboxed embeddings.
750///
751/// `mode` is the Lua popen mode string — `b"r"` for reading the child's
752/// stdout, `b"w"` for writing to the child's stdin.
753pub type PopenHook =
754 fn(cmd: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
755
756/// Function-pointer signature for removing a file, installed on
757/// [`GlobalState::file_remove_hook`] by the embedder.
758///
759/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.remove`
760/// reaches the filesystem via this hook. Returns `Ok(())` on success.
761pub type FileRemoveHook = fn(filename: &[u8]) -> Result<(), LuaError>;
762
763/// Function-pointer signature for renaming a file, installed on
764/// [`GlobalState::file_rename_hook`] by the embedder.
765///
766/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.rename`
767/// reaches the filesystem via this hook. Returns `Ok(())` on success.
768pub type FileRenameHook = fn(from: &[u8], to: &[u8]) -> Result<(), LuaError>;
769
770/// Reason a shell command terminated, returned by [`OsExecuteHook`].
771///
772/// Mirrors the two string literals that C-Lua's `l_inspectstat` / `luaL_execresult`
773/// can produce: `"exit"` for normal process exit, `"signal"` for signal termination
774/// (POSIX only).
775#[derive(Clone, Copy, Debug)]
776pub enum OsExecuteReason {
777 /// Process exited with an exit code (`WIFEXITED` / `ExitStatus::code()` is `Some`).
778 Exit,
779 /// Process was terminated by a signal (`WIFSIGNALED` / `ExitStatus::signal()` is `Some`).
780 Signal,
781}
782
783/// Result returned by [`OsExecuteHook`], carrying the three values that
784/// C-Lua's `luaL_execresult` pushes: `(boolean|nil, "exit"|"signal", int)`.
785#[derive(Debug)]
786pub struct OsExecuteResult {
787 /// `true` when the command exited successfully (exit code 0).
788 pub success: bool,
789 /// How the process terminated.
790 pub reason: OsExecuteReason,
791 /// Exit code (for `Exit`) or signal number (for `Signal`).
792 pub code: i32,
793}
794
795/// Function-pointer signature for executing a shell command, installed on
796/// [`GlobalState::os_execute_hook`] by the embedder.
797///
798/// `std::process` is banned outside `lua-cli`, so `lua-stdlib`'s `os.execute`
799/// reaches the shell via this hook. Returns an [`OsExecuteResult`] on success,
800/// or a [`LuaError`] when the spawn itself fails.
801pub type OsExecuteHook = fn(cmd: &[u8]) -> Result<OsExecuteResult, LuaError>;
802
803/// Opaque handle to a dynamically loaded library, allocated by a
804/// [`DynLibLoadHook`] backend and stored in `package._CLIBS`.
805///
806/// The handle is a backend-owned `u64`; the embedder is free to use it as an
807/// index into a `Vec<libloading::Library>` or a `HashMap` key. `lua-stdlib`
808/// stores the value verbatim and never inspects it.
809#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
810pub struct DynLibId(pub u64);
811
812/// Resolved dynamic-library symbol.
813///
814/// Only `RustNative` is callable by this build of the VM. `LuaCAbi` resolves
815/// to a real C function pointer compiled against stock Lua 5.4's `lua_State *`
816/// ABI but cannot be safely invoked here — it is reported as an `"init"`
817/// failure with a clear message. `Unsupported` carries an embedder-provided
818/// reason byte-string.
819pub enum DynamicSymbol {
820 /// Function pointer that follows this build's Rust-native module ABI:
821 /// `fn(&mut LuaState) -> Result<usize, LuaError>`.
822 RustNative(LuaCFunction),
823 /// Symbol exported against stock Lua 5.4's C ABI. The function pointer is
824 /// resolved but never called from this build, since `lua_State *` is not
825 /// our `LuaState`. Kept as a payload so a future C-ABI facade can pick it
826 /// up; the embedder is responsible for ensuring the underlying library
827 /// outlives this value.
828 LuaCAbi(*const ()),
829 /// Embedder-provided refusal reason, e.g. "symbol resolved but ABI version
830 /// mismatch". Reported verbatim as an `"init"` failure.
831 Unsupported { reason: Vec<u8> },
832}
833
834/// Function-pointer signature for loading a dynamic library, installed on
835/// [`GlobalState::dynlib_load_hook`] by the embedder.
836///
837/// `libloading`/`dlopen`/`LoadLibraryEx` are FFI calls and require `unsafe`,
838/// which is banned in `lua-stdlib`. `lua-cli` installs a `libloading`-backed
839/// implementation. `None` causes `package.loadlib` to return the C-Lua
840/// `"absent"` failure shape, matching the fallback platform stub.
841///
842/// `see_global` mirrors C-Lua's `seeglb` (POSIX `RTLD_GLOBAL`): set when the
843/// caller invokes `package.loadlib(path, "*")`.
844pub type DynLibLoadHook =
845 fn(state: &mut LuaState, path: &[u8], see_global: bool) -> Result<DynLibId, LuaError>;
846
847/// Function-pointer signature for resolving a symbol in a previously loaded
848/// dynamic library, installed on [`GlobalState::dynlib_symbol_hook`].
849///
850/// The hook receives the [`DynLibId`] returned by [`DynLibLoadHook`] and the
851/// requested symbol name. Returning `DynamicSymbol::RustNative` makes the
852/// symbol callable; `LuaCAbi`/`Unsupported` propagate to `package.loadlib`
853/// as an `"init"` failure with a clear message.
854pub type DynLibSymbolHook =
855 fn(state: &mut LuaState, handle: DynLibId, symbol: &[u8]) -> Result<DynamicSymbol, LuaError>;
856
857/// Function-pointer signature for unloading a dynamic library, installed on
858/// [`GlobalState::dynlib_unload_hook`].
859///
860/// Called from the `_CLIBS` `__gc` metamethod when the Lua state closes.
861/// `libloading`'s safety model requires every loaded library to outlive the
862/// last symbol it exports; the CLI backend is therefore free to ignore this
863/// hook and keep libraries alive until process exit.
864pub type DynLibUnloadHook = fn(handle: DynLibId);
865
866/// One row of [`GlobalState::threads`]. Pairs the per-thread `LuaState`
867/// with the canonical `GcRef<LuaThread>` so every `push_thread` for the
868/// same id shares pointer-identity. Phase E-1 adds this; Phase E-2
869/// extends it with interior-mutability bookkeeping when `resume`/`yield`
870/// need to mutate the child thread while the parent holds a borrow.
871pub struct ThreadRegistryEntry {
872 /// The owned coroutine `LuaState`. Wrapped in `Rc<RefCell<...>>` so
873 /// that `coroutine.resume` can borrow the child mutably while the
874 /// parent is still in scope. Single-threaded — borrows never overlap
875 /// in practice because only one resume path is live at a time.
876 pub state: Rc<RefCell<LuaState>>,
877 /// Canonical thread-value handle. Reused on every push so
878 /// `GcRef::ptr_eq` is true across pushes.
879 pub value: GcRef<lua_types::value::LuaThread>,
880}
881
882/// Stable key for a value pinned in [`ExternalRootSet`].
883///
884/// The generation is part of the key so a handle that has already unrooted its
885/// slot cannot accidentally observe a later handle's value after slot reuse.
886#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
887pub struct ExternalRootKey {
888 index: usize,
889 generation: u64,
890}
891
892#[derive(Debug)]
893struct ExternalRootSlot {
894 value: Option<LuaValue>,
895 generation: u64,
896}
897
898/// Values held alive by external Rust handles.
899///
900/// This is the embedding API's GC anchor. It intentionally lives directly on
901/// `GlobalState` instead of inside the Lua registry table: handle drop/unroot
902/// must be cheap, infallible, and independent of the Lua stack protocol.
903#[derive(Debug, Default)]
904pub struct ExternalRootSet {
905 slots: Vec<ExternalRootSlot>,
906 free: Vec<usize>,
907 live: usize,
908}
909
910impl ExternalRootSet {
911 pub fn insert(&mut self, value: LuaValue) -> ExternalRootKey {
912 if let Some(index) = self.free.pop() {
913 let slot = &mut self.slots[index];
914 debug_assert!(
915 slot.value.is_none(),
916 "free external-root slot is occupied"
917 );
918 slot.generation = slot.generation.wrapping_add(1).max(1);
919 slot.value = Some(value);
920 self.live += 1;
921 ExternalRootKey {
922 index,
923 generation: slot.generation,
924 }
925 } else {
926 let index = self.slots.len();
927 self.slots.push(ExternalRootSlot {
928 value: Some(value),
929 generation: 1,
930 });
931 self.live += 1;
932 ExternalRootKey {
933 index,
934 generation: 1,
935 }
936 }
937 }
938
939 pub fn get(&self, key: ExternalRootKey) -> Option<&LuaValue> {
940 let slot = self.slots.get(key.index)?;
941 if slot.generation == key.generation {
942 slot.value.as_ref()
943 } else {
944 None
945 }
946 }
947
948 pub fn replace(&mut self, key: ExternalRootKey, value: LuaValue) -> Option<LuaValue> {
949 let slot = self.slots.get_mut(key.index)?;
950 if slot.generation != key.generation || slot.value.is_none() {
951 return None;
952 }
953 slot.value.replace(value)
954 }
955
956 pub fn remove(&mut self, key: ExternalRootKey) -> Option<LuaValue> {
957 let slot = self.slots.get_mut(key.index)?;
958 if slot.generation != key.generation {
959 return None;
960 }
961 let old = slot.value.take()?;
962 self.free.push(key.index);
963 self.live -= 1;
964 Some(old)
965 }
966
967 pub fn iter_values(&self) -> impl Iterator<Item = &LuaValue> {
968 self.slots.iter().filter_map(|slot| slot.value.as_ref())
969 }
970
971 pub fn len(&self) -> usize {
972 self.live
973 }
974
975 pub fn is_empty(&self) -> bool {
976 self.live == 0
977 }
978
979 pub fn vacant_len(&self) -> usize {
980 self.free.len()
981 }
982}
983
984/// Process-wide state shared by all Lua threads.
985///
986/// types.tsv: `global_State → GlobalState`
987///
988/// Not exposed directly at the API; accessed via `state.global()` / `state.global_mut()`.
989pub struct GlobalState {
990 /// Phase-B hook for the Lua text parser. Set by the embedder (`lua-cli`
991 /// or stdlib host) to bridge the cyclic crate split between `lua-vm` and
992 /// `lua-parse`: when `f_parser` decides the chunk is text, it invokes
993 /// this hook instead of the parser stub. `None` leaves the stub in place
994 /// so unit tests that never load text still work.
995 pub parser_hook: Option<ParserHook>,
996
997 /// The Lua language version this state speaks. The single source of truth
998 /// for version-gated behavior in the layers that read the state (parser,
999 /// stdlib openers). The embedder sets this from the [`Lua`] instance's
1000 /// [`lua_types::LuaVersion`] at construction; it defaults to
1001 /// [`lua_types::LuaVersion::V54`] so any state built without an explicit
1002 /// version keeps the existing 5.4 behavior unchanged.
1003 pub lua_version: lua_types::LuaVersion,
1004
1005 /// Phase-B hook for reading a Lua source file from disk. Set by `lua-cli`
1006 /// (or any embedder that wants `require`/`loadfile` to reach the file
1007 /// system) since `std::fs` is banned in `lua-stdlib`. `None` makes
1008 /// `loadfile` and the Lua-file searcher report a file-not-found error.
1009 pub file_loader_hook: Option<FileLoaderHook>,
1010
1011 /// Phase-B hook for opening a file handle for read/write/append. Set by
1012 /// `lua-cli` since `std::fs` is banned in `lua-stdlib`. `None` causes
1013 /// `io.open` and `io.output(name)` to return an error; standard output and
1014 /// error are controlled separately through output hooks/native fallbacks.
1015 pub file_open_hook: Option<FileOpenHook>,
1016
1017 /// Hook for host stdout. When absent, native builds fall back to Rust stdout
1018 /// for compatibility; bare `wasm32-unknown-unknown` reports stdout
1019 /// unavailable instead of touching a stubbed stdio implementation.
1020 pub stdout_hook: Option<OutputHook>,
1021
1022 /// Hook for host stderr. See [`GlobalState::stdout_hook`].
1023 pub stderr_hook: Option<OutputHook>,
1024
1025 /// Hook for host stdin. When absent, native builds fall back to Rust stdin
1026 /// for compatibility; bare `wasm32-unknown-unknown` behaves like EOF.
1027 pub stdin_hook: Option<InputHook>,
1028
1029 /// Hook for host environment lookups. `None` makes `os.getenv` return nil.
1030 pub env_hook: Option<EnvHook>,
1031
1032 /// Hook for host wall-clock time. Required for `os.time()` and `os.date()`
1033 /// without an explicit timestamp under bare WASM.
1034 pub unix_time_hook: Option<UnixTimeHook>,
1035
1036 /// Hook for host program CPU time. Backs `os.clock`. When unset, native builds
1037 /// use a monotonic wall-clock baseline and bare WASM reports it unavailable.
1038 pub cpu_clock_hook: Option<CpuClockHook>,
1039
1040 /// Hook for the host's local timezone offset at a given instant. Backs the
1041 /// local-time semantics of `os.date` (non-`!` formats) and `os.time`. When
1042 /// unset, both use UTC, matching the prior behaviour and keeping the
1043 /// `os.date`/`os.time` round-trip exact under bare WASM.
1044 pub local_offset_hook: Option<LocalOffsetHook>,
1045
1046 /// Hook for host entropy. Used by default `math.randomseed` and table sort
1047 /// pivot randomisation; absent hooks fall back to deterministic seeds.
1048 pub entropy_hook: Option<EntropyHook>,
1049
1050 /// Hook for host temporary filenames. Used by `os.tmpname` and `io.tmpfile`.
1051 pub temp_name_hook: Option<TempNameHook>,
1052
1053 /// Phase-G hook for spawning a child process and connecting one stream
1054 /// (stdin or stdout) to a Lua file handle. Set by `lua-cli` since
1055 /// `std::process::Command` is banned in `lua-stdlib`. `None` causes
1056 /// `io.popen` to raise a Lua error rather than panic.
1057 pub popen_hook: Option<PopenHook>,
1058
1059 /// Phase-B hook for removing a file. Set by `lua-cli` since `std::fs` is
1060 /// banned in `lua-stdlib`. `None` causes `os.remove` to return an error.
1061 pub file_remove_hook: Option<FileRemoveHook>,
1062
1063 /// Phase-B hook for renaming a file. Set by `lua-cli` since `std::fs` is
1064 /// banned in `lua-stdlib`. `None` causes `os.rename` to return an error.
1065 pub file_rename_hook: Option<FileRenameHook>,
1066
1067 /// Phase-G hook for executing a shell command. Set by `lua-cli` since
1068 /// `std::process` is banned in `lua-stdlib`. `None` causes `os.execute`
1069 /// to report no shell available (matching C-Lua's `system(NULL) == 0`).
1070 pub os_execute_hook: Option<OsExecuteHook>,
1071
1072 /// Phase-D-3.5 hook for loading a dynamic library (`dlopen` /
1073 /// `LoadLibraryEx`). Set by `lua-cli` since `libloading` is FFI and
1074 /// requires `unsafe`, which is banned in `lua-stdlib`. `None` causes
1075 /// `package.loadlib` to return the `"absent"` fallback shape.
1076 pub dynlib_load_hook: Option<DynLibLoadHook>,
1077
1078 /// Phase-D-3.5 hook for resolving a symbol in a previously loaded
1079 /// dynamic library (`dlsym` / `GetProcAddress`). Set by `lua-cli`.
1080 /// `None` is treated as "absent" by `package.loadlib`.
1081 pub dynlib_symbol_hook: Option<DynLibSymbolHook>,
1082
1083 /// Phase-D-3.5 hook for unloading a dynamic library (`dlclose` /
1084 /// `FreeLibrary`). Set by `lua-cli`. `None` keeps libraries loaded
1085 /// until process exit, which matches `libloading`'s safety model.
1086 pub dynlib_unload_hook: Option<DynLibUnloadHook>,
1087
1088 // types.tsv: global_State.totalbytes → isize
1089 pub totalbytes: isize,
1090
1091 /// Per-runtime sandbox budget shared across all threads. Inactive by
1092 /// default (`interval == 0`); see [`SandboxLimits`].
1093 pub sandbox: SandboxLimits,
1094
1095 // types.tsv: global_State.GCdebt → isize
1096 pub gc_debt: isize,
1097
1098 pub gc_estimate: usize,
1099
1100 // types.tsv: global_State.lastatomic → usize
1101 pub lastatomic: usize,
1102
1103 // types.tsv: global_State.strt → StringPool
1104 pub strt: StringPool,
1105
1106 // types.tsv: global_State.l_registry → LuaValue
1107 pub l_registry: LuaValue,
1108
1109 /// External Rust handles root their referents here while they are live.
1110 /// Traced from `GlobalState::trace`.
1111 pub external_roots: ExternalRootSet,
1112
1113 // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder has
1114 // no storage, so we cannot persist `registry[LUA_RIDX_GLOBALS] = globals`
1115 // via the canonical registry path. Until the placeholder reconciles with
1116 // lua-vm::table::LuaTable, the globals table lives in a direct field
1117 // and `get_global_table` reads it from here. Same for `loaded` (the
1118 // module cache normally at `registry[_LOADED]`).
1119 pub globals: LuaValue,
1120 pub loaded: LuaValue,
1121
1122 // types.tsv: global_State.nilvalue → LuaValue
1123 // PORT NOTE: In Rust we use a dedicated `is_complete: bool` flag rather than
1124 // the C trick of checking `ttisnil(&g->nilvalue)`. See `is_complete()`.
1125 pub nilvalue: LuaValue,
1126
1127 // types.tsv: global_State.seed → u32
1128 pub seed: u32,
1129
1130 // types.tsv: global_State.currentwhite → u8
1131 pub currentwhite: u8,
1132
1133 pub gcstate: u8,
1134
1135 pub gckind: u8,
1136
1137 pub gcstopem: bool,
1138
1139 // types.tsv: global_State.genminormul → u8
1140 pub genminormul: u8,
1141
1142 pub genmajormul: u8,
1143
1144 pub gcstp: u8,
1145
1146 pub gcemergency: bool,
1147
1148 // types.tsv: global_State.gcpause → u8
1149 pub gcpause: u8,
1150
1151 // types.tsv: global_State.gcstepmul → u8
1152 pub gcstepmul: u8,
1153
1154 pub gcstepsize: u8,
1155
1156 // Phase-D NOTE: the C-Lua intrusive GC lists (allgc, sweepgc, finobj,
1157 // gray, grayagain, weak, ephemeron, allweak) were declared here as
1158 // `Vec<GcRef<dyn Collectable>>` during Phase A but never populated or
1159 // read. The real GC owns its own allgc chain inside `self.heap`
1160 // (lua_gc::Heap). Removed during D-1e-prep to clear the `?Sized` blocker
1161 // for swapping `GcRef<T> = Gc<T>` (Gc requires T: Sized for unsizing).
1162 // sweepgc_cursor stayed because non-list bookkeeping kept it.
1163 pub sweepgc_cursor: usize,
1164
1165 /// Phase-B cross-table weak-sweep registry.
1166 ///
1167 /// `lua_types::value::sweep_weak_tables` iterates this list at
1168 /// `collectgarbage("collect")` time to clear entries whose weak target
1169 /// is held only by other weak slots. Holds `Weak<LuaTable>` so the
1170 /// registry itself does not pin tables that the user has dropped.
1171 /// Replaced by the proper `weak` / `ephemeron` / `allweak` lists when
1172 /// Phase D's incremental sweep lands.
1173 pub weak_tables_registry: Vec<lua_types::gc::GcWeak<lua_types::value::LuaTable>>,
1174
1175 /// Phase-B long-string allocation tracker.
1176 ///
1177 /// Each entry pairs a `Weak<LuaString>` with the byte count that was
1178 /// added to `gc_debt` at allocation time. `collectgarbage("count")` walks
1179 /// the list and reclaims `gc_debt` for entries whose weak target has been
1180 /// dropped, so the Lua-visible memory total tracks live long-string bytes.
1181 /// Short strings are interned and bounded in size, so they are not tracked
1182 /// individually. Replaced by Phase D's real allocator accounting.
1183 pub gc_tracked_long_strings: Vec<(lua_types::gc::GcWeak<lua_types::string::LuaString>, usize)>,
1184
1185 /// Phase-B pending-finalizer registry.
1186 ///
1187 /// Each entry is a strong `GcRef<LuaTable>` to a table whose metatable
1188 /// carried `__gc` at the time `setmetatable` was called. The strong ref
1189 /// pins the table so a normal `Rc::drop` does not destroy it before its
1190 /// `__gc` metamethod runs. The Phase-B finalizer sweep
1191 /// (`crate::api::run_pending_finalizers`) scans this list, takes any
1192 /// entry whose strong count is 1 (only this list holds it — i.e. the
1193 /// user has dropped every reference), and invokes its `__gc` before
1194 /// releasing the ref. Replaced by `finobj` / `tobefnz` when the real
1195 /// incremental GC lands in Phase D.
1196 pub pending_finalizers: Vec<GcRef<lua_types::value::LuaTable>>,
1197
1198 /// Tables identified by the most recent `collect_via_heap` mark phase as
1199 /// reachable only through `pending_finalizers` (i.e. the user has dropped
1200 /// every reference). Their `__gc` runs the next time
1201 /// `run_pending_finalizers` executes; entries are then cleared. Traced as
1202 /// strong roots so they survive the sweep that scheduled them.
1203 pub to_be_finalized: Vec<GcRef<lua_types::value::LuaTable>>,
1204
1205 // Phase-D NOTE: tobefnz + fixedgc removed (dead since Phase A — see
1206 // sibling note above re allgc et al). Pending finalizers live in
1207 // `pending_finalizers` above; fixed objects live in heap.allgc with the
1208 // GC's own `fixed` bit.
1209
1210 // Generational cohort markers — Phase D only
1211 // types.tsv: global_State.survival/old1/reallyold/firstold1/finobjsur/finobjold1/finobjrold
1212 // → (removed; replaced by index cursors in Phase D)
1213
1214 // types.tsv: global_State.twups → Vec<GcRef<LuaState>>
1215 pub twups: Vec<GcRef<LuaState>>,
1216
1217 // types.tsv: global_State.panic → Option<lua_CFunction>
1218 pub panic: Option<LuaCFunction>,
1219
1220 // types.tsv: global_State.mainthread → GcRef<LuaState>
1221 // TODO(port): self-referential Rc cycle; Phase D GC handles cycles properly
1222 pub mainthread: Option<GcRef<LuaState>>,
1223
1224 /// Registry of all live coroutine threads, keyed by `ThreadId`. Phase E-1
1225 /// replaces the `thread_token` placeholder with a real id-indexed map so
1226 /// `coroutine.create` allocates a fresh `LuaState`, registers it, and
1227 /// returns a value that resolves back to the same state on every
1228 /// `coroutine.status` / `coroutine.resume` call.
1229 ///
1230 /// Each entry pairs the per-thread `LuaState` with the canonical
1231 /// `GcRef<LuaThread>` value, so two `LuaValue::Thread` pushes of the
1232 /// same id share `GcRef::ptr_eq` identity. The main thread is NOT
1233 /// stored here — its `LuaState` is owned externally by the embedder.
1234 /// `main_thread_id` is reserved as `0` and a `LuaValue::Thread`
1235 /// carrying id `0` is recognized as the main thread by lookup helpers.
1236 pub threads: std::collections::HashMap<u64, ThreadRegistryEntry>,
1237
1238 /// Cached `LuaValue::Thread` payload for the main thread (id 0).
1239 /// Built once during `new_state` so every `push_thread` on the main
1240 /// thread shares the same `GcRef<LuaThread>` and thus compares
1241 /// pointer-equal under `LuaValue::PartialEq`.
1242 pub main_thread_value: GcRef<lua_types::value::LuaThread>,
1243
1244 /// Identity of the currently-running thread. `0` (main) until a
1245 /// coroutine resume swaps it in slice 02b. The Phase E-1 slice
1246 /// always leaves this at `main_thread_id` because resume is not yet
1247 /// implemented.
1248 pub current_thread_id: u64,
1249
1250 /// Identity of the main thread. Convention: `0`. Held as a field so
1251 /// the lookup helpers can read it without hard-coding the constant.
1252 pub main_thread_id: u64,
1253
1254 /// Monotonic counter handing out fresh ids in `new_thread`. Starts
1255 /// at `1` because `0` is reserved for the main thread.
1256 pub next_thread_id: u64,
1257
1258 // types.tsv: global_State.memerrmsg → GcRef<LuaString>
1259 pub memerrmsg: GcRef<LuaString>,
1260
1261 // types.tsv: global_State.tmname → [GcRef<LuaString>; TM_N]
1262 // TODO(port): TM_N constant and TagMethod enum come from ltm.c → tagmethods.rs
1263 pub tmname: Vec<GcRef<LuaString>>,
1264
1265 // types.tsv: global_State.mt → [Option<GcRef<LuaTable>>; LUA_NUMTYPES]
1266 pub mt: [Option<GcRef<LuaTable>>; LUA_NUMTYPES],
1267
1268 // types.tsv: global_State.strcache → [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N]
1269 pub strcache: [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N],
1270
1271 /// Stable intern map for the public [`LuaString`] type. Distinct from
1272 /// `strt` (which keys internal `LuaStringImpl`) because the parser and
1273 /// stdlib need pointer-equality across `intern_str` calls so
1274 /// `GcRef::ptr_eq` can resolve variable identity. Without this map each
1275 /// call allocates a fresh `GcRef` and locals/upvalues fail to resolve.
1276 pub interned_lt: std::collections::HashMap<Box<[u8]>, GcRef<LuaString>>,
1277
1278 // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
1279 pub warnf: Option<Box<dyn FnMut(&[u8], bool)>>,
1280
1281 /// Registry of native `LuaCFunction` pointers. Lua-types cannot reference
1282 /// `LuaState`, so `LuaClosure::LightC` carries a `usize` index into this
1283 /// vector instead of the real function pointer. `push_c_function`
1284 /// registers the function and stores the resulting index in the closure.
1285 pub c_functions: Vec<LuaCallable>,
1286
1287 /// Phase-D heap. Owns the allgc intrusive list and runs collections.
1288 /// During Phase A-C this is `paused=true`, so allocations don't auto-
1289 /// register and `step` is a no-op. Phase D-1d wires `unpause()` after
1290 /// state initialization, at which point `step` runs during VM dispatch.
1291 pub heap: lua_gc::Heap,
1292
1293 /// Phase E-3 cross-thread open-upvalue mirror. Maps `(thread_id, stack_idx)`
1294 /// to the live value of an open upvalue whose home thread is currently
1295 /// suspended while another thread runs. `coroutine.resume` snapshots the
1296 /// parent's open upvalues into this map before yielding control to the
1297 /// child, and reads the (possibly mutated) values back into the parent's
1298 /// stack when the child suspends or returns. From the running thread's
1299 /// perspective, `upvalue_get` / `upvalue_set` consult the mirror whenever
1300 /// an open upvalue's `thread_id` does not match `current_thread_id`.
1301 ///
1302 /// This avoids a stack refactor: the parent's `LuaState` is held by a
1303 /// `&mut` reference up the call stack during resume, so its stack cannot
1304 /// be reached directly through any `Rc<RefCell<_>>`. The mirror is the
1305 /// shared scratchpad that bridges the gap for the duration of a resume.
1306 pub cross_thread_upvals: std::collections::HashMap<(u64, StackIdx), LuaValue>,
1307
1308 /// Phase F-1.a workaround for GC use-after-free across coroutine boundaries.
1309 /// When `aux_resume` switches to a child thread, the parent's live stack
1310 /// values would otherwise become unreachable to the tracer for the duration
1311 /// of the resume (the parent `LuaState` is held only as a stack-borrowed
1312 /// `&mut` up the call chain and is not part of any traced root set). To
1313 /// keep those values alive, `aux_resume` pushes a snapshot of the parent
1314 /// stack here before transferring control, and pops it on suspension or
1315 /// completion. The tracer visits every snapshot as a GC root via the
1316 /// `Trace for GlobalState` impl in `trace_impls.rs`.
1317 ///
1318 /// Phase F-2.b added a reachability-driven thread sweep that supersedes
1319 /// most of this, but the snapshot still guards values that live only on
1320 /// the parent's stack (i.e. not yet rooted by any thread node).
1321 pub suspended_parent_stacks: Vec<Vec<LuaValue>>,
1322
1323 /// Open-upvalue handles belonging to the same suspended parent windows as
1324 /// `suspended_parent_stacks`. Stack snapshots keep the pointed-to values
1325 /// alive; this roots the `UpVal` objects themselves so a GC inside the
1326 /// child coroutine cannot sweep entries still present in the parent's
1327 /// `openupval` list.
1328 pub suspended_parent_open_upvals: Vec<Vec<GcRef<UpVal>>>,
1329}
1330
1331/// `LUA_MASKCOUNT` (`1 << LUA_HOOKCOUNT`) — the count-hook event mask the
1332/// sandbox arms on every thread to drive per-interval budget enforcement.
1333const SANDBOX_COUNT_MASK: u8 = 1 << 3;
1334
1335/// Sandbox trip code: not tripped.
1336pub const SANDBOX_TRIP_NONE: u8 = 0;
1337/// Sandbox trip code: the instruction budget reached zero.
1338pub const SANDBOX_TRIP_INSTRUCTIONS: u8 = 1;
1339/// Sandbox trip code: GC-tracked memory exceeded the configured ceiling.
1340pub const SANDBOX_TRIP_MEMORY: u8 = 2;
1341
1342/// Per-runtime sandbox budget, shared by every thread (main + coroutines) via
1343/// the `Rc<RefCell<GlobalState>>` they all hold. Every field is a `Cell` so the
1344/// VM can charge the budget through the shared `Ref` it borrows in the
1345/// count-hook path — no `&mut` and no write-borrow on the hot path.
1346/// `interval == 0` means inactive; in that case the VM never sets the
1347/// count-hook mask, so there is zero overhead.
1348#[derive(Default)]
1349pub struct SandboxLimits {
1350 /// Count-hook interval in instructions; `0` = sandbox inactive.
1351 pub interval: std::cell::Cell<i32>,
1352 /// Whether an instruction budget is enforced.
1353 pub instr_limited: std::cell::Cell<bool>,
1354 /// Instructions left before the budget trips.
1355 pub instr_remaining: std::cell::Cell<u64>,
1356 /// Configured instruction limit, retained so `reset` can refill.
1357 pub instr_limit: std::cell::Cell<u64>,
1358 /// GC-byte ceiling; `None` = no memory limit.
1359 pub mem_limit: std::cell::Cell<Option<usize>>,
1360 /// One of the `SANDBOX_TRIP_*` codes.
1361 pub tripped: std::cell::Cell<u8>,
1362 /// Sticky once a limit trips: the abort is *uncatchable*. While set,
1363 /// `pcall`/`xpcall`/`coroutine.resume` re-raise the trip error instead of
1364 /// swallowing it, so untrusted code cannot defeat the budget by catching
1365 /// it in a loop. Cleared only by [`LuaState::sandbox_reset`].
1366 pub aborting: std::cell::Cell<bool>,
1367}
1368
1369impl GlobalState {
1370 /// True while a sandbox instruction/memory budget is active on this runtime.
1371 pub fn sandbox_active(&self) -> bool {
1372 self.sandbox.interval.get() != 0
1373 }
1374
1375 /// Total live bytes allocated (GCdebt + totalbytes).
1376 ///
1377 /// macros.tsv: `gettotalbytes → g.total_bytes()`
1378 pub fn total_bytes(&self) -> usize {
1379 (self.totalbytes + self.gc_debt) as usize
1380 }
1381
1382 /// Look up the coroutine `LuaState` registered under `id`. Returns
1383 /// `None` for the main-thread id (the main `LuaState` is owned by
1384 /// the embedder, not stored in `threads`) and for ids that were
1385 /// never issued or have already been closed.
1386 pub fn get_thread(&self, id: u64) -> Option<&ThreadRegistryEntry> {
1387 self.threads.get(&id)
1388 }
1389
1390 /// Return the canonical `GcRef<LuaThread>` for `id`. For the main
1391 /// thread that's `main_thread_value`; for a coroutine it's the
1392 /// value stored in the registry. Returns `None` if `id` is unknown.
1393 pub fn thread_value_for(&self, id: u64) -> Option<GcRef<lua_types::value::LuaThread>> {
1394 if id == self.main_thread_id {
1395 Some(self.main_thread_value.clone())
1396 } else {
1397 self.threads.get(&id).map(|e| e.value.clone())
1398 }
1399 }
1400
1401 /// Returns `true` when the state has been fully initialized.
1402 ///
1403 /// macros.tsv: `completestate → g.is_complete()`
1404 ///
1405 /// PORT NOTE: C uses `g->nilvalue` being nil as the "complete" signal.
1406 /// We replicate the same logic: `nilvalue == Nil` means complete.
1407 pub fn is_complete(&self) -> bool {
1408 matches!(self.nilvalue, LuaValue::Nil)
1409 }
1410
1411 /// Returns the "current white" GC color bitmask.
1412 ///
1413 /// macros.tsv: `luaC_white → g.current_white()`
1414 ///
1415 /// PORT NOTE: GC color management deferred to Phase D; always returns
1416 /// the initial white bit.
1417 pub fn current_white(&self) -> u8 {
1418 self.currentwhite
1419 }
1420
1421 /// Returns the "other white" GC color bitmask.
1422 ///
1423 /// macros.tsv: `otherwhite → g.other_white()`
1424 pub fn other_white(&self) -> u8 {
1425 // TODO(port): Phase D — toggle white bit properly
1426 self.currentwhite ^ 0x03
1427 }
1428
1429 /// Returns `true` if the GC is in generational mode.
1430 ///
1431 /// macros.tsv: `isdecGCmodegen → g.is_gen_mode()`
1432 pub fn is_gen_mode(&self) -> bool {
1433 self.gckind == GcKind::Generational as u8
1434 }
1435
1436 /// Returns `true` if the GC is currently running.
1437 ///
1438 /// macros.tsv: `gcrunning → g.gc_running()`
1439 pub fn gc_running(&self) -> bool {
1440 self.gcstp == 0
1441 }
1442
1443 /// Returns `true` while the GC is in its propagation phase.
1444 ///
1445 /// macros.tsv: `keepinvariant → g.keep_invariant()`
1446 pub fn keep_invariant(&self) -> bool {
1447 // TODO(port): Phase D — check gcstate for propagation phases
1448 false
1449 }
1450
1451 /// Returns `true` while the GC is in a sweep phase.
1452 ///
1453 /// macros.tsv: `issweepphase → g.is_sweep_phase()`
1454 pub fn is_sweep_phase(&self) -> bool {
1455 // TODO(port): Phase D — check gcstate for sweep states (GCSswpallgc etc.)
1456 false
1457 }
1458
1459 // ── Phase-B stubs ─────────────────────────────────────────────────────────
1460 pub fn gc_debt(&self) -> isize { self.gc_debt }
1461 pub fn set_gc_debt(&mut self, d: isize) { self.gc_debt = d; }
1462 pub fn gc_at_pause(&self) -> bool { self.gcstate == 0 }
1463 pub fn gc_pause_param(&self) -> u8 { self.gcpause }
1464 pub fn set_gc_pause_param(&mut self, p: u8) { self.gcpause = p; }
1465 pub fn gc_stepmul_param(&self) -> u8 { self.gcstepmul }
1466 pub fn set_gc_stepmul_param(&mut self, p: u8) { self.gcstepmul = p; }
1467 pub fn set_gc_genmajormul(&mut self, p: u8) { self.genmajormul = p; }
1468 pub fn gc_stop_flags(&self) -> u8 { self.gcstp }
1469 pub fn set_gc_stop_flags(&mut self, f: u8) { self.gcstp = f; }
1470 pub fn stop_gc_internal(&mut self) -> u8 {
1471 let old = self.gcstp;
1472 self.gcstp |= GCSTPGC;
1473 old
1474 }
1475 pub fn set_gc_stop_user(&mut self) {
1476 // GCSTPUSR (lgc.h:155) = 1 — bit set when GC is stopped by user (lua_gc(L, LUA_GCSTOP)).
1477 self.gcstp = GCSTPUSR;
1478 }
1479 pub fn clear_gc_stop(&mut self) { self.gcstp = 0; }
1480 pub fn is_gc_running(&self) -> bool { self.gcstp == 0 }
1481 /// True when the GC has been disabled internally (state setup, mid-GC,
1482 /// or while closing); user-stop via `collectgarbage("stop")` does NOT
1483 /// set this bit, so `lua_gc` continues to honour Count/Step/etc.
1484 ///
1485 pub fn is_gc_stopped_internally(&self) -> bool { (self.gcstp & GCSTPGC) != 0 }
1486
1487 /// Returns the interned `__xxx` name string for tag method `tm`, or
1488 /// `None` if `tmname` has not yet been initialised (early bootstrap).
1489 ///
1490 /// macros.tsv: `getshrstr(G(L)->tmname[tm]) → g.tm_name(tm)`.
1491 ///
1492 /// PORT NOTE: The lua-vm crate carries two distinct `TagMethod` enums
1493 /// (one in `lua-types`, one in `crate::tagmethods`) with identical
1494 /// `#[repr(u8)]` ordering. The [`TmIndex`] trait bridges them so callers
1495 /// from either side can index `tmname` uniformly.
1496 pub fn tm_name<T: TmIndex>(&self, tm: T) -> Option<GcRef<LuaString>> {
1497 self.tmname.get(tm.tm_index()).cloned()
1498 }
1499}
1500
1501/// Discriminant-to-index conversion for the two parallel `TagMethod` enums.
1502///
1503/// Both `lua_types::tagmethod::TagMethod` and `crate::tagmethods::TagMethod`
1504/// are `#[repr(u8)]` with the same ORDER TM layout, so casting through `u8`
1505/// yields the correct `GlobalState.tmname` index for either type.
1506pub trait TmIndex: Copy {
1507 fn tm_index(self) -> usize;
1508}
1509impl TmIndex for lua_types::tagmethod::TagMethod {
1510 fn tm_index(self) -> usize { self as u8 as usize }
1511}
1512impl TmIndex for crate::tagmethods::TagMethod {
1513 fn tm_index(self) -> usize { self as u8 as usize }
1514}
1515impl TmIndex for usize {
1516 fn tm_index(self) -> usize { self }
1517}
1518impl TmIndex for u8 {
1519 fn tm_index(self) -> usize { self as usize }
1520}
1521
1522use lua_types::tagmethod::TagMethod;
1523
1524// ─── LuaState ────────────────────────────────────────────────────────────────
1525
1526/// Per-thread Lua execution state.
1527///
1528/// types.tsv: `lua_State → LuaState`
1529///
1530/// All stack-pointer fields in C (`StkIdRel`, `StkId`) become `StackIdx` (u32
1531/// index into `stack: Vec<StackValue>`). The C intrusive `CallInfo` linked list
1532/// becomes `call_info: Vec<CallInfo>` indexed by `CallInfoIdx`.
1533pub struct LuaState {
1534 // ── Thread status ──
1535
1536 // types.tsv: lua_State.status → u8
1537 pub status: u8,
1538
1539 // types.tsv: lua_State.allowhook → bool
1540 pub allowhook: bool,
1541
1542 // types.tsv: lua_State.nci → u32
1543 pub nci: u32,
1544
1545 // ── Stack ──
1546
1547 // types.tsv: lua_State.top → StackIdx
1548 pub top: StackIdx,
1549
1550 // types.tsv: lua_State.stack_last → StackIdx (redundant once Vec; kept for parity)
1551 pub stack_last: StackIdx,
1552
1553 // types.tsv: lua_State.stack → Vec<StackValue>
1554 pub stack: Vec<StackValue>,
1555
1556 // ── Call info ──
1557
1558 // types.tsv: lua_State.ci → CallInfoIdx
1559 pub ci: CallInfoIdx,
1560
1561 // types.tsv: lua_State.base_ci → CallInfo (Vec element 0)
1562 // PORT NOTE: In Rust, base_ci is call_info[0]. There is no separate field.
1563 pub call_info: Vec<CallInfo>,
1564
1565 // ── Upvalues / to-be-closed ──
1566
1567 // types.tsv: lua_State.openupval → Vec<GcRef<UpVal>>
1568 pub openupval: Vec<GcRef<UpVal>>,
1569
1570 // types.tsv: lua_State.tbclist → Vec<StackIdx>
1571 pub tbclist: Vec<StackIdx>,
1572
1573 // ── Global state ──
1574
1575 // types.tsv: lua_State.l_G → (accessed via method)
1576 // PORT NOTE: Rc<RefCell<>> for shared ownership across coroutine threads.
1577 pub(crate) global: Rc<RefCell<GlobalState>>,
1578
1579 // ── Hooks ──
1580
1581 // types.tsv: lua_State.hook → Option<Box<dyn FnMut(&mut LuaState, &LuaDebug)>>
1582 pub hook: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>,
1583
1584 // types.tsv: lua_State.hookmask → u8
1585 pub hookmask: u8,
1586
1587 // types.tsv: lua_State.basehookcount → i32
1588 pub basehookcount: i32,
1589
1590 // types.tsv: lua_State.hookcount → i32
1591 pub hookcount: i32,
1592
1593 // ── Error handling ──
1594
1595 // types.tsv: lua_State.errorJmp → (removed; replaced by Result<T, LuaError>)
1596 // PORT NOTE: Entirely removed. The `?` operator replaces setjmp/longjmp.
1597
1598 // types.tsv: lua_State.errfunc → isize
1599 pub errfunc: isize,
1600
1601 // ── C-call depth ──
1602
1603 // types.tsv: lua_State.n_ccalls → u32
1604 pub n_ccalls: u32,
1605
1606 // ── Debug / hooks ──
1607
1608 // types.tsv: lua_State.oldpc → u32
1609 pub oldpc: u32,
1610
1611 // ── GC color (Phase D) ──
1612
1613 // types.tsv: GCObject.marked → u8
1614 pub marked: u8,
1615
1616 /// Owner thread id for this `LuaState`, cached as a plain `u64` so the
1617 /// hot path of `upvalue_get` can compare against an open upvalue's
1618 /// `thread_id` without taking a `RefCell::borrow` on the shared
1619 /// `GlobalState`.
1620 ///
1621 /// Invariant: while this `LuaState` is the actively running thread,
1622 /// `GlobalState::current_thread_id == self.cached_thread_id`. This is
1623 /// maintained structurally by `new_state`/`new_thread` (which set
1624 /// `cached_thread_id` to the thread's own id once at construction)
1625 /// combined with the coroutine resume protocol: `coro_lib::resume`
1626 /// writes `co_state.global.current_thread_id = co_id` before the
1627 /// coroutine runs, and restores `parent_thread_id` on yield/return.
1628 /// Because each thread caches its own id (not the global's id), the
1629 /// invariant survives every context switch without an explicit refresh
1630 /// at the resume site.
1631 pub cached_thread_id: u64,
1632
1633 /// Local GC gate.
1634 ///
1635 /// Avoids borrowing `GlobalState` on every call edge when GC/finalizers
1636 /// are not currently due.
1637 pub gc_check_needed: bool,
1638
1639}
1640
1641impl LuaState {
1642 /// Access the process-wide `GlobalState` immutably.
1643 ///
1644 /// macros.tsv: `G → state.global()`
1645 ///
1646 /// PORT NOTE: Returns `std::cell::Ref<GlobalState>` because GlobalState is held in
1647 /// `Rc<RefCell<...>>`. Call sites that do `state.global().field` should work fine
1648 /// via `Deref`. Callers must not hold the `Ref` across a `global_mut()` call.
1649 pub fn global(&self) -> std::cell::Ref<'_, GlobalState> {
1650 self.global.borrow()
1651 }
1652
1653 /// Access the process-wide `GlobalState` mutably.
1654 ///
1655 /// macros.tsv: `G → state.global()` (writes use `state.global_mut()`)
1656 pub fn global_mut(&self) -> std::cell::RefMut<'_, GlobalState> {
1657 self.global.borrow_mut()
1658 }
1659
1660 /// Clone the `Rc` handle to the GlobalState for sharing with a new coroutine.
1661 ///
1662 /// Used in `new_thread` to give the child thread access to the same GlobalState.
1663 pub fn global_rc(&self) -> Rc<RefCell<GlobalState>> {
1664 Rc::clone(&self.global)
1665 }
1666
1667 /// Return the current C-call recursion depth (lower 16 bits of `n_ccalls`).
1668 ///
1669 /// macros.tsv: `getCcalls → state.c_calls()`
1670 pub fn c_calls(&self) -> u32 {
1671 self.n_ccalls & 0xffff
1672 }
1673
1674 /// Increment the non-yieldable call count (upper 16 bits of `n_ccalls`).
1675 ///
1676 /// macros.tsv: `incnny → state.inc_nny()`
1677 pub fn inc_nny(&mut self) {
1678 self.n_ccalls += 0x10000;
1679 }
1680
1681 /// Decrement the non-yieldable call count.
1682 ///
1683 /// macros.tsv: `decnny → state.dec_nny()`
1684 pub fn dec_nny(&mut self) {
1685 self.n_ccalls -= 0x10000;
1686 }
1687
1688 /// Returns `true` if the thread can yield (no non-yieldable frames on the stack).
1689 ///
1690 /// macros.tsv: `yieldable → state.is_yieldable()`
1691 pub fn is_yieldable(&self) -> bool {
1692 (self.n_ccalls & 0xffff0000) == 0
1693 }
1694
1695 /// Reset the hook countdown to the baseline.
1696 ///
1697 /// macros.tsv: `resethookcount → state.reset_hook_count()`
1698 pub fn reset_hook_count(&mut self) {
1699 self.hookcount = self.basehookcount;
1700 }
1701
1702 /// Activate the per-runtime sandbox budget and arm the current thread.
1703 ///
1704 /// Stores the budget in `GlobalState` (shared across every thread) and
1705 /// sets the count-hook mask on this thread so the dispatch loop traps every
1706 /// `interval` instructions. Coroutines created afterwards inherit the mask
1707 /// via `preinit_thread`, so metering spans all threads — closing the
1708 /// coroutine-escape that a per-thread closure could not. Pass `None` for a
1709 /// limit to leave that dimension unbounded.
1710 pub fn install_sandbox_limits(
1711 &mut self,
1712 interval: i32,
1713 instr_limit: Option<u64>,
1714 mem_limit: Option<usize>,
1715 ) {
1716 let interval = interval.max(1);
1717 {
1718 let g = self.global();
1719 g.sandbox.interval.set(interval);
1720 g.sandbox.instr_limited.set(instr_limit.is_some());
1721 g.sandbox.instr_remaining.set(instr_limit.unwrap_or(0));
1722 g.sandbox.instr_limit.set(instr_limit.unwrap_or(0));
1723 g.sandbox.mem_limit.set(mem_limit);
1724 g.sandbox.tripped.set(SANDBOX_TRIP_NONE);
1725 }
1726 self.hookmask |= SANDBOX_COUNT_MASK;
1727 self.basehookcount = interval;
1728 self.hookcount = interval;
1729 crate::debug::arm_traps(self);
1730 }
1731
1732 /// Charge the shared budget for one count-hook interval. Returns the abort
1733 /// error if a limit has been crossed (and records why in `tripped`).
1734 /// Called from `trace_exec` on every thread, once per `interval`
1735 /// instructions — never on the budget-disabled hot path.
1736 pub fn sandbox_charge_interval(&self) -> Option<LuaError> {
1737 let interval = self.global().sandbox.interval.get();
1738 self.sandbox_charge(interval as u64)
1739 }
1740
1741 /// Charge `amount` instructions against the runtime-wide budget and sample
1742 /// the memory ceiling. Returns the uncatchable abort error if a limit is
1743 /// crossed (recording the reason and arming the sticky `aborting` flag), or
1744 /// `None` otherwise. No-op when no sandbox is active.
1745 ///
1746 /// Used both by the per-interval VM charge and by loop-heavy stdlib
1747 /// functions (the pattern matcher) so a single native call cannot run for
1748 /// longer than the instruction budget allows.
1749 pub fn sandbox_charge(&self, amount: u64) -> Option<LuaError> {
1750 let g = self.global();
1751 if g.sandbox.interval.get() == 0 {
1752 return None;
1753 }
1754 if g.sandbox.instr_limited.get() {
1755 let rem = g.sandbox.instr_remaining.get().saturating_sub(amount);
1756 g.sandbox.instr_remaining.set(rem);
1757 if rem == 0 {
1758 g.sandbox.tripped.set(SANDBOX_TRIP_INSTRUCTIONS);
1759 g.sandbox.aborting.set(true);
1760 return Some(LuaError::runtime(format_args!(
1761 "sandbox: instruction budget exhausted"
1762 )));
1763 }
1764 }
1765 if let Some(limit) = g.sandbox.mem_limit.get() {
1766 if g.total_bytes() > limit {
1767 g.sandbox.tripped.set(SANDBOX_TRIP_MEMORY);
1768 g.sandbox.aborting.set(true);
1769 return Some(LuaError::runtime(format_args!(
1770 "sandbox: memory limit exceeded"
1771 )));
1772 }
1773 }
1774 None
1775 }
1776
1777 /// Reject a size-known-upfront allocation that would push GC-tracked memory
1778 /// past the ceiling, *before* the buffer is built. Returns the uncatchable
1779 /// memory abort if `total_bytes() + additional` exceeds the limit. Used by
1780 /// stdlib functions that allocate a large buffer of a computed size in one
1781 /// instruction (e.g. `string.rep`, `string.pack`, `table.concat`), where the
1782 /// per-instruction `sandbox_check_memory` would only fire *after* the
1783 /// allocation already happened.
1784 pub fn sandbox_reserve(&self, additional: usize) -> Option<LuaError> {
1785 let g = self.global();
1786 if g.sandbox.interval.get() == 0 {
1787 return None;
1788 }
1789 if let Some(limit) = g.sandbox.mem_limit.get() {
1790 let projected = g.total_bytes().saturating_add(additional);
1791 if projected > limit {
1792 g.sandbox.tripped.set(SANDBOX_TRIP_MEMORY);
1793 g.sandbox.aborting.set(true);
1794 return Some(LuaError::runtime(format_args!(
1795 "sandbox: memory limit exceeded"
1796 )));
1797 }
1798 }
1799 None
1800 }
1801
1802 /// Upper bound on the work a single pattern-match call may do before it must
1803 /// stop and let the caller charge the budget. Equal to the remaining
1804 /// instruction budget when an instruction limit is active, else `0` meaning
1805 /// "unlimited" (preserving non-sandboxed behavior exactly).
1806 pub fn sandbox_match_step_limit(&self) -> u64 {
1807 let g = self.global();
1808 if g.sandbox.interval.get() != 0 && g.sandbox.instr_limited.get() {
1809 g.sandbox.instr_remaining.get()
1810 } else {
1811 0
1812 }
1813 }
1814
1815 /// Whether a sandbox abort is in flight. While true, protected-call builtins
1816 /// (`pcall`/`xpcall`/`coroutine.resume`) must re-raise rather than catch, so
1817 /// the budget trip is uncatchable. Set on trip, cleared by `sandbox_reset`.
1818 pub fn sandbox_aborting(&self) -> bool {
1819 self.global().sandbox.aborting.get()
1820 }
1821
1822 /// Whether an instruction budget is active (vs. only a memory limit / none).
1823 pub fn sandbox_instr_limited(&self) -> bool {
1824 self.global().sandbox.instr_limited.get()
1825 }
1826
1827 /// Instructions left before the budget trips (meaningful only when
1828 /// [`sandbox_instr_limited`](Self::sandbox_instr_limited)).
1829 pub fn sandbox_instr_remaining(&self) -> u64 {
1830 self.global().sandbox.instr_remaining.get()
1831 }
1832
1833 /// The configured instruction limit (for computing "used").
1834 pub fn sandbox_instr_limit(&self) -> u64 {
1835 self.global().sandbox.instr_limit.get()
1836 }
1837
1838 /// The current trip code (one of the `SANDBOX_TRIP_*` constants).
1839 pub fn sandbox_tripped_code(&self) -> u8 {
1840 self.global().sandbox.tripped.get()
1841 }
1842
1843 /// Refill the instruction budget to its configured limit and clear the
1844 /// trip flag, so the same runtime can run another chunk.
1845 pub fn sandbox_reset(&self) {
1846 let g = self.global();
1847 if g.sandbox.instr_limited.get() {
1848 g.sandbox.instr_remaining.set(g.sandbox.instr_limit.get());
1849 }
1850 g.sandbox.tripped.set(SANDBOX_TRIP_NONE);
1851 g.sandbox.aborting.set(false);
1852 }
1853
1854 /// Returns the current stack capacity (slots between base and stack_last).
1855 ///
1856 /// macros.tsv: `stacksize → state.stack_size()`
1857 pub fn stack_size(&self) -> usize {
1858 self.stack_last.0 as usize
1859 }
1860
1861 /// Push a value onto the stack, incrementing `top`.
1862 ///
1863 /// macros.tsv: `api_incr_top → gone — state.push() already increments`
1864 #[inline(always)]
1865 pub fn push(&mut self, val: LuaValue) {
1866 let top = self.top.0 as usize;
1867 if top < self.stack.len() {
1868 self.stack[top] = StackValue { val, tbc_delta: 0 };
1869 } else {
1870 self.stack.push(StackValue { val, tbc_delta: 0 });
1871 }
1872 self.top = StackIdx(self.top.0 + 1);
1873 }
1874
1875 /// Pop the top value from the stack, decrementing `top`.
1876 ///
1877 #[inline(always)]
1878 pub fn pop(&mut self) -> LuaValue {
1879 if self.top.0 == 0 {
1880 return LuaValue::Nil;
1881 }
1882 self.top = StackIdx(self.top.0 - 1);
1883 self.stack[self.top.0 as usize].val.clone()
1884 }
1885
1886 /// Retrieve the value at the given stack index without removing it.
1887 ///
1888 /// macros.tsv: `s2v → state.stack_at(idx)` → returns `&LuaValue`
1889 #[inline(always)]
1890 pub fn stack_val(&self, idx: StackIdx) -> &LuaValue {
1891 &self.stack[idx.0 as usize].val
1892 }
1893
1894 /// Write a value to a specific stack slot.
1895 #[inline(always)]
1896 pub fn set_stack_val(&mut self, idx: StackIdx, val: LuaValue) {
1897 self.stack[idx.0 as usize].val = val;
1898 }
1899
1900 /// Returns a no-op GC handle.
1901 ///
1902 /// macros.tsv: `luaC_checkGC → state.gc().check_step()`, etc.
1903 ///
1904 /// PORT NOTE: In Phases A–C the GC is `Rc`-based and all GC operations are
1905 /// no-ops. Phase D replaces this with real GC logic in `lua-gc`.
1906 pub fn gc(&mut self) -> GcHandle<'_> {
1907 GcHandle { _state: self }
1908 }
1909
1910 /// Pin a Lua value in the external root set and return its stable key.
1911 pub fn external_root_value(&mut self, value: LuaValue) -> ExternalRootKey {
1912 self.global_mut().external_roots.insert(value)
1913 }
1914
1915 /// Read a value currently pinned by an external root key.
1916 pub fn external_rooted_value(&self, key: ExternalRootKey) -> Option<LuaValue> {
1917 self.global().external_roots.get(key).cloned()
1918 }
1919
1920 /// Replace the value pinned by an external root key.
1921 pub fn external_replace_root(
1922 &mut self,
1923 key: ExternalRootKey,
1924 value: LuaValue,
1925 ) -> Option<LuaValue> {
1926 self.global_mut().external_roots.replace(key, value)
1927 }
1928
1929 /// Remove an external root. Returns `None` for stale or already-removed keys.
1930 pub fn external_unroot_value(&mut self, key: ExternalRootKey) -> Option<LuaValue> {
1931 self.global_mut().external_roots.remove(key)
1932 }
1933
1934 /// Best-effort external root removal for destructors that may run while
1935 /// the collector holds an immutable `GlobalState` borrow.
1936 pub fn try_external_unroot_value(
1937 &mut self,
1938 key: ExternalRootKey,
1939 ) -> std::result::Result<Option<LuaValue>, std::cell::BorrowMutError> {
1940 self.global
1941 .try_borrow_mut()
1942 .map(|mut global| global.external_roots.remove(key))
1943 }
1944
1945 /// Create a new empty table and register it with the GC.
1946 ///
1947 /// macros.tsv: `lua_newtable → state.new_table()`
1948 pub fn new_table(&mut self) -> GcRef<LuaTable> {
1949 // TODO(port): register with GC tracking (state.global_mut().allgc) in Phase D
1950 self.mark_gc_check_needed();
1951 GcRef::new(LuaTable::placeholder())
1952 }
1953
1954 /// Create a fresh table with pre-sized array/hash parts.
1955 ///
1956 /// mirrors the `luaH_new` + `luaH_resize` pair in one call so we don't
1957 /// pay an extra resize path for hot construction sites.
1958 pub fn new_table_with_sizes(
1959 &mut self,
1960 array_size: u32,
1961 hash_size: u32,
1962 ) -> Result<GcRef<LuaTable>, LuaError> {
1963 self.mark_gc_check_needed();
1964 let t = GcRef::new(LuaTable::placeholder());
1965 self.table_resize(&t, array_size as usize, hash_size as usize)?;
1966 t.account_buffer(t.buffer_bytes() as isize);
1967 Ok(t)
1968 }
1969
1970 /// Intern a byte string in the global string pool.
1971 ///
1972 /// In C, short strings (≤ LUAI_MAXSHORTLEN = 40 bytes) are interned globally
1973 /// via `luaS_newlstr`, while long strings allocate a fresh TString each
1974 /// call so distinct long strings keep distinct object identity (observable
1975 /// via `string.format("%p", s)`). The parser separately deduplicates
1976 /// long-string literals within a single chunk through `luaX_newstring`'s
1977 /// `ls->h` anchor table.
1978 ///
1979 /// macros.tsv: `luaS_new → state.intern_str(s)`
1980 pub fn intern_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
1981 if bytes.len() <= crate::string::MAX_SHORT_LEN {
1982 if let Some(existing) = self.global().interned_lt.get(bytes) {
1983 return Ok(existing.clone());
1984 }
1985 self.mark_gc_check_needed();
1986 let new_ref = GcRef::new(LuaString::from_bytes(bytes.to_vec()));
1987 self.global_mut()
1988 .interned_lt
1989 .insert(bytes.to_vec().into_boxed_slice(), new_ref.clone());
1990 Ok(new_ref)
1991 } else {
1992 self.mark_gc_check_needed();
1993 let new_ref = GcRef::new(LuaString::from_bytes(bytes.to_vec()));
1994 // PORT NOTE: Phase-B byte tracking for `collectgarbage("count")`.
1995 // C-Lua's `luaC_newobj` calls `luaM_malloc`, which adds
1996 // `sizeof(TString) + len + 1` to `g->GCdebt`. Phases A–C bypass
1997 // that allocator, so without explicit accounting the Lua-visible
1998 // memory total never reflects string payload — gc.lua's
1999 // string-keys-in-weak-tables block depends on observing the >8MB
2000 // jump after allocating two 4MB strings. Short strings are
2001 // interned (bounded in size) so they are not tracked here.
2002 // `reclaim_dead_long_strings` later subtracts the size back out
2003 // when the underlying `Rc` is dropped.
2004 let size = bytes.len()
2005 + std::mem::size_of::<LuaString>()
2006 + std::mem::size_of::<usize>();
2007 let mut g = self.global_mut();
2008 g.gc_debt += size as isize;
2009 g.gc_tracked_long_strings
2010 .push((new_ref.downgrade(), size));
2011 Ok(new_ref)
2012 }
2013 }
2014
2015 /// Returns the current CallInfo index (the active call frame).
2016 #[inline(always)]
2017 pub fn top_idx(&self) -> StackIdx {
2018 self.top
2019 }
2020}
2021
2022// ─── Phase-B stub methods ─────────────────────────────────────────────────────
2023//
2024// The methods in the impl blocks below were referenced by api.rs, debug.rs,
2025// do_.rs, vm.rs, tagmethods.rs etc. during Phase A. Each body is a `todo!()`
2026// pinned to a phase-b task; once the corresponding C function is faithfully
2027// ported the stub will be replaced. Signatures are inferred from call sites
2028// and should be treated as Phase-B-grade approximations.
2029
2030impl LuaState {
2031 #[inline(always)]
2032 pub fn get_at(&self, idx: impl Into<StackIdxConv>) -> LuaValue {
2033 let i: StackIdx = idx.into().0;
2034 match self.stack.get(i.0 as usize) {
2035 Some(slot) => slot.val.clone(),
2036 None => LuaValue::Nil,
2037 }
2038 }
2039 #[inline(always)]
2040 pub fn set_at(&mut self, idx: impl Into<StackIdxConv>, v: LuaValue) {
2041 let i: StackIdx = idx.into().0;
2042 self.stack[i.0 as usize].val = v;
2043 }
2044
2045 /// Clear stack slots in `[start, end)` without changing `top`.
2046 ///
2047 /// Internal call setup reserves space up to `ci.top`; while GC tracing is
2048 /// conservative over that range, the unused tail must not retain stale
2049 /// collectable values from previous frames.
2050 pub fn clear_stack_range(&mut self, start: StackIdx, end: StackIdx) {
2051 if end.0 <= start.0 {
2052 return;
2053 }
2054 let end_u = end.0 as usize;
2055 if end_u > self.stack.len() {
2056 self.stack.resize_with(end_u, StackValue::default);
2057 }
2058 for i in start.0..end.0 {
2059 self.stack[i as usize].val = LuaValue::Nil;
2060 self.stack[i as usize].tbc_delta = 0;
2061 }
2062 }
2063 /// Hot-path accessor: returns `Some(i)` only when the stack slot at `idx`
2064 /// holds a `LuaValue::Int(i)`. Returns `None` for any other tag (including
2065 /// out-of-bounds, which behaves as `Nil`).
2066 ///
2067 /// `ttisinteger` predicate that gates the integer arithmetic fast path in
2068 /// `lvm.c`'s `op_arith_aux` macro. Avoids the full `LuaValue` clone that
2069 /// `get_at` performs — the operand is only needed for its `i64` payload.
2070 #[inline(always)]
2071 pub fn get_int_at(&self, idx: impl Into<StackIdxConv>) -> Option<i64> {
2072 let i: StackIdx = idx.into().0;
2073 match self.stack.get(i.0 as usize) {
2074 Some(slot) => match &slot.val {
2075 LuaValue::Int(v) => Some(*v),
2076 _ => None,
2077 },
2078 None => None,
2079 }
2080 }
2081 /// Hot-path accessor: returns `Some((a, b))` only when both stack slots
2082 /// at `rb` and `rc` hold integers. Equivalent to two `get_int_at` calls
2083 /// but is shaped so the arithmetic opcode dispatch arms can pattern-match
2084 /// the common case with a single `if let`.
2085 ///
2086 /// the `op_arith_aux` macro.
2087 #[inline(always)]
2088 pub fn get_int_pair_at(
2089 &self,
2090 rb: impl Into<StackIdxConv>,
2091 rc: impl Into<StackIdxConv>,
2092 ) -> Option<(i64, i64)> {
2093 let rb: StackIdx = rb.into().0;
2094 let rc: StackIdx = rc.into().0;
2095 match (
2096 self.stack[rb.0 as usize].val,
2097 self.stack[rc.0 as usize].val,
2098 ) {
2099 (LuaValue::Int(ib), LuaValue::Int(ic)) => Some((ib, ic)),
2100 _ => None,
2101 }
2102 }
2103 /// Hot-path accessor: returns `Some(f)` when the slot holds a `Float(f)`
2104 /// or coerces an `Int(i)` to `f64`. Returns `None` for any other tag.
2105 /// No `LuaValue` clone — only the primitive payload travels back.
2106 ///
2107 #[inline(always)]
2108 pub fn get_num_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
2109 let i: StackIdx = idx.into().0;
2110 match self.stack.get(i.0 as usize) {
2111 Some(slot) => match &slot.val {
2112 LuaValue::Float(f) => Some(*f),
2113 LuaValue::Int(v) => Some(*v as f64),
2114 _ => None,
2115 },
2116 None => None,
2117 }
2118 }
2119 /// Hot-path accessor: returns `Some(f)` only when the slot holds a
2120 /// `LuaValue::Float(f)`. Does NOT coerce integers; the integer branch is
2121 /// the caller's responsibility. Used by opcode arms that have already
2122 /// ruled out the integer fast path.
2123 #[inline(always)]
2124 pub fn get_float_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
2125 let i: StackIdx = idx.into().0;
2126 match self.stack.get(i.0 as usize) {
2127 Some(slot) => match &slot.val {
2128 LuaValue::Float(f) => Some(*f),
2129 _ => None,
2130 },
2131 None => None,
2132 }
2133 }
2134 /// Hot-path accessor: pair version of `get_num_at` — returns `Some((a,b))`
2135 /// when both slots coerce to `f64` (Float or Int), `None` if either does
2136 /// not. Used by the float fast path of the arith opcodes.
2137 ///
2138 #[inline(always)]
2139 pub fn get_num_pair_at(
2140 &self,
2141 rb: impl Into<StackIdxConv>,
2142 rc: impl Into<StackIdxConv>,
2143 ) -> Option<(f64, f64)> {
2144 let rb: StackIdx = rb.into().0;
2145 let rc: StackIdx = rc.into().0;
2146 match (
2147 self.stack[rb.0 as usize].val,
2148 self.stack[rc.0 as usize].val,
2149 ) {
2150 (LuaValue::Float(nb), LuaValue::Float(nc)) => Some((nb, nc)),
2151 (LuaValue::Int(ib), LuaValue::Int(ic)) => Some((ib as f64, ic as f64)),
2152 (LuaValue::Int(ib), LuaValue::Float(nc)) => Some((ib as f64, nc)),
2153 (LuaValue::Float(nb), LuaValue::Int(ic)) => Some((nb, ic as f64)),
2154 _ => None,
2155 }
2156 }
2157 /// Set `top` to an absolute stack index. Grows the backing stack vector
2158 /// (filling new slots with `Nil`) when `idx` is past `stack.len()`, but
2159 /// never clobbers existing slots between the old top and the new top —
2160 /// VM opcodes (Call, ForPrep, etc.) write registers via `set_at` and then
2161 /// raise `top` to signal "these are now live"; nil-filling here would
2162 /// erase the just-written values.
2163 ///
2164 /// setnilvalue(s2v(L->top.p++))` clear loop in `lua_settop` (lapi.c) is
2165 /// part of the public API path and lives in `api::set_top` instead.
2166 /// PORT NOTE: callers pass an absolute `StackIdx`, not the relative `idx`
2167 /// of the public `lua_settop`. The to-be-closed (`tbclist`) close path
2168 /// is Phase E and not handled here.
2169 #[inline(always)]
2170 pub fn set_top(&mut self, idx: impl Into<StackIdxConv>) {
2171 let new_top: StackIdx = idx.into().0;
2172 let new_top_u = new_top.0 as usize;
2173 if new_top_u > self.stack.len() {
2174 self.stack.resize_with(new_top_u, StackValue::default);
2175 }
2176 self.top = new_top;
2177 }
2178 /// Primitive "set top index" — just writes `self.top`, no nil-fill.
2179 ///
2180 /// PORT NOTE: callers (`api.rs::set_top`, `raw_set`, etc.) pre-nil-fill or
2181 /// only shrink, so this routine intentionally does no clearing or resizing.
2182 /// The to-be-closed (`tbclist`) close path is Phase E.
2183 #[inline(always)]
2184 pub fn set_top_idx(&mut self, idx: impl Into<StackIdxConv>) {
2185 let new_top: StackIdx = idx.into().0;
2186 self.top = new_top;
2187 }
2188 /// Decrement `top` by 1 (saturating at zero).
2189 ///
2190 #[inline(always)]
2191 pub fn dec_top(&mut self) {
2192 if self.top.0 > 0 {
2193 self.top = StackIdx(self.top.0 - 1);
2194 }
2195 }
2196 #[inline(always)]
2197 pub fn pop_n(&mut self, n: usize) {
2198 let cur = self.top.0 as usize;
2199 let new = cur.saturating_sub(n);
2200 self.top = StackIdx(new as u32);
2201 }
2202 /// Returns the value at the given stack index without removing it.
2203 ///
2204 #[inline(always)]
2205 pub fn peek_at(&mut self, idx: impl Into<StackIdxConv>) -> LuaValue {
2206 let i: StackIdx = idx.into().0;
2207 match self.stack.get(i.0 as usize) {
2208 Some(slot) => slot.val.clone(),
2209 None => LuaValue::Nil,
2210 }
2211 }
2212 /// Returns the value just below `top` (the topmost live slot) without
2213 /// removing it.
2214 ///
2215 #[inline(always)]
2216 pub fn peek_top(&mut self) -> LuaValue {
2217 if self.top.0 == 0 {
2218 return LuaValue::Nil;
2219 }
2220 self.stack[(self.top.0 - 1) as usize].val.clone()
2221 }
2222 /// Returns the topmost slot interpreted as a string. Panics if the slot
2223 /// is not a `LuaValue::Str`. Callers (e.g. `luaO_pushvfstring`) guarantee
2224 /// the value has been pushed as an interned string immediately prior.
2225 ///
2226 pub fn peek_string_at_top(&mut self) -> GcRef<LuaString> {
2227 match self.peek_top() {
2228 LuaValue::Str(s) => s,
2229 _ => panic!("peek_string_at_top: top of stack is not a string"),
2230 }
2231 }
2232 /// Mutable reference to the value at the given stack slot.
2233 ///
2234 pub fn stack_at(&mut self, idx: impl Into<StackIdxConv>) -> &mut LuaValue {
2235 let i: StackIdx = idx.into().0;
2236 &mut self.stack[i.0 as usize].val
2237 }
2238 /// Writes `Nil` to the given stack slot.
2239 ///
2240 pub fn stack_set_nil(&mut self, idx: impl Into<StackIdxConv>) {
2241 let i: StackIdx = idx.into().0;
2242 let slot = i.0 as usize;
2243 if slot < self.stack.len() {
2244 self.stack[slot].val = LuaValue::Nil;
2245 }
2246 }
2247 /// Resizes the underlying stack vector to `size` slots, padding new slots
2248 /// with `StackValue::default()` (which is `Nil`). Returns `Ok(())` on
2249 /// success — `Vec::resize_with` in Rust does not have a fallible path the
2250 /// way `luaM_reallocvector` does in C, so the `Result` is here for
2251 /// signature parity with future fallible allocators.
2252 ///
2253 /// newsize+EXTRA_STACK, StackValue)`.
2254 pub fn stack_resize(&mut self, size: usize) -> Result<(), LuaError> {
2255 self.stack.resize_with(size, StackValue::default);
2256 Ok(())
2257 }
2258 pub fn stack_available(&mut self) -> usize {
2259 (self.stack_last.0 as usize).saturating_sub(self.top.0 as usize)
2260 }
2261 pub fn check_stack(&mut self, n: i32) -> Result<(), LuaError> {
2262 let free = (self.stack_last.0 as i32) - (self.top.0 as i32);
2263 if free <= n {
2264 self.grow_stack(n, true)?;
2265 }
2266 Ok(())
2267 }
2268 /// Inherent method wrapper around the free function `do_::grow_stack`,
2269 /// preserving the historical `Result<(), LuaError>` signature used by
2270 /// `check_stack` and other VM call sites. The bool returned by the
2271 /// underlying implementation distinguishes soft failure (when
2272 /// `raise_error` is false) from success; that distinction is dropped here
2273 /// because every current caller passes `raise_error = true` and only
2274 /// cares about error propagation.
2275 ///
2276 pub fn grow_stack(&mut self, n: i32, raise_error: bool) -> Result<(), LuaError> {
2277 crate::do_::grow_stack(self, n, raise_error).map(|_| ())
2278 }
2279
2280 #[inline(always)]
2281 pub fn get_ci(&self, idx: CallInfoIdx) -> &CallInfo { &self.call_info[idx.as_usize()] }
2282 #[inline(always)]
2283 pub fn get_ci_mut(&mut self, idx: CallInfoIdx) -> &mut CallInfo { &mut self.call_info[idx.as_usize()] }
2284 #[inline(always)]
2285 pub fn current_call_info(&self) -> &CallInfo { &self.call_info[self.ci.as_usize()] }
2286 #[inline(always)]
2287 pub fn current_call_info_mut(&mut self) -> &mut CallInfo { let i = self.ci.as_usize(); &mut self.call_info[i] }
2288 #[inline(always)]
2289 pub fn current_ci_idx(&self) -> CallInfoIdx { self.ci }
2290 pub fn call_stack_mut(&mut self) -> &mut Vec<CallInfo> { &mut self.call_info }
2291 #[inline(always)]
2292 pub fn next_ci(&mut self) -> Result<CallInfoIdx, LuaError> {
2293 match self.call_info[self.ci.as_usize()].next {
2294 Some(idx) => Ok(idx),
2295 None => Ok(extend_ci(self)),
2296 }
2297 }
2298 #[inline(always)]
2299 pub fn prev_ci(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> { self.call_info[idx.as_usize()].previous }
2300 pub fn get_prev_ci(&self, idx: CallInfoIdx) -> Option<&CallInfo> {
2301 self.call_info[idx.as_usize()]
2302 .previous
2303 .map(|p| &self.call_info[p.as_usize()])
2304 }
2305 #[inline(always)]
2306 pub fn is_base_ci(&self, idx: CallInfoIdx) -> bool { idx.as_usize() == 0 }
2307 #[inline(always)]
2308 pub fn is_current_ci(&self, idx: CallInfoIdx) -> bool { idx == self.ci }
2309 pub fn ci_next_func(&self, idx: CallInfoIdx) -> StackIdx {
2310 let next = self.call_info[idx.as_usize()]
2311 .next
2312 .expect("ci_next_func: no next CallInfo");
2313 self.call_info[next.as_usize()].func
2314 }
2315 #[inline(always)]
2316 pub fn ci_top(&self, idx: CallInfoIdx) -> StackIdx { self.call_info[idx.as_usize()].top }
2317 #[inline(always)]
2318 pub fn ci_trap(&mut self, idx: CallInfoIdx) -> bool {
2319 if let CallInfoFrame::Lua { trap, .. } = self.call_info[idx.as_usize()].u {
2320 trap
2321 } else {
2322 false
2323 }
2324 }
2325 #[inline(always)]
2326 pub fn ci_savedpc(&self, idx: CallInfoIdx) -> u32 { self.call_info[idx.as_usize()].saved_pc() }
2327 #[inline(always)]
2328 pub fn set_ci_savedpc(&mut self, idx: CallInfoIdx, pc: u32) {
2329 self.call_info[idx.as_usize()].set_saved_pc(pc);
2330 }
2331 #[inline(always)]
2332 pub fn set_ci_previous(&mut self, idx: CallInfoIdx) {
2333 self.ci = self.call_info[idx.as_usize()]
2334 .previous
2335 .expect("set_ci_previous: returning frame has no previous CallInfo");
2336 }
2337 #[inline(always)]
2338 pub fn ci_previous(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> { self.call_info[idx.as_usize()].previous }
2339 #[inline(always)]
2340 pub fn ci_adjust_func(&mut self, idx: CallInfoIdx, delta: i32) {
2341 let ci = &mut self.call_info[idx.as_usize()];
2342 ci.func = StackIdx((ci.func.0 as i32 - delta) as u32);
2343 }
2344 #[inline(always)]
2345 pub fn ci_base(&self, idx: CallInfoIdx) -> StackIdx { self.call_info[idx.as_usize()].func + 1 }
2346 #[inline(always)]
2347 pub fn ci_is_fresh(&self, idx: CallInfoIdx) -> bool {
2348 (self.call_info[idx.as_usize()].callstatus & CIST_FRESH) != 0
2349 }
2350 #[inline(always)]
2351 pub fn ci_lua_closure(&self, idx: CallInfoIdx) -> Option<GcRef<lua_types::closure::LuaLClosure>> {
2352 let func_idx = self.call_info[idx.as_usize()].func;
2353 match self.get_at(func_idx) {
2354 LuaValue::Function(lua_types::closure::LuaClosure::Lua(cl)) => Some(cl),
2355 _ => None,
2356 }
2357 }
2358 #[inline(always)]
2359 pub fn ci_nextraargs(&self, idx: CallInfoIdx) -> i32 {
2360 self.call_info[idx.as_usize()].nextra_args()
2361 }
2362 #[inline(always)]
2363 pub fn ci_nres(&self, idx: CallInfoIdx) -> i32 {
2364 self.call_info[idx.as_usize()].u2.value
2365 }
2366 #[inline(always)]
2367 pub fn ci_nres_set(&mut self, idx: CallInfoIdx, n: i32) {
2368 self.call_info[idx.as_usize()].u2.value = n;
2369 }
2370 #[inline(always)]
2371 pub fn ci_nresults(&self, idx: CallInfoIdx) -> i32 { self.call_info[idx.as_usize()].nresults as i32 }
2372 pub fn ci_prev_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
2373 let pc = self.call_info[idx.as_usize()].saved_pc();
2374 let cl = self.ci_lua_closure(idx)
2375 .expect("ci_prev_instruction: CallInfo does not hold a Lua closure");
2376 cl.proto.code[(pc - 1) as usize]
2377 }
2378 pub fn ci_prev2_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
2379 let pc = self.call_info[idx.as_usize()].saved_pc();
2380 let cl = self.ci_lua_closure(idx)
2381 .expect("ci_prev2_instruction: CallInfo does not hold a Lua closure");
2382 cl.proto.code[(pc - 2) as usize]
2383 }
2384 pub fn ci_skip_next_instruction(&mut self, idx: CallInfoIdx) {
2385 let pc = self.call_info[idx.as_usize()].saved_pc();
2386 self.call_info[idx.as_usize()].set_saved_pc(pc + 1);
2387 }
2388 pub fn ci_step_pc_back(&mut self, idx: CallInfoIdx) {
2389 let pc = self.call_info[idx.as_usize()].saved_pc();
2390 self.call_info[idx.as_usize()].set_saved_pc(pc - 1);
2391 }
2392 pub fn get_ci_pcrel(&mut self, idx: CallInfoIdx) -> u32 {
2393 self.call_info[idx.as_usize()].saved_pc().saturating_sub(1)
2394 }
2395 pub fn get_ci_u2_funcidx(&mut self, idx: CallInfoIdx) -> i32 {
2396 self.call_info[idx.as_usize()].u2.value
2397 }
2398 pub fn get_ci_u2_nres(&mut self, idx: CallInfoIdx) -> i32 {
2399 self.call_info[idx.as_usize()].u2.value
2400 }
2401 pub fn get_ci_u2_nyield(&mut self, idx: CallInfoIdx) -> i32 {
2402 self.call_info[idx.as_usize()].u2.value
2403 }
2404 pub fn get_ci_vararg_info(&mut self, idx: CallInfoIdx) -> (bool, i32, i32) {
2405 let nextraargs = self.call_info[idx.as_usize()].nextra_args();
2406 match self.ci_lua_closure(idx) {
2407 Some(cl) => (cl.proto.is_vararg, nextraargs, cl.proto.numparams as i32),
2408 None => (false, nextraargs, 0),
2409 }
2410 }
2411 pub fn get_ci_lua_proto_numparams(&mut self, idx: CallInfoIdx) -> u8 {
2412 self.ci_lua_closure(idx)
2413 .map(|cl| cl.proto.numparams)
2414 .unwrap_or(0)
2415 }
2416 pub fn set_ci_u2_nres(&mut self, idx: CallInfoIdx, n: i32) {
2417 self.call_info[idx.as_usize()].u2.value = n;
2418 }
2419 pub fn set_ci_u2_nyield(&mut self, idx: CallInfoIdx, n: i32) {
2420 self.call_info[idx.as_usize()].u2.value = n;
2421 }
2422 pub fn set_ci_transfer_info(&mut self, idx: CallInfoIdx, ftransfer: u16, ntransfer: u16) {
2423 let ci = &mut self.call_info[idx.as_usize()];
2424 ci.u2.ftransfer = ftransfer;
2425 ci.u2.ntransfer = ntransfer;
2426 }
2427 pub fn shrink_ci(&mut self) { shrink_ci(self) }
2428 pub fn check_c_stack(&mut self) -> Result<(), LuaError> { check_c_stack(self) }
2429
2430 pub fn status(&mut self) -> LuaStatus { LuaStatus::from_raw(self.status as i32) }
2431 pub fn errfunc(&mut self) -> isize { self.errfunc }
2432 pub fn old_pc(&mut self) -> u32 { self.oldpc }
2433 pub fn set_old_pc(&mut self, pc: u32) { self.oldpc = pc; }
2434 pub fn set_oldpc(&mut self, pc: u32) { self.oldpc = pc; }
2435 pub fn _hook_call_noargs(&mut self) {}
2436 pub fn hook(&self) -> Option<&Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>> {
2437 self.hook.as_ref()
2438 }
2439 pub fn has_hook(&mut self) -> bool { self.hook.is_some() }
2440 pub fn hook_count(&mut self) -> i32 { self.hookcount }
2441 pub fn set_hook_count(&mut self, n: i32) { self.hookcount = n; }
2442 pub fn hook_mask(&self) -> u8 { self.hookmask }
2443 pub fn set_hook_mask(&mut self, m: u8) { self.hookmask = m; }
2444 pub fn base_hook_count(&self) -> i32 { self.basehookcount }
2445 pub fn set_base_hook_count(&mut self, n: i32) { self.basehookcount = n; }
2446 pub fn set_hook(&mut self, h: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>) {
2447 self.hook = h;
2448 }
2449 pub fn call_hook_event(&mut self, event: i32, line: i32) -> Result<(), LuaError> {
2450 crate::do_::hook(self, event, line, 0, 0)
2451 }
2452
2453 pub fn registry_value(&self) -> LuaValue { self.global().l_registry.clone() }
2454 pub fn registry_get(&self, key: usize) -> LuaValue {
2455 let reg = self.global().l_registry.clone();
2456 match reg {
2457 LuaValue::Table(t) => t.get(&LuaValue::Int(key as i64)),
2458 _ => LuaValue::Nil,
2459 }
2460 }
2461
2462 pub fn new_string(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> { self.intern_or_create_str(bytes) }
2463
2464 // ── Phase D-1a: state-owned allocation API ──────────────────────────────
2465 // These methods are the canonical allocation surface. They wrap
2466 // `GcRef::new` today; at D-1e they route through `state.global.heap.allocate`.
2467 // Callers must reach them through `&mut LuaState`, which mirrors C-Lua's
2468 // requirement that every allocation passes `lua_State *L`.
2469
2470 /// Allocate a new Lua function prototype.
2471 ///
2472 /// Caller mutates the returned proto in place (it's behind GcRef, which is
2473 /// Rc during Phase D-1; mutable access via `Rc::get_mut` only works while
2474 /// no other GcRefs alias it — true at construction).
2475 pub fn new_proto(&mut self) -> GcRef<LuaProto> {
2476 self.mark_gc_check_needed();
2477 GcRef::new(LuaProto::placeholder())
2478 }
2479
2480 /// Allocate a Lua-side closure (compiled function + upvalue slots).
2481 pub fn new_lclosure(&mut self, proto: GcRef<LuaProto>, nupvals: usize) -> GcRef<LuaClosureLua> {
2482 self.mark_gc_check_needed();
2483 let mut upvals = Vec::with_capacity(nupvals);
2484 for _ in 0..nupvals {
2485 upvals.push(std::cell::Cell::new(self.new_upval_closed(LuaValue::Nil)));
2486 }
2487 GcRef::new(LuaClosureLua { proto, upvals })
2488 }
2489
2490 /// Allocate a closed upvalue holding the given value.
2491 pub fn new_upval_closed(&mut self, v: LuaValue) -> GcRef<UpVal> {
2492 self.mark_gc_check_needed();
2493 GcRef::new(UpVal::closed(v))
2494 }
2495
2496 /// Allocate an open upvalue referring to a thread's stack slot.
2497 pub fn new_upval_open(&mut self, thread_id: usize, level: StackIdx) -> GcRef<UpVal> {
2498 self.mark_gc_check_needed();
2499 GcRef::new(UpVal::open(thread_id, level))
2500 }
2501 /// Mirrors `luaS_newlstr`: short strings are interned globally so equal
2502 /// content shares a single TString; long strings (> LUAI_MAXSHORTLEN = 40)
2503 /// always create a fresh TString without interning. This is what lets
2504 /// `string.format("%p", "long" .. "concat")` differ from a same-content
2505 /// literal — concat must produce a new object even when the literal already
2506 /// lives in the lexer's constant pool.
2507 pub fn intern_or_create_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
2508 self.intern_str(bytes)
2509 }
2510 pub fn new_userdata(&mut self, _size: usize, _nuvalue: usize) -> Result<GcRef<LuaUserData>, LuaError> {
2511 Err(LuaError::runtime(format_args!("new_userdata not implemented in this Phase-B build; use new_userdata_typed instead")))
2512 }
2513 pub fn new_c_closure(&mut self, _f: LuaCFunction, _n: i32) -> Result<LuaClosure, LuaError> {
2514 Err(LuaError::runtime(format_args!("new_c_closure not implemented in this Phase-B build; use push_cclosure in lua_vm::api instead")))
2515 }
2516 pub fn push_closure(
2517 &mut self,
2518 proto_idx: usize,
2519 ci: CallInfoIdx,
2520 base: StackIdx,
2521 ra: StackIdx,
2522 ) -> Result<(), LuaError> {
2523 let parent_cl = self.ci_lua_closure(ci).expect(
2524 "push_closure: current frame is not a Lua closure",
2525 );
2526 let child_proto = parent_cl.proto.p[proto_idx].clone();
2527 let nup = child_proto.upvalues.len();
2528 let mut upvals: Vec<std::cell::Cell<GcRef<UpVal>>> = Vec::with_capacity(nup);
2529 for i in 0..nup {
2530 let desc = &child_proto.upvalues[i];
2531 let uv = if desc.instack {
2532 let level = base + desc.idx as i32;
2533 crate::func::find_upval(self, level)
2534 } else {
2535 parent_cl.upval(desc.idx as usize)
2536 };
2537 upvals.push(std::cell::Cell::new(uv));
2538 }
2539 // TODO(D-1c-bridge): upvals are pre-populated from parent frame; state.new_lclosure
2540 // fills with fresh Nil upvals which would drop the captured bindings.
2541 self.mark_gc_check_needed();
2542 let new_cl = GcRef::new(LuaClosureLua {
2543 proto: child_proto,
2544 upvals,
2545 });
2546 self.set_at(ra, LuaValue::Function(LuaClosure::Lua(new_cl)));
2547 Ok(())
2548 }
2549 pub fn new_tbc_upval(&mut self, idx: StackIdx) -> Result<(), LuaError> {
2550 crate::func::new_tbc_upval(self, idx)
2551 }
2552
2553 /// Read an open or closed upvalue.
2554 ///
2555 /// Closed upvalues own their value and read trivially. Open upvalues
2556 /// point at a stack slot on the home thread that captured them.
2557 ///
2558 /// Resolution order for an open upvalue whose home is not the current
2559 /// thread:
2560 ///
2561 /// 1. If the home thread is registered in `GlobalState::threads` and
2562 /// its `RefCell` is currently borrowable, read straight from its
2563 /// stack. This is the path used when the main thread reads a
2564 /// closure created inside a now-suspended coroutine, or when one
2565 /// coroutine reads an upvalue homed on a sibling suspended
2566 /// coroutine.
2567 /// 2. Otherwise fall back to `GlobalState::cross_thread_upvals`. This
2568 /// is the path used while inside a `coroutine.resume`: the parent
2569 /// thread's `LuaState` is held by an outer `&mut` and is not
2570 /// reachable through any `Rc<RefCell<_>>`, so `aux_resume`
2571 /// snapshots the parent's open upvalues into the mirror across the
2572 /// resume boundary.
2573 #[inline(always)]
2574 pub fn upvalue_get(&self, cl: &GcRef<LuaClosureLua>, n: usize) -> LuaValue {
2575 let uv = cl.upval(n);
2576 let (thread_id, idx) = match uv.try_open_payload() {
2577 Some(p) => p,
2578 None => return *uv.closed_value(),
2579 };
2580 let current = self.cached_thread_id;
2581 let tid = thread_id as u64;
2582 if tid == current {
2583 return self.stack[idx.0 as usize].val;
2584 }
2585 self.upvalue_get_cross_thread(tid, idx)
2586 }
2587
2588 #[cold]
2589 #[inline(never)]
2590 fn upvalue_get_cross_thread(&self, tid: u64, idx: StackIdx) -> LuaValue {
2591 let entry_rc = {
2592 let g = self.global();
2593 g.threads.get(&tid).map(|e| e.state.clone())
2594 };
2595 if let Some(rc) = entry_rc {
2596 if let Ok(home_state) = rc.try_borrow() {
2597 return home_state.get_at(idx);
2598 }
2599 }
2600 let g = self.global();
2601 match g.cross_thread_upvals.get(&(tid, idx)) {
2602 Some(v) => *v,
2603 None => LuaValue::Nil,
2604 }
2605 }
2606 /// Write an open or closed upvalue.
2607 ///
2608 /// Mirrors [`upvalue_get`]: open upvalues homed on the current thread
2609 /// write through `self.stack`. For cross-thread open upvalues, the
2610 /// home thread's stack is written directly when its `RefCell` is
2611 /// borrowable, otherwise the write lands in
2612 /// `GlobalState::cross_thread_upvals` (the active-resume case where
2613 /// the home thread is borrow-locked further up the call stack).
2614 #[inline(always)]
2615 pub fn upvalue_set(&mut self, cl: &GcRef<LuaClosureLua>, n: usize, val: LuaValue) -> Result<(), LuaError> {
2616 let uv = cl.upval(n);
2617 match uv.try_open_payload() {
2618 Some((thread_id, idx)) => {
2619 let tid = thread_id as u64;
2620 let current = self.cached_thread_id;
2621 if tid == current {
2622 self.stack[idx.0 as usize].val = val;
2623 return Ok(());
2624 }
2625 return self.upvalue_set_cross_thread(tid, idx, val);
2626 }
2627 None => {
2628 uv.set_closed_value(val);
2629 }
2630 }
2631 Ok(())
2632 }
2633
2634 #[cold]
2635 #[inline(never)]
2636 fn upvalue_set_cross_thread(
2637 &mut self,
2638 tid: u64,
2639 idx: StackIdx,
2640 val: LuaValue,
2641 ) -> Result<(), LuaError> {
2642 let entry_rc = {
2643 let g = self.global();
2644 g.threads.get(&tid).map(|e| e.state.clone())
2645 };
2646 if let Some(rc) = entry_rc {
2647 if let Ok(mut home_state) = rc.try_borrow_mut() {
2648 home_state.set_at(idx, val);
2649 return Ok(());
2650 }
2651 }
2652 let mut g = self.global_mut();
2653 g.cross_thread_upvals.insert((tid, idx), val);
2654 Ok(())
2655 }
2656
2657 pub fn protected_call_raw(&mut self, func: StackIdx, nresults: i32, errfunc: StackIdx) -> Result<(), LuaError> {
2658 let ef = errfunc.0 as isize;
2659 let status = crate::do_::pcall(
2660 self,
2661 |s| s.call_no_yield(func, nresults),
2662 func,
2663 ef,
2664 );
2665 match status {
2666 LuaStatus::Ok => Ok(()),
2667 LuaStatus::ErrSyntax => {
2668 let err_val = self.get_at(func);
2669 self.set_top(func);
2670 Err(LuaError::Syntax(err_val))
2671 }
2672 LuaStatus::Yield => {
2673 self.set_top(func);
2674 Err(LuaError::Yield)
2675 }
2676 _ => {
2677 let err_val = self.get_at(func);
2678 self.set_top(func);
2679 Err(LuaError::Runtime(err_val))
2680 }
2681 }
2682 }
2683 pub fn protected_parser(&mut self, z: crate::zio::ZIO, name: &[u8], mode: Option<&[u8]>) -> LuaStatus {
2684 crate::do_::protected_parser(self, z, name, mode)
2685 }
2686 pub fn do_call(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2687 crate::do_::call(self, func, nresults)
2688 }
2689 pub fn do_call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2690 crate::do_::callnoyield(self, func, nresults)
2691 }
2692 pub fn call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2693 crate::do_::callnoyield(self, func, nresults)
2694 }
2695 pub fn call_at(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2696 crate::do_::call(self, func, nresults)
2697 }
2698 #[inline(always)]
2699 pub fn precall(&mut self, func: StackIdx, nresults: i32) -> Result<Option<CallInfoIdx>, LuaError> {
2700 crate::do_::precall(self, func, nresults)
2701 }
2702 #[inline(always)]
2703 pub fn pretailcall(
2704 &mut self,
2705 ci: CallInfoIdx,
2706 func: StackIdx,
2707 narg1: i32,
2708 delta: i32,
2709 ) -> Result<i32, LuaError> {
2710 crate::do_::pretailcall(self, ci, func, narg1, delta)
2711 }
2712 #[inline(always)]
2713 pub fn poscall<N: TryInto<i32>>(&mut self, ci: CallInfoIdx, nres: N) -> Result<(), LuaError>
2714 where
2715 <N as TryInto<i32>>::Error: std::fmt::Debug,
2716 {
2717 let n = nres.try_into().expect("poscall: nres out of i32 range");
2718 crate::do_::poscall(self, ci, n)
2719 }
2720 pub fn adjust_results(&mut self, nresults: i32) {
2721 const LUA_MULTRET: i32 = -1;
2722 if nresults <= LUA_MULTRET {
2723 let ci_idx = self.ci.as_usize();
2724 if self.call_info[ci_idx].top.0 < self.top.0 {
2725 self.call_info[ci_idx].top = self.top;
2726 }
2727 }
2728 }
2729 pub fn adjust_varargs(
2730 &mut self,
2731 ci: CallInfoIdx,
2732 nfixparams: i32,
2733 cl: &GcRef<lua_types::closure::LuaLClosure>,
2734 ) -> Result<(), LuaError> {
2735 crate::tagmethods::adjust_varargs(self, nfixparams, ci, &cl.0.proto)
2736 }
2737 pub fn get_varargs(
2738 &mut self,
2739 ci: CallInfoIdx,
2740 ra: StackIdx,
2741 n: i32,
2742 ) -> Result<i32, LuaError> {
2743 crate::tagmethods::get_varargs(self, ci, ra, n)?;
2744 Ok(0)
2745 }
2746
2747 pub fn close_upvals(&mut self, level: StackIdx) -> Result<(), LuaError> {
2748 crate::func::close_upval(self, level);
2749 Ok(())
2750 }
2751 pub fn close_upvals_status(&mut self, level: StackIdx, _status: i32) -> Result<(), LuaError> {
2752 crate::func::close_upval(self, level);
2753 Ok(())
2754 }
2755 pub fn close_upvals_from_base(&mut self, ci: CallInfoIdx) -> Result<(), LuaError> {
2756 let base = self.ci_base(ci);
2757 crate::func::close_upval(self, base);
2758 Ok(())
2759 }
2760
2761 pub fn arith_op(&mut self, op: i32, p1: &LuaValue, p2: &LuaValue) -> Result<LuaValue, LuaError> {
2762 let arith_op = match op {
2763 0 => lua_types::arith::ArithOp::Add,
2764 1 => lua_types::arith::ArithOp::Sub,
2765 2 => lua_types::arith::ArithOp::Mul,
2766 3 => lua_types::arith::ArithOp::Mod,
2767 4 => lua_types::arith::ArithOp::Pow,
2768 5 => lua_types::arith::ArithOp::Div,
2769 6 => lua_types::arith::ArithOp::Idiv,
2770 7 => lua_types::arith::ArithOp::Band,
2771 8 => lua_types::arith::ArithOp::Bor,
2772 9 => lua_types::arith::ArithOp::Bxor,
2773 10 => lua_types::arith::ArithOp::Shl,
2774 11 => lua_types::arith::ArithOp::Shr,
2775 12 => lua_types::arith::ArithOp::Unm,
2776 13 => lua_types::arith::ArithOp::Bnot,
2777 _ => return Err(LuaError::runtime(format_args!("invalid arith op {}", op))),
2778 };
2779 let mut res = LuaValue::Nil;
2780 if crate::object::raw_arith(self, arith_op, p1, p2, &mut res)? {
2781 Ok(res)
2782 } else {
2783 Err(LuaError::arith_error(p1, p2, "perform arithmetic on"))
2784 }
2785 }
2786 pub fn concat(&mut self, n: i32) -> Result<(), LuaError> {
2787 crate::vm::concat(self, n)
2788 }
2789 pub fn less_than(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2790 crate::vm::less_than(self, l, r)
2791 }
2792 pub fn less_equal(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2793 crate::vm::less_equal(self, l, r)
2794 }
2795 pub fn equal_obj(&self, _ctx: Option<&LuaValue>, l: &LuaValue, r: &LuaValue) -> bool {
2796 crate::vm::equal_obj(None, l, r).unwrap_or(false)
2797 }
2798 pub fn equal_obj_with_tm(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2799 crate::vm::equal_obj(Some(self), l, r)
2800 }
2801 pub fn obj_len(&mut self, v: &LuaValue) -> Result<LuaValue, LuaError> {
2802 match v {
2803 LuaValue::Table(_) => {
2804 let mt = self.table_metatable(v);
2805 let tm = self.fast_tm_table(mt.as_ref(), TagMethod::Len);
2806 if matches!(tm, LuaValue::Nil) {
2807 let n = self.table_length(v)?;
2808 return Ok(LuaValue::Int(n));
2809 }
2810 self.push(LuaValue::Nil);
2811 let slot = StackIdx(self.top.0 - 1);
2812 crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
2813 Ok(self.pop())
2814 }
2815 LuaValue::Str(s) => Ok(LuaValue::Int(s.len() as i64)),
2816 other => {
2817 let tm = crate::tagmethods::get_tm_by_obj(self, other, crate::tagmethods::TagMethod::Len);
2818 if matches!(tm, LuaValue::Nil) {
2819 let mut msg = b"attempt to get length of a ".to_vec();
2820 msg.extend_from_slice(&self.obj_type_name(other));
2821 msg.extend_from_slice(b" value");
2822 return Err(crate::debug::prefixed_runtime_pub(self, msg));
2823 }
2824 self.push(LuaValue::Nil);
2825 let slot = StackIdx(self.top.0 - 1);
2826 crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
2827 Ok(self.pop())
2828 }
2829 }
2830 }
2831 pub fn obj_to_string(&mut self, idx: i32) -> Result<GcRef<LuaString>, LuaError> {
2832 let slot: StackIdx = if idx > 0 {
2833 let ci_func = self.current_call_info().func;
2834 ci_func + idx
2835 } else {
2836 debug_assert!(idx != 0, "invalid index");
2837 StackIdx((self.top_idx().0 as i32 + idx) as u32)
2838 };
2839 let val = self.get_at(slot);
2840 match val {
2841 LuaValue::Str(s) => Ok(s),
2842 LuaValue::Int(_) | LuaValue::Float(_) => {
2843 let s = crate::object::num_to_string(self, &val)?;
2844 self.set_at(slot, LuaValue::Str(s.clone()));
2845 Ok(s)
2846 }
2847 _ => Err(LuaError::type_error(&val, "convert to string")),
2848 }
2849 }
2850 pub fn coerce_to_string(&mut self, idx: StackIdx) -> Result<GcRef<LuaString>, LuaError> {
2851 let val = self.get_at(idx);
2852 match val {
2853 LuaValue::Str(s) => Ok(s),
2854 LuaValue::Int(_) | LuaValue::Float(_) => {
2855 let s = crate::object::num_to_string(self, &val)?;
2856 self.set_at(idx, LuaValue::Str(s.clone()));
2857 Ok(s)
2858 }
2859 _ => Err(LuaError::type_error(&val, "convert to string")),
2860 }
2861 }
2862 pub fn str_to_num(&mut self, s: &[u8]) -> Option<(LuaValue, usize)> {
2863 let mut out = LuaValue::Nil;
2864 let sz = crate::object::str2num(s, &mut out);
2865 if sz == 0 { None } else { Some((out, sz)) }
2866 }
2867
2868 pub fn fast_get(&mut self, t: &LuaValue, k: &LuaValue) -> Result<Option<LuaValue>, LuaError> {
2869 let LuaValue::Table(tbl) = t else { return Ok(None); };
2870 let v = tbl.get(k);
2871 if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2872 }
2873 pub fn fast_get_int(&mut self, t: &LuaValue, k: i64) -> Result<Option<LuaValue>, LuaError> {
2874 let LuaValue::Table(tbl) = t else { return Ok(None); };
2875 let v = tbl.get_int(k);
2876 if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2877 }
2878 pub fn fast_get_short_str(&mut self, t: &LuaValue, k: &LuaValue) -> Result<Option<LuaValue>, LuaError> {
2879 let LuaValue::Table(tbl) = t else { return Ok(None); };
2880 let LuaValue::Str(s) = k else { return Ok(None); };
2881 let v = tbl.get_short_str(s);
2882 if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2883 }
2884 pub fn fast_tm_table(&mut self, t: Option<&GcRef<LuaTable>>, tm: TagMethod) -> LuaValue {
2885 let Some(mt) = t else { return LuaValue::Nil; };
2886 debug_assert!((tm as u8) <= TagMethod::Eq as u8);
2887 let ename = self.global().tmname[tm as usize].clone();
2888 mt.get_short_str(&ename)
2889 }
2890 pub fn fast_tm_ud(&mut self, u: &GcRef<LuaUserData>, tm: TagMethod) -> LuaValue {
2891 // metatable then index by the interned `__xxx` name.
2892 let mt = u.metatable();
2893 self.fast_tm_table(mt.as_ref(), tm)
2894 }
2895
2896 pub fn table_get_with_tm(&mut self, t: &LuaValue, k: &LuaValue) -> Result<LuaValue, LuaError> {
2897 // Fast path: when the table has no metatable, `__index` can never
2898 // fire — so we can return the raw slot value (Nil if absent) without
2899 // routing through finish_get's push/pop scaffolding. Halves the
2900 // get-hot-path cost on tables without metamethods, which is the
2901 // common case in table.remove/insert shift loops and most user code.
2902 if let LuaValue::Table(tbl) = t {
2903 if tbl.metatable().is_none() {
2904 return Ok(tbl.get(k));
2905 }
2906 }
2907 if let Some(v) = self.fast_get(t, k)? {
2908 return Ok(v);
2909 }
2910 let res = self.top_idx();
2911 self.push(LuaValue::Nil);
2912 crate::vm::finish_get(self, t.clone(), k.clone(), res, true, None)?;
2913 let value = self.get_at(res);
2914 self.pop();
2915 Ok(value)
2916 }
2917 /// Set `t[k] = v` with `__newindex` metamethod awareness.
2918 ///
2919 /// Fast path: when the table has no metatable, `__newindex` can never
2920 /// fire, so the existence check via `fast_get` is pure waste —
2921 /// `try_raw_set` handles both "key exists" and "key absent" cases via
2922 /// a single lookup internally. Removing the `fast_get` halves the
2923 /// lookups per set on the metamethod-free path (table.remove/insert
2924 /// hot loops, most user code).
2925 ///
2926 /// The GC backward barrier is invoked before the store (with `&v`)
2927 /// instead of after; the barrier only inspects the value's color, not
2928 /// its location, so the order is semantically equivalent to upstream
2929 /// C-Lua and lets us move `v` straight into `table_raw_set` without
2930 /// the extra `v.clone()` that the post-store ordering forced.
2931 #[inline]
2932 pub fn table_set_with_tm(&mut self, t: &LuaValue, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
2933 if let LuaValue::Table(tbl) = t {
2934 if tbl.metatable().is_none() {
2935 self.gc_barrier_back(t, &v);
2936 return self.table_raw_set(t, k, v);
2937 }
2938 }
2939 if self.fast_get(t, &k)?.is_some() {
2940 self.gc_barrier_back(t, &v);
2941 return self.table_raw_set(t, k, v);
2942 }
2943 crate::vm::finish_set(self, t.clone(), k, v, true, None, None)
2944 }
2945 #[inline]
2946 pub fn table_raw_set(&mut self, t: &LuaValue, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
2947 let LuaValue::Table(tbl) = t else {
2948 return Err(LuaError::type_error(t, "index"));
2949 };
2950 let tbl = tbl.clone();
2951 tbl.raw_set(self, k, v)
2952 }
2953 #[inline]
2954 pub fn table_array_set(&mut self, t: &LuaValue, idx: usize, v: LuaValue) -> Result<(), LuaError> {
2955 let LuaValue::Table(tbl) = t else {
2956 return Err(LuaError::type_error(t, "index"));
2957 };
2958 let tbl = tbl.clone();
2959 tbl.raw_set_int(self, idx as i64 + 1, v)
2960 }
2961 pub fn table_ensure_array(&mut self, t: &LuaValue, n: usize) -> Result<(), LuaError> {
2962 let LuaValue::Table(tbl) = t else {
2963 return Err(LuaError::type_error(t, "index"));
2964 };
2965 if n > tbl.array_len() {
2966 tbl.resize(self, n, 0)?;
2967 }
2968 Ok(())
2969 }
2970 pub fn table_length(&mut self, t: &LuaValue) -> Result<i64, LuaError> {
2971 let LuaValue::Table(tbl) = t else {
2972 return Err(LuaError::type_error(t, "get length of"));
2973 };
2974 Ok(tbl.getn() as i64)
2975 }
2976 pub fn table_metatable(&mut self, v: &LuaValue) -> Option<GcRef<LuaTable>> {
2977 match v {
2978 LuaValue::Table(t) => t.metatable(),
2979 LuaValue::UserData(u) => u.metatable(),
2980 other => {
2981 let idx = other.base_type() as usize;
2982 self.global().mt[idx].clone()
2983 }
2984 }
2985 }
2986 pub fn table_resize(&mut self, t: &GcRef<LuaTable>, na: usize, nh: usize) -> Result<(), LuaError> {
2987 self.mark_gc_check_needed();
2988 t.resize(self, na, nh)
2989 }
2990 pub fn table_getn(&self, t: &GcRef<LuaTable>) -> i64 {
2991 // PORT NOTE: C's `luaH_getn` returns a boundary i such that t[i] is
2992 // present and t[i+1] is absent (or 0 if t[1] is absent), exploiting the
2993 // hybrid array+hash layout. Phase B's LuaTable (lua-types/src/value.rs)
2994 // is a flat Vec<(K,V)> with no array part, so we linearly probe integer
2995 // keys starting at 1. The rich array+hash impl in
2996 // crates/lua-vm/src/table.rs lights up in Phase D.
2997 // PERF(port): O(n) linear scan with O(n) lookups → O(n²); Phase D fixes.
2998 let mut i: i64 = 1;
2999 loop {
3000 let v = t.get_int(i);
3001 if matches!(v, LuaValue::Nil) {
3002 return i - 1;
3003 }
3004 i += 1;
3005 }
3006 }
3007
3008 pub fn try_bin_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, p2: &LuaValue, p2_idx: Option<StackIdx>, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
3009 let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
3010 crate::tagmethods::try_bin_tm(self, p1, p1_idx, p2, p2_idx, res, event)
3011 }
3012 pub fn try_bin_i_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, imm: i64, flip: bool, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
3013 let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
3014 crate::tagmethods::try_bini_tm(self, p1, p1_idx, imm, flip, res, event)
3015 }
3016 pub fn try_bin_assoc_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, p2: &LuaValue, p2_idx: Option<StackIdx>, flip: bool, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
3017 let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
3018 crate::tagmethods::try_bin_assoc_tm(self, p1, p1_idx, p2, p2_idx, flip, res, event)
3019 }
3020 pub fn try_concat_tm(&mut self, _p1: &LuaValue, _p2: &LuaValue) -> Result<(), LuaError> {
3021 crate::tagmethods::try_concat_tm(self)
3022 }
3023 pub fn call_tm(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue, p3: &LuaValue) -> Result<(), LuaError> {
3024 crate::tagmethods::call_tm(self, f, p1.clone(), p2.clone(), p3.clone())
3025 }
3026 pub fn call_tm_res(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue, res: StackIdx) -> Result<(), LuaError> {
3027 crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)
3028 }
3029 pub fn call_tm_res_bool(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue) -> Result<bool, LuaError> {
3030 let res = self.top_idx();
3031 self.push(LuaValue::Nil);
3032 crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)?;
3033 let result = self.get_at(res).clone();
3034 self.pop();
3035 Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)))
3036 }
3037 pub fn call_order_tm(&mut self, p1: &LuaValue, p2: &LuaValue, tm: lua_types::tagmethod::TagMethod) -> Result<bool, LuaError> {
3038 let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
3039 crate::tagmethods::call_order_tm(self, p1, p2, event)
3040 }
3041 pub fn call_order_i_tm(&mut self, p1: &LuaValue, v2: i64, flip: bool, isfloat: bool, tm: lua_types::tagmethod::TagMethod) -> Result<bool, LuaError> {
3042 let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
3043 crate::tagmethods::call_orderi_tm(self, p1, v2 as i32, flip, isfloat, event)
3044 }
3045
3046 #[inline(always)]
3047 pub fn proto_code(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, pc: u32) -> lua_types::opcode::Instruction {
3048 cl.proto.code[pc as usize]
3049 }
3050 #[inline(always)]
3051 pub fn proto_const(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> LuaValue {
3052 cl.proto.k[idx].clone()
3053 }
3054 /// Hot-path accessor: returns `Some(i)` only when the constant pool entry
3055 /// at `idx` is an `Int`. Avoids the full `LuaValue` clone that
3056 /// `proto_const` performs.
3057 ///
3058 /// arithmetic opcode macros (`op_arithK`).
3059 #[inline(always)]
3060 pub fn proto_const_int(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> Option<i64> {
3061 match &cl.proto.k[idx] {
3062 LuaValue::Int(v) => Some(*v),
3063 _ => None,
3064 }
3065 }
3066 /// Hot-path accessor: returns `Some(f)` for `Float(f)` or `Int(i)` (coerced)
3067 /// constants. Avoids the full `LuaValue` clone. Used by the float fast
3068 /// path of `OP_ADDK`/`OP_SUBK`/`OP_MULK`/`OP_DIVK`/`OP_POWK`.
3069 #[inline(always)]
3070 pub fn proto_const_num(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> Option<f64> {
3071 match &cl.proto.k[idx] {
3072 LuaValue::Float(f) => Some(*f),
3073 LuaValue::Int(v) => Some(*v as f64),
3074 _ => None,
3075 }
3076 }
3077 pub fn get_proto_instr(&self, ci: CallInfoIdx, pc: u32) -> lua_types::opcode::Instruction {
3078 let cl = self.ci_lua_closure(ci)
3079 .expect("get_proto_instr: CallInfo does not hold a Lua closure");
3080 cl.proto.code[pc as usize]
3081 }
3082 /// flag as `bool` (C returns `int` 0/1).
3083 ///
3084 /// The C function reads `L->ci` directly, so the `_idx` argument is unused;
3085 /// the VM passes its locally tracked `ci` for symmetry with `trace_exec`.
3086 pub fn trace_call(&mut self, _idx: CallInfoIdx) -> Result<bool, LuaError> {
3087 Ok(crate::debug::trace_call(self)? != 0)
3088 }
3089 /// returning `bool` for the trap flag. `_idx` is unused for the same reason
3090 /// as `trace_call`; `pc` is the 0-based index of the next instruction.
3091 pub fn trace_exec(&mut self, _idx: CallInfoIdx, pc: u32) -> Result<bool, LuaError> {
3092 Ok(crate::debug::trace_exec(self, pc)? != 0)
3093 }
3094 pub fn hook_call(&mut self, idx: CallInfoIdx) -> Result<(), LuaError> {
3095 crate::do_::hookcall(self, idx)
3096 }
3097 #[inline(always)]
3098 fn gc_step_flags(&self) -> Option<(bool, bool)> {
3099 let g = self.global();
3100 if !g.is_gc_running() {
3101 return None;
3102 }
3103 let should_collect = g.heap.would_collect();
3104 let has_finalizers = !g.to_be_finalized.is_empty();
3105 if should_collect || has_finalizers {
3106 Some((should_collect, has_finalizers))
3107 } else {
3108 None
3109 }
3110 }
3111
3112 #[inline(always)]
3113 fn should_check_gc(&mut self) -> bool {
3114 if self.gc_check_needed {
3115 return true;
3116 }
3117 if !self.global().to_be_finalized.is_empty() {
3118 self.gc_check_needed = true;
3119 return true;
3120 }
3121 false
3122 }
3123
3124 #[inline(always)]
3125 pub(crate) fn mark_gc_check_needed(&mut self) {
3126 self.gc_check_needed = true;
3127 }
3128
3129 #[inline(always)]
3130 pub fn gc_check_step(&mut self) {
3131 if !self.allowhook {
3132 return;
3133 }
3134 if !self.should_check_gc() {
3135 return;
3136 }
3137 let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
3138 self.gc_check_needed = false;
3139 return;
3140 };
3141 if should_collect || has_finalizers {
3142 if should_collect {
3143 self.gc().check_step();
3144 }
3145 crate::api::run_pending_finalizers(self);
3146 self.gc_check_needed = true;
3147 }
3148 let should_keep_checking = {
3149 let g = self.global();
3150 g.heap.would_collect() || !g.to_be_finalized.is_empty()
3151 };
3152 self.gc_check_needed = should_keep_checking;
3153 }
3154 #[inline(always)]
3155 pub fn gc_cond_step(&mut self) {
3156 if !self.allowhook {
3157 return;
3158 }
3159 if !self.should_check_gc() {
3160 return;
3161 }
3162 let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
3163 self.gc_check_needed = false;
3164 return;
3165 };
3166 if should_collect || has_finalizers {
3167 if should_collect {
3168 self.gc().check_step();
3169 }
3170 crate::api::run_pending_finalizers(self);
3171 self.gc_check_needed = true;
3172 }
3173 let should_keep_checking = {
3174 let g = self.global();
3175 g.heap.would_collect() || !g.to_be_finalized.is_empty()
3176 };
3177 self.gc_check_needed = should_keep_checking;
3178 }
3179 pub fn gc_barrier_back<T, U>(&mut self, _t: T, _v: U) { /* phase-b no-op */ }
3180 pub fn gc_barrier_upval<T, U, V>(&mut self, _cl: T, _uv: U, _v: V) { /* phase-b no-op */ }
3181 ///
3182 /// Phase E-1: compares `GlobalState::current_thread_id` against
3183 /// `main_thread_id`. Coroutine resume (slice 02b) is what will swap
3184 /// `current_thread_id` in and out; until then the running thread is
3185 /// always the main thread and this returns `true`.
3186 pub fn is_main_thread(&mut self) -> bool {
3187 let g = self.global();
3188 g.current_thread_id == g.main_thread_id
3189 }
3190 pub fn obj_type_name<'v>(&self, v: &'v LuaValue) -> std::borrow::Cow<'static, [u8]> {
3191 match v {
3192 LuaValue::LightUserData(_) => std::borrow::Cow::Borrowed(b"light userdata"),
3193 LuaValue::Table(t) => {
3194 if let Some(mt) = t.metatable() {
3195 if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
3196 return std::borrow::Cow::Owned(s.as_bytes().to_vec());
3197 }
3198 }
3199 std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
3200 }
3201 LuaValue::UserData(u) => {
3202 if let Some(mt) = u.metatable() {
3203 if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
3204 return std::borrow::Cow::Owned(s.as_bytes().to_vec());
3205 }
3206 }
3207 std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
3208 }
3209 _ => std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type())),
3210 }
3211 }
3212
3213 pub fn full_type_name(&mut self, v: &LuaValue) -> Result<Vec<u8>, LuaError> {
3214 crate::tagmethods::obj_type_name(self, v)
3215 }
3216 pub fn emit_warning(&mut self, _msg: &[u8], _to_cont: bool) { warning(self, _msg, _to_cont) }
3217}
3218
3219// ─── GcHandle — no-op GC facade ───────────────────────────────────────────────
3220
3221/// A short-lived handle returned by `state.gc()` for GC operations.
3222///
3223/// In Phases A–C all methods are no-ops. Phase D replaces with real GC.
3224pub struct GcHandle<'a> {
3225 _state: &'a mut LuaState,
3226}
3227
3228/// Composite root passed to `Heap::full_collect`. The Phase-A workaround in
3229/// `new_state` leaves `GlobalState.mainthread = None` (to break the
3230/// self-referential Rc cycle pre-D), so the running thread's stack and
3231/// openupval list are not reachable from `GlobalState::trace`. Wrapping both
3232/// references in a single `Trace`-implementing root injects the active
3233/// thread as a second mark source for the duration of the collection.
3234struct CollectRoots<'a> {
3235 global: &'a GlobalState,
3236 thread: &'a LuaState,
3237}
3238
3239impl<'a> lua_gc::Trace for CollectRoots<'a> {
3240 fn trace(&self, m: &mut lua_gc::Marker) {
3241 self.global.trace(m);
3242 self.thread.trace(m);
3243 }
3244}
3245
3246fn trace_reachable_threads(
3247 global: &GlobalState,
3248 _current_thread_id: u64,
3249 marker: &mut lua_gc::Marker,
3250) {
3251 use lua_gc::Trace;
3252
3253 loop {
3254 let visited_before = marker.visited_count();
3255 for (id, entry) in global.threads.iter() {
3256 if thread_entry_marked_alive(marker, *id, entry) {
3257 if let Ok(thread) = entry.state.try_borrow() {
3258 thread.trace(marker);
3259 }
3260 }
3261 }
3262 marker.drain_gray_queue();
3263 if marker.visited_count() == visited_before {
3264 break;
3265 }
3266 }
3267}
3268
3269fn thread_entry_marked_alive(
3270 marker: &lua_gc::Marker,
3271 id: u64,
3272 entry: &ThreadRegistryEntry,
3273) -> bool {
3274 marker.is_visited(entry.value.identity()) && entry.value.id == id
3275}
3276
3277fn close_open_upvalues_for_unreachable_threads(
3278 global: &GlobalState,
3279 marker: &mut lua_gc::Marker,
3280) {
3281 use lua_gc::Trace;
3282
3283 let mut closed_values = Vec::<LuaValue>::new();
3284 for (id, entry) in global.threads.iter() {
3285 if entry.value.id != *id {
3286 continue;
3287 }
3288 if thread_entry_marked_alive(marker, *id, entry) {
3289 continue;
3290 }
3291 let Ok(thread) = entry.state.try_borrow() else {
3292 continue;
3293 };
3294 for uv in thread.openupval.iter() {
3295 if !marker.is_visited(uv.identity()) {
3296 continue;
3297 }
3298 let Some((thread_id, idx)) = uv.try_open_payload() else {
3299 continue;
3300 };
3301 if thread_id as u64 != *id {
3302 continue;
3303 }
3304 let value = thread.get_at(idx);
3305 uv.close_with(value.clone());
3306 closed_values.push(value);
3307 }
3308 }
3309 for value in closed_values {
3310 value.trace(marker);
3311 }
3312 marker.drain_gray_queue();
3313}
3314
3315impl<'a> GcHandle<'a> {
3316 /// macros.tsv: `luaC_checkGC → state.gc().check_step()`
3317 ///
3318 /// Phase D-2: drives implicit collection when the heap's byte threshold
3319 /// is exceeded. Without this hook, loops that allocate without an
3320 /// explicit `collectgarbage()` call (e.g. `closure.lua`'s
3321 /// `while x[1] do local a = A..A end` GC-driven loop) never settle.
3322 pub fn check_step(&self) {
3323 if !self._state.global().is_gc_running() {
3324 return;
3325 }
3326 self.collect_via_heap(/* force = */ false);
3327 }
3328
3329 /// macros.tsv: `luaC_fullgc → state.gc().full_collect()`
3330 pub fn full_collect(&self) {
3331 self.collect_via_heap(/* force = */ true);
3332 }
3333
3334 /// Shared driver behind both `full_collect` (force-collect) and
3335 /// `check_step` (collect only if heap byte threshold exceeded).
3336 ///
3337 /// Snapshots the weak-tables registry, invokes the heap's collect path
3338 /// with a post-mark weak-prune hook, and rebuilds the registry by
3339 /// retaining only entries whose target was reachable. The same hook
3340 /// works for both modes — the heap short-circuits when force=false and
3341 /// the threshold isn't met.
3342 fn collect_via_heap(&self, force: bool) {
3343 use lua_gc::Trace;
3344 let state_ref: &LuaState = &*self._state;
3345
3346 // Fast path: when the caller did not force a collection, skip all
3347 // the snapshot work (3 Vec allocations + 3 HashSet allocations) if
3348 // the heap is paused or under threshold — a `step()` in that state
3349 // is a no-op, so the snapshot would be pure waste. Called millions
3350 // of times per recursive workload via `gc_check_step` in `precall`.
3351 if !force {
3352 let g = state_ref.global.borrow();
3353 if !g.heap.would_collect() {
3354 return;
3355 }
3356 }
3357
3358 // Snapshot weak tables BEFORE the collect. `identity()` reads only
3359 // the pointer address — safe even on still-dangling weak handles —
3360 // and dedup by identity keeps the iteration linear.
3361 let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3362 let g = state_ref.global.borrow();
3363 let mut seen = std::collections::HashSet::<usize>::new();
3364 g.weak_tables_registry
3365 .iter()
3366 .filter_map(|w| w.upgrade())
3367 .filter(|t| seen.insert(t.identity()))
3368 .collect()
3369 };
3370
3371 // Snapshot pending finalizers. `GlobalState::trace` deliberately
3372 // does NOT root these — that's how the post-mark hook below can
3373 // distinguish "still reachable from program state" from "only kept
3374 // alive by the finalizer registry."
3375 let pending_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3376 let g = state_ref.global.borrow();
3377 g.pending_finalizers.clone()
3378 };
3379
3380 // Snapshot tracked long-string identities + byte sizes BEFORE the
3381 // collect. The post-mark hook compares each identity against the
3382 // marker's visited set; anything not visited is unreachable and
3383 // its bytes get reclaimed from `gc_debt` after the heap collect
3384 // returns. Bare `usize` is safe to carry across the hook — long
3385 // strings use `new_uncollected` so the pointer never dangles.
3386 let long_string_snapshot: Vec<(usize, usize)> = {
3387 let g = state_ref.global.borrow();
3388 g.gc_tracked_long_strings
3389 .iter()
3390 .map(|(w, sz)| (w.0.identity(), *sz))
3391 .collect()
3392 };
3393
3394 let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
3395 std::cell::RefCell::new(std::collections::HashSet::new());
3396 let newly_unreachable: std::cell::RefCell<Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>>> =
3397 std::cell::RefCell::new(Vec::new());
3398 let dead_long_strings: std::cell::RefCell<std::collections::HashSet<usize>> =
3399 std::cell::RefCell::new(std::collections::HashSet::new());
3400 let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
3401 std::cell::RefCell::new(std::collections::HashSet::new());
3402 let collect_ran = std::cell::Cell::new(false);
3403
3404 {
3405 let global = state_ref.global.borrow();
3406 global.heap.unpause();
3407 let roots = CollectRoots { global: &*global, thread: state_ref };
3408 let hook = |marker: &mut lua_gc::Marker| {
3409 collect_ran.set(true);
3410 trace_reachable_threads(&*global, global.current_thread_id, marker);
3411 close_open_upvalues_for_unreachable_threads(&*global, marker);
3412 loop {
3413 let visited_before = marker.visited_count();
3414 for t in &weak_tables_snapshot {
3415 let t_id = t.identity();
3416 if !marker.is_visited(t_id) {
3417 continue;
3418 }
3419 let to_mark = t.ephemeron_values_to_mark(
3420 &|id| marker.is_visited(id),
3421 );
3422 for v in &to_mark {
3423 v.trace(marker);
3424 }
3425 }
3426 marker.drain_gray_queue();
3427 if marker.visited_count() == visited_before {
3428 break;
3429 }
3430 }
3431 for pf in &pending_snapshot {
3432 if !marker.is_visited(pf.identity()) {
3433 marker.mark(pf.0);
3434 newly_unreachable.borrow_mut().push(pf.clone());
3435 }
3436 }
3437 marker.drain_gray_queue();
3438 loop {
3439 let visited_before = marker.visited_count();
3440 for t in &weak_tables_snapshot {
3441 let t_id = t.identity();
3442 if !marker.is_visited(t_id) {
3443 continue;
3444 }
3445 let to_mark = t.ephemeron_values_to_mark(
3446 &|id| marker.is_visited(id),
3447 );
3448 for v in &to_mark {
3449 v.trace(marker);
3450 }
3451 }
3452 marker.drain_gray_queue();
3453 if marker.visited_count() == visited_before {
3454 break;
3455 }
3456 }
3457 for t in &weak_tables_snapshot {
3458 let id = t.identity();
3459 if marker.is_visited(id) {
3460 let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3461 for v in &to_mark {
3462 v.trace(marker);
3463 }
3464 alive_ids.borrow_mut().insert(id);
3465 }
3466 }
3467 marker.drain_gray_queue();
3468 // Long-string Phase-B reclaim. With `new_uncollected`
3469 // allocation, long strings never enter the heap's sweep
3470 // path, so we rely on the marker's visited set: any
3471 // tracked long-string identity that wasn't reached by mark
3472 // is unreferenced and its bytes can be returned to
3473 // `gc_debt`. Done here (inside the hook) so it sees the
3474 // visited set BEFORE drop of the marker.
3475 {
3476 let mut dead = dead_long_strings.borrow_mut();
3477 for (id, _sz) in &long_string_snapshot {
3478 if !marker.is_visited(*id) {
3479 dead.insert(*id);
3480 }
3481 }
3482 }
3483 {
3484 let mut alive = alive_thread_ids.borrow_mut();
3485 for (id, entry) in global.threads.iter() {
3486 if thread_entry_marked_alive(marker, *id, entry) {
3487 alive.insert(*id);
3488 }
3489 }
3490 }
3491 };
3492 if force {
3493 global.heap.full_collect_with_post_mark(&roots, hook);
3494 } else {
3495 global.heap.step_with_post_mark(&roots, hook);
3496 }
3497 }
3498
3499 if !collect_ran.get() {
3500 return;
3501 }
3502
3503 // After collect, drop weak-table-registry entries whose target was
3504 // swept. Without this filter the registry leaks one dangling
3505 // `GcWeak<LuaTable>` per dead weak table; the next collect would
3506 // upgrade those handles (current placeholder GcWeak always returns
3507 // Some) and the prune walk would deref freed memory.
3508 let alive_set = alive_ids.into_inner();
3509 let promote: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> =
3510 newly_unreachable.into_inner();
3511 let promote_ids: std::collections::HashSet<usize> =
3512 promote.iter().map(|t| t.identity()).collect();
3513 let dead_ls_ids = dead_long_strings.into_inner();
3514 let alive_thread_ids = alive_thread_ids.into_inner();
3515 let mut g = state_ref.global.borrow_mut();
3516 g.weak_tables_registry
3517 .retain(|w| alive_set.contains(&w.0.identity()));
3518 let main_thread_id = g.main_thread_id;
3519 g.threads.retain(|id, _| alive_thread_ids.contains(id));
3520 g.cross_thread_upvals
3521 .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
3522 // Move newly-unreachable finalizables from `pending_finalizers` to
3523 // `to_be_finalized`. The latter is rooted by `GlobalState::trace`,
3524 // so these tables remain alive until their `__gc` runs.
3525 g.pending_finalizers
3526 .retain(|t| !promote_ids.contains(&t.identity()));
3527 g.to_be_finalized.extend(promote);
3528 // Reclaim long-string byte accounting for entries the marker said
3529 // were unreachable. The underlying `Gc<LuaString>` was allocated
3530 // via `new_uncollected` and stays live in process memory; only
3531 // `gc_debt` is adjusted so `collectgarbage("count")` reflects the
3532 // drop in user-visible live bytes.
3533 if !dead_ls_ids.is_empty() {
3534 let mut freed: isize = 0;
3535 g.gc_tracked_long_strings.retain(|(w, sz)| {
3536 if dead_ls_ids.contains(&w.0.identity()) {
3537 freed += *sz as isize;
3538 false
3539 } else {
3540 true
3541 }
3542 });
3543 g.gc_debt -= freed;
3544 }
3545 }
3546
3547 /// Phase-B stub for `luaC_step(L)`.
3548 pub fn step(&self) { /* phase-b no-op */ }
3549
3550 /// Run one budgeted incremental step of the GC.
3551 ///
3552 /// `work_units` is the number of GC work units the step is allowed to
3553 /// perform (one gray trace, one sweep visit, or one phase transition).
3554 /// Returns `true` if the step completed a cycle and the collector is
3555 /// now in the `Pause` state; `false` otherwise.
3556 ///
3557 /// Mirrors `collect_via_heap` for the post-mark weak-table /
3558 /// finalizer-promotion logic, but only the atomic-phase transition will
3559 /// invoke the snapshot-walking hook — propagate and sweep steps reuse
3560 /// the snapshot but never execute it. The snapshot is rebuilt on every
3561 /// call; the cost is `O(weak_tables_registry)` per step.
3562 pub fn incremental_step(&self, work_units: isize) -> bool {
3563 use lua_gc::{StepBudget, StepOutcome, Trace};
3564 let state_ref: &LuaState = &*self._state;
3565
3566 let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3567 let g = state_ref.global.borrow();
3568 let mut seen = std::collections::HashSet::<usize>::new();
3569 g.weak_tables_registry
3570 .iter()
3571 .filter_map(|w| w.upgrade())
3572 .filter(|t| seen.insert(t.identity()))
3573 .collect()
3574 };
3575
3576 let pending_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3577 let g = state_ref.global.borrow();
3578 g.pending_finalizers.clone()
3579 };
3580
3581 let long_string_snapshot: Vec<(usize, usize)> = {
3582 let g = state_ref.global.borrow();
3583 g.gc_tracked_long_strings
3584 .iter()
3585 .map(|(w, sz)| (w.0.identity(), *sz))
3586 .collect()
3587 };
3588
3589 let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
3590 std::cell::RefCell::new(std::collections::HashSet::new());
3591 let newly_unreachable: std::cell::RefCell<Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>>> =
3592 std::cell::RefCell::new(Vec::new());
3593 let dead_long_strings: std::cell::RefCell<std::collections::HashSet<usize>> =
3594 std::cell::RefCell::new(std::collections::HashSet::new());
3595 let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
3596 std::cell::RefCell::new(std::collections::HashSet::new());
3597 let atomic_ran = std::cell::Cell::new(false);
3598
3599 let outcome = {
3600 let global = state_ref.global.borrow();
3601 global.heap.unpause();
3602 let roots = CollectRoots { global: &*global, thread: state_ref };
3603 let hook = |marker: &mut lua_gc::Marker| {
3604 atomic_ran.set(true);
3605 trace_reachable_threads(&*global, global.current_thread_id, marker);
3606 close_open_upvalues_for_unreachable_threads(&*global, marker);
3607 loop {
3608 let visited_before = marker.visited_count();
3609 for t in &weak_tables_snapshot {
3610 let t_id = t.identity();
3611 if !marker.is_visited(t_id) {
3612 continue;
3613 }
3614 let to_mark = t.ephemeron_values_to_mark(
3615 &|id| marker.is_visited(id),
3616 );
3617 for v in &to_mark {
3618 v.trace(marker);
3619 }
3620 }
3621 marker.drain_gray_queue();
3622 if marker.visited_count() == visited_before {
3623 break;
3624 }
3625 }
3626 for pf in &pending_snapshot {
3627 if !marker.is_visited(pf.identity()) {
3628 marker.mark(pf.0);
3629 newly_unreachable.borrow_mut().push(pf.clone());
3630 }
3631 }
3632 marker.drain_gray_queue();
3633 loop {
3634 let visited_before = marker.visited_count();
3635 for t in &weak_tables_snapshot {
3636 let t_id = t.identity();
3637 if !marker.is_visited(t_id) {
3638 continue;
3639 }
3640 let to_mark = t.ephemeron_values_to_mark(
3641 &|id| marker.is_visited(id),
3642 );
3643 for v in &to_mark {
3644 v.trace(marker);
3645 }
3646 }
3647 marker.drain_gray_queue();
3648 if marker.visited_count() == visited_before {
3649 break;
3650 }
3651 }
3652 for t in &weak_tables_snapshot {
3653 let id = t.identity();
3654 if marker.is_visited(id) {
3655 let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3656 for v in &to_mark {
3657 v.trace(marker);
3658 }
3659 alive_ids.borrow_mut().insert(id);
3660 }
3661 }
3662 marker.drain_gray_queue();
3663 {
3664 let mut dead = dead_long_strings.borrow_mut();
3665 for (id, _sz) in &long_string_snapshot {
3666 if !marker.is_visited(*id) {
3667 dead.insert(*id);
3668 }
3669 }
3670 }
3671 {
3672 let mut alive = alive_thread_ids.borrow_mut();
3673 for (id, entry) in global.threads.iter() {
3674 if thread_entry_marked_alive(marker, *id, entry) {
3675 alive.insert(*id);
3676 }
3677 }
3678 }
3679 };
3680 let budget = StepBudget::from_work(work_units);
3681 global.heap.incremental_step_with_post_mark(&roots, budget, hook)
3682 };
3683
3684 if atomic_ran.get() {
3685 let alive_set = alive_ids.into_inner();
3686 let promote: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> =
3687 newly_unreachable.into_inner();
3688 let promote_ids: std::collections::HashSet<usize> =
3689 promote.iter().map(|t| t.identity()).collect();
3690 let dead_ls_ids = dead_long_strings.into_inner();
3691 let alive_thread_ids = alive_thread_ids.into_inner();
3692 let mut g = state_ref.global.borrow_mut();
3693 g.weak_tables_registry
3694 .retain(|w| alive_set.contains(&w.0.identity()));
3695 let main_thread_id = g.main_thread_id;
3696 g.threads.retain(|id, _| alive_thread_ids.contains(id));
3697 g.cross_thread_upvals
3698 .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
3699 g.pending_finalizers
3700 .retain(|t| !promote_ids.contains(&t.identity()));
3701 g.to_be_finalized.extend(promote);
3702 if !dead_ls_ids.is_empty() {
3703 let mut freed: isize = 0;
3704 g.gc_tracked_long_strings.retain(|(w, sz)| {
3705 if dead_ls_ids.contains(&w.0.identity()) {
3706 freed += *sz as isize;
3707 false
3708 } else {
3709 true
3710 }
3711 });
3712 g.gc_debt -= freed;
3713 }
3714 }
3715
3716 matches!(outcome, StepOutcome::Paused)
3717 }
3718
3719 /// Run only the weak-table atomic cleanup used by a generational step.
3720 ///
3721 /// C-Lua's `genstep` performs young/full generational work and includes
3722 /// weak-table clearing at the atomic boundary. This heap does not model
3723 /// ages yet; this mark-only pass gives explicit generational steps the
3724 /// weak cleanup they need without sweeping objects from suspended threads.
3725 pub fn prune_weak_tables_mark_only(&self) {
3726 use lua_gc::Trace;
3727 let state_ref: &LuaState = &*self._state;
3728
3729 let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3730 let g = state_ref.global.borrow();
3731 let mut seen = std::collections::HashSet::<usize>::new();
3732 g.weak_tables_registry
3733 .iter()
3734 .filter_map(|w| w.upgrade())
3735 .filter(|t| seen.insert(t.identity()))
3736 .collect()
3737 };
3738
3739 let global = state_ref.global.borrow();
3740 global.heap.unpause();
3741 let roots = CollectRoots { global: &*global, thread: state_ref };
3742 let hook = |marker: &mut lua_gc::Marker| {
3743 trace_reachable_threads(&*global, global.current_thread_id, marker);
3744 loop {
3745 let visited_before = marker.visited_count();
3746 for t in &weak_tables_snapshot {
3747 let t_id = t.identity();
3748 if !marker.is_visited(t_id) {
3749 continue;
3750 }
3751 let to_mark = t.ephemeron_values_to_mark(
3752 &|id| marker.is_visited(id),
3753 );
3754 for v in &to_mark {
3755 v.trace(marker);
3756 }
3757 }
3758 marker.drain_gray_queue();
3759 if marker.visited_count() == visited_before {
3760 break;
3761 }
3762 }
3763 for t in &weak_tables_snapshot {
3764 if marker.is_visited(t.identity()) {
3765 let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3766 for v in &to_mark {
3767 v.trace(marker);
3768 }
3769 }
3770 }
3771 };
3772 global.heap.mark_only_with_post_mark(&roots, hook);
3773 }
3774
3775 /// Set the GC kind (incremental/generational).
3776 ///
3777 /// itself is `Rc`-based, so the only observable effect is the mode flag
3778 /// returned by `lua_gc(LUA_GCGEN)` / `lua_gc(LUA_GCINC)` on the next call.
3779 pub fn change_mode(&self, mode: GcKind) {
3780 self._state.global_mut().gckind = mode as u8;
3781 }
3782
3783 /// Phase-B stub for `luaC_fix(L, o)` — pin an object so GC won't collect it.
3784 pub fn fix_object<T: lua_gc::Trace + 'static>(&self, _o: &GcRef<T>) { /* phase-b no-op */ }
3785
3786 /// Free all collectable objects (called during state teardown).
3787 ///
3788 /// PORT NOTE: In Phases A–C, Rc drop chains handle deallocation automatically.
3789 pub fn free_all_objects(&self) {
3790 // PORT NOTE: Phase A–C no-op; Rc::drop handles deallocation
3791 }
3792
3793 /// GC write barrier for a TValue.
3794 ///
3795 /// macros.tsv: `luaC_barrier → state.gc().barrier(p, v)` — no-op in Phases A–C
3796 pub fn barrier(&self, _p: &dyn std::any::Any, _v: &LuaValue) {}
3797
3798 /// Backward write barrier.
3799 ///
3800 /// macros.tsv: `luaC_barrierback → state.gc().barrier_back(p, v)` — no-op
3801 pub fn barrier_back(&self, _p: &dyn std::any::Any, _v: &LuaValue) {}
3802
3803 /// Object write barrier.
3804 ///
3805 /// macros.tsv: `luaC_objbarrier → state.gc().obj_barrier(p, o)` — no-op
3806 pub fn obj_barrier(&self, _p: &dyn std::any::Any, _o: &dyn std::any::Any) {}
3807
3808 /// Backward object write barrier.
3809 ///
3810 pub fn obj_barrier_back(&self, _p: &dyn std::any::Any, _o: &dyn std::any::Any) {}
3811}
3812
3813// ─── Functions from lstate.c ──────────────────────────────────────────────────
3814
3815//
3816// PORT NOTE: `luai_makeseed` in C mixed ASLR entropy (pointer addresses of a
3817// heap var, stack var, and code symbol) with the current time via `luaS_hash`.
3818// In Rust, raw pointer addresses require `unsafe` which is forbidden outside
3819// lua-gc/lua-coro. Native builds use time-only entropy for now; bare WASM uses
3820// a fixed seed so state creation never touches a stubbed host clock.
3821fn make_seed() -> u32 {
3822 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
3823 {
3824 return crate::string::hash_bytes(b"lua-rs-wasm-seed", 0x9e37_79b9);
3825 }
3826
3827 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
3828 {
3829 use std::time::{SystemTime, UNIX_EPOCH};
3830 let t = SystemTime::now()
3831 .duration_since(UNIX_EPOCH)
3832 .map(|d| d.as_secs() as u32)
3833 .unwrap_or(0);
3834
3835 // TODO(port): mix in ASLR entropy (pointer to heap / stack / code).
3836 // Requires a short `unsafe` block to cast references to usize.
3837 // The entropy improvement is important for hash DoS resistance (CVE-class).
3838 // Phase B should add this via a platform-specific helper in lua-gc or via
3839 // the `getrandom` crate if it is added as a dependency.
3840
3841 // For Phase A, just hash the time bytes against itself.
3842 crate::string::hash_bytes(&t.to_le_bytes(), t)
3843 }
3844}
3845
3846/// Adjust `GCdebt` to `debt` while preserving the `totalbytes + GCdebt` invariant.
3847///
3848///
3849/// ```c
3850///
3851/// // l_mem tb = gettotalbytes(g);
3852/// // lua_assert(tb > 0);
3853/// // if (debt < tb - MAX_LMEM)
3854/// // debt = tb - MAX_LMEM;
3855/// // g->totalbytes = tb - debt;
3856/// // g->GCdebt = debt;
3857/// // }
3858/// ```
3859pub(crate) fn set_debt(g: &mut GlobalState, mut debt: isize) {
3860 let tb = g.total_bytes() as isize;
3861 debug_assert!(tb > 0);
3862 // macros.tsv: MAX_LMEM → isize::MAX
3863 if debt < tb.saturating_sub(isize::MAX) {
3864 debt = tb - isize::MAX;
3865 }
3866 g.totalbytes = tb - debt;
3867 g.gc_debt = debt;
3868}
3869
3870/// Sweep the Phase-B long-string tracker and decrement `gc_debt` by the
3871/// recorded byte count of any entry whose underlying `Rc` has been dropped.
3872///
3873/// PORT NOTE: Phase D will replace this with the real allocator's per-object
3874/// accounting through `luaM_realloc`. For now, long-string creation pushes a
3875/// `(Weak, size)` pair onto `gc_tracked_long_strings`, and this helper
3876/// reclaims the bytes lazily — at every `collectgarbage("count")` query and
3877/// at the end of `collectgarbage("collect")` — so the Lua-visible memory
3878/// total reflects live string bytes rather than peak allocation.
3879pub(crate) fn reclaim_dead_long_strings(g: &mut GlobalState) {
3880 let mut freed: isize = 0;
3881 g.gc_tracked_long_strings.retain(|(w, sz)| {
3882 if w.strong_count() == 0 {
3883 freed += *sz as isize;
3884 false
3885 } else {
3886 true
3887 }
3888 });
3889 g.gc_debt -= freed;
3890}
3891
3892/// Deprecated no-op that returns `LUAI_MAXCCALLS`.
3893///
3894///
3895/// ```c
3896///
3897/// // UNUSED(L); UNUSED(limit);
3898/// // return LUAI_MAXCCALLS; /* warning?? */
3899/// // }
3900/// ```
3901pub fn set_c_stack_limit(_state: &mut LuaState, _limit: u32) -> i32 {
3902 let _ = (_state, _limit);
3903 LUAI_MAXCCALLS as i32
3904}
3905
3906/// Allocate a fresh `CallInfo` beyond the current frame and return its index.
3907///
3908///
3909/// ```c
3910///
3911/// // CallInfo *ci;
3912/// // lua_assert(L->ci->next == NULL);
3913/// // ci = luaM_new(L, CallInfo);
3914/// // L->ci->next = ci;
3915/// // ci->previous = L->ci;
3916/// // ci->next = NULL;
3917/// // ci->u.l.trap = 0;
3918/// // L->nci++;
3919/// // return ci;
3920/// // }
3921/// ```
3922pub(crate) fn extend_ci(state: &mut LuaState) -> CallInfoIdx {
3923 debug_assert!(
3924 state.call_info[state.ci.0 as usize].next.is_none(),
3925 "extend_ci: current ci already has a cached next frame"
3926 );
3927
3928 let current_idx = state.ci;
3929 // macros.tsv: luaM_new → Box::new(T::default()) — here we push onto the Vec
3930 let new_idx = CallInfoIdx(state.call_info.len() as u32);
3931
3932 state.call_info.push(CallInfo {
3933 previous: Some(current_idx),
3934 next: None,
3935 u: CallInfoFrame::lua_default(),
3936 ..CallInfo::default()
3937 });
3938
3939 state.call_info[current_idx.0 as usize].next = Some(new_idx);
3940
3941 state.nci += 1;
3942
3943 new_idx
3944}
3945
3946/// Free all cached (unused) `CallInfo` frames beyond the current frame.
3947///
3948///
3949/// ```c
3950///
3951/// // CallInfo *ci = L->ci;
3952/// // CallInfo *next = ci->next;
3953/// // ci->next = NULL;
3954/// // while ((ci = next) != NULL) {
3955/// // next = ci->next;
3956/// // luaM_free(L, ci);
3957/// // L->nci--;
3958/// // }
3959/// // }
3960/// ```
3961///
3962/// PORT NOTE: In C, each `CallInfo` is an independent heap allocation freed by
3963/// `luaM_free`. In Rust, all `CallInfo` entries live in `state.call_info: Vec<CallInfo>`.
3964/// We walk the link chain to count removals (updating `nci`), then truncate the Vec.
3965/// This is safe as long as all free entries have indices greater than `state.ci`.
3966fn free_ci(state: &mut LuaState) {
3967 let ci_idx = state.ci.0 as usize;
3968
3969 let mut next_opt = state.call_info[ci_idx].next.take();
3970
3971 while let Some(idx) = next_opt {
3972 next_opt = state.call_info[idx.0 as usize].next;
3973 state.nci = state.nci.saturating_sub(1);
3974 }
3975
3976 // Truncate: drop all entries beyond the current ci.
3977 // TODO(port): verify invariant that all cached frames have contiguous indices > state.ci
3978 state.call_info.truncate(ci_idx + 1);
3979}
3980
3981/// Free approximately half of the cached `CallInfo` frames beyond the current frame.
3982///
3983///
3984/// ```c
3985///
3986/// // CallInfo *ci = L->ci->next;
3987/// // CallInfo *next;
3988/// // if (ci == NULL) return;
3989/// // while ((next = ci->next) != NULL) {
3990/// // CallInfo *next2 = next->next;
3991/// // ci->next = next2;
3992/// // L->nci--;
3993/// // luaM_free(L, next);
3994/// // if (next2 == NULL) break;
3995/// // else { next2->previous = ci; ci = next2; }
3996/// // }
3997/// // }
3998/// ```
3999///
4000/// PORT NOTE: The C code removes every other node from the free-list chain by
4001/// pointer manipulation. In Rust, removing elements from the middle of a `Vec`
4002/// shifts subsequent elements and invalidates `CallInfoIdx` values that point
4003/// past the removal site. For Phase A, we approximate by halving the free count
4004/// via truncation. TODO(port): Phase B should implement a proper free-list
4005/// pool (e.g., a slab) that allows O(1) element removal without index
4006/// invalidation.
4007pub(crate) fn shrink_ci(state: &mut LuaState) {
4008 let ci_idx = state.ci.0 as usize;
4009
4010 if state.call_info[ci_idx].next.is_none() {
4011 return;
4012 }
4013
4014 let free_count = state.call_info.len().saturating_sub(ci_idx + 1);
4015 if free_count <= 1 {
4016 return;
4017 }
4018
4019 // Remove every other cached frame (halve the free list).
4020 // PERF(port): truncation is O(n) copy for the drop; a slab allocator
4021 // would be O(1) — profile in Phase B.
4022 let keep = free_count / 2;
4023 let removed = free_count - keep;
4024 let new_len = ci_idx + 1 + keep;
4025 state.call_info.truncate(new_len);
4026 state.nci = state.nci.saturating_sub(removed as u32);
4027
4028 // Terminate the now-last cached frame.
4029 if let Some(last) = state.call_info.last_mut() {
4030 last.next = None;
4031 }
4032}
4033
4034/// Check whether the C-call depth has reached its limit and raise an error if so.
4035///
4036///
4037/// ```c
4038///
4039/// // if (getCcalls(L) == LUAI_MAXCCALLS)
4040/// // luaG_runerror(L, "C stack overflow");
4041/// // else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11))
4042/// // luaD_throw(L, LUA_ERRERR);
4043/// // }
4044/// ```
4045pub(crate) fn check_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
4046 // macros.tsv: getCcalls → state.c_calls()
4047 // error_sites.tsv: luaG_runerror → return Err(LuaError::runtime(format_args!(...)))
4048 if state.c_calls() == LUAI_MAXCCALLS {
4049 return Err(LuaError::runtime(format_args!("C stack overflow")));
4050 }
4051 // error_sites.tsv: luaD_throw(L, LUA_ERRERR) → return Err(LuaError::with_status(LuaStatus::ErrErr))
4052 if state.c_calls() >= (LUAI_MAXCCALLS / 10 * 11) {
4053 // TODO(port): LuaError::with_status takes a LuaStatus enum, not a raw i32.
4054 // The exact constructor shape depends on lua-types/error.rs in Phase B.
4055 return Err(LuaError::runtime(format_args!(
4056 "error while handling stack overflow (C stack overflow)"
4057 )));
4058 }
4059 Ok(())
4060}
4061
4062/// Increment the C-call depth counter, checking for overflow.
4063///
4064///
4065/// ```c
4066///
4067/// // L->n_ccalls++;
4068/// // if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS))
4069/// // luaE_checkcstack(L);
4070/// // }
4071/// ```
4072pub fn inc_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
4073 state.n_ccalls += 1;
4074 // macros.tsv: l_unlikely → x (drop branch hint); getCcalls → state.c_calls()
4075 if state.c_calls() >= LUAI_MAXCCALLS {
4076 check_c_stack(state)?;
4077 }
4078 Ok(())
4079}
4080
4081//
4082// PORT NOTE: In C, `L` is a separate thread used only for memory allocation
4083// (via `luaM_newvector`). In Rust we don't have a custom allocator; all
4084// allocation goes through the global Rust allocator. The function takes only
4085// the new thread (`thread`) and ignores the caller.
4086fn stack_init(thread: &mut LuaState) {
4087 // macros.tsv: luaM_newvector → vec![T::default(); n]
4088 let total_slots = BASIC_STACK_SIZE + EXTRA_STACK;
4089 thread.stack = vec![StackValue::default(); total_slots];
4090
4091 // types.tsv: lua_State.tbclist → Vec<StackIdx>
4092 // PORT NOTE: In C, tbclist.p = stack.p is a sentinel meaning "no tbc vars".
4093 // In Rust the Vec is empty when there are no tbc variables.
4094 thread.tbclist = Vec::new();
4095
4096 // setnilvalue(s2v(L1->stack.p + i)); /* erase new stack */
4097 // macros.tsv: setnilvalue → *o = LuaValue::Nil
4098 // Already initialized to LuaValue::Nil via StackValue::default().
4099
4100 thread.top = StackIdx(0);
4101
4102 thread.stack_last = StackIdx(BASIC_STACK_SIZE as u32);
4103
4104
4105 let base_ci = CallInfo {
4106 func: StackIdx(0),
4107 top: StackIdx(1 + LUA_MINSTACK as u32),
4108 previous: None,
4109 next: None,
4110 callstatus: CIST_C,
4111 nresults: 0,
4112 u: CallInfoFrame::c_default(),
4113 u2: CallInfoExtra::default(),
4114 };
4115
4116 if thread.call_info.is_empty() {
4117 thread.call_info.push(base_ci);
4118 } else {
4119 thread.call_info[0] = base_ci;
4120 thread.call_info.truncate(1);
4121 }
4122
4123 thread.stack[0] = StackValue { val: LuaValue::Nil, tbc_delta: 0 };
4124
4125 thread.top = StackIdx(1);
4126
4127 thread.ci = CallInfoIdx(0);
4128}
4129
4130fn free_stack(state: &mut LuaState) {
4131 if state.stack.is_empty() {
4132 return;
4133 }
4134 state.ci = CallInfoIdx(0);
4135 free_ci(state);
4136 debug_assert_eq!(state.nci, 0, "nci should be 0 after free_ci");
4137 // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
4138 state.stack.clear();
4139 state.stack.shrink_to_fit();
4140}
4141
4142fn init_registry(state: &mut LuaState) -> Result<(), LuaError> {
4143 // macros.tsv: luaH_new → state.new_table()
4144 let registry = state.new_table();
4145
4146 // macros.tsv: sethvalue → *o = LuaValue::Table(x.clone())
4147 state.global_mut().l_registry = LuaValue::Table(registry.clone());
4148
4149 // macros.tsv: luaH_resize → t.resize(state, na, nh)?
4150 // TODO(port): registry is a GcRef<LuaTable> (Rc); calling methods requires borrow_mut()
4151 // For Phase A, use RefCell interior mutability on LuaTable, or accept the limitation.
4152 // Using Rc::get_mut is not available because of possible aliasing.
4153 // TODO(port): LuaTable resize requires &mut access through Rc — needs RefCell<LuaTable>
4154 // or a redesign in Phase B.
4155
4156 // macros.tsv: setthvalue → *o = LuaValue::Thread(x.clone())
4157 // TODO(port): cannot create GcRef<LuaState> to self (self-referential Rc).
4158 // In Phase E this would be resolved once coroutine threads are GcRef-tracked.
4159 // For Phase A: leave registry[LUA_RIDX_MAINTHREAD-1] as Nil and add a TODO.
4160 // TODO(port): set registry[LUA_RIDX_MAINTHREAD - 1] = LuaValue::Thread(main_thread_gcref)
4161
4162 // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
4163 // storage-less, so we can't actually persist the globals table inside
4164 // the registry via array_set. Store it in a direct GlobalState field
4165 // and patch get_global_table to read it from there. Symmetric for the
4166 // _LOADED module cache. Once the LuaTable placeholder reconciles, the
4167 // canonical registry storage takes over and these fields disappear.
4168 let globals = state.new_table();
4169 state.global_mut().globals = LuaValue::Table(globals);
4170 let loaded = state.new_table();
4171 state.global_mut().loaded = LuaValue::Table(loaded);
4172
4173 Ok(())
4174}
4175
4176fn lua_open(state: &mut LuaState) -> Result<(), LuaError> {
4177 stack_init(state);
4178 init_registry(state)?;
4179 crate::string::init(state)?;
4180 crate::tagmethods::init(state)?;
4181 // TODO(port): luaX_init lives in the lua-lex crate; cross-crate call needed in Phase B
4182 state.global_mut().gcstp = 0;
4183 state.global().heap.unpause();
4184 // macros.tsv: setnilvalue → *o = LuaValue::Nil
4185 // PORT NOTE: setting nilvalue = Nil signals completestate() → is_complete() = true
4186 state.global_mut().nilvalue = LuaValue::Nil;
4187 // macros.tsv: luai_userstateopen → (extension hook, no-op default; drop)
4188 Ok(())
4189}
4190
4191fn preinit_thread(thread: &mut LuaState, global: Rc<RefCell<GlobalState>>) {
4192 thread.global = global;
4193 thread.stack = Vec::new();
4194 thread.call_info = Vec::new();
4195 // PORT NOTE: We initialize ci to 0 but call_info is empty; stack_init() must be
4196 // called before any use of call_info.
4197 thread.ci = CallInfoIdx(0);
4198 thread.nci = 0;
4199 // PORT NOTE: In C, L->twups = L is a self-reference sentinel meaning "no open upvals".
4200 // In Rust, GlobalState.twups is a Vec<GcRef<LuaState>>; absence from that Vec is the
4201 // sentinel. The per-thread `twups` field is removed (types.tsv: lua_State.twups → removed).
4202 thread.n_ccalls = 0;
4203 thread.hook = None;
4204 thread.hookmask = 0;
4205 thread.basehookcount = 0;
4206 thread.allowhook = true;
4207 // macros.tsv: resethookcount → state.reset_hook_count()
4208 thread.hookcount = thread.basehookcount;
4209
4210 // Sandbox inheritance: a coroutine joins the runtime-wide instruction/memory
4211 // budget so metering spans every thread, not just the main one. The budget
4212 // itself lives in `GlobalState` (shared); the new thread only needs the
4213 // count-hook mask armed so the dispatch loop traps and charges it.
4214 {
4215 let (active, interval) = {
4216 let g = thread.global.borrow();
4217 (g.sandbox_active(), g.sandbox.interval.get())
4218 };
4219 if active {
4220 thread.hookmask = SANDBOX_COUNT_MASK;
4221 thread.basehookcount = interval;
4222 thread.hookcount = interval;
4223 }
4224 }
4225 thread.openupval = Vec::new();
4226 thread.status = LuaStatus::Ok as u8;
4227 thread.errfunc = 0;
4228 thread.oldpc = 0;
4229 thread.gc_check_needed = true;
4230}
4231
4232fn close_state(state: &mut LuaState) {
4233 let is_complete = state.global().is_complete();
4234
4235 if !is_complete {
4236 // macros.tsv: luaC_freeallobjects via GcHandle
4237 state.gc().free_all_objects();
4238 } else {
4239 state.ci = CallInfoIdx(0);
4240 // TODO(port): crate::do_::close_protected(state, StackIdx(1), LuaStatus::Ok)
4241 // Ignoring result here because we are in teardown (same as C behavior).
4242 state.gc().free_all_objects();
4243 // macros.tsv: luai_userstateclose → (extension hook; drop)
4244 }
4245
4246 // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
4247 state.global_mut().strt = StringPool::default();
4248
4249 free_stack(state);
4250
4251 // PORT NOTE: C-specific memory accounting assertion; not applicable in Rust.
4252
4253 // PORT NOTE: Custom allocator freed LG here. Rust's allocator (via Drop) handles
4254 // deallocation of GlobalState and LuaState automatically.
4255}
4256
4257/// Create a new coroutine thread sharing the same GlobalState as the caller.
4258///
4259/// Pushes the new thread onto the caller's stack and returns `Ok(())`.
4260///
4261///
4262/// ```c
4263///
4264/// // global_State *g = G(L);
4265/// // GCObject *o;
4266/// // lua_State *L1;
4267/// // lua_lock(L); luaC_checkGC(L);
4268/// // o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l));
4269/// // L1 = gco2th(o);
4270/// // setthvalue2s(L, L->top.p, L1); api_incr_top(L);
4271/// // preinit_thread(L1, g);
4272/// // ... (copy hook settings, extra space, stack_init) ...
4273/// // lua_unlock(L); return L1;
4274/// // }
4275/// ```
4276/// Allocate a fresh coroutine `LuaState`, register it under a new
4277/// `ThreadId`, and push the resulting `LuaValue::Thread(value)` onto
4278/// `state`'s stack.
4279///
4280/// If `initial_body` is `Some(f)`, `f` is also pushed onto the new
4281/// thread's stack so that `coroutine.status` reports `"suspended"`
4282/// rather than `"dead"`. The full cross-thread `xmove` from caller to
4283/// coroutine arrives in slice 02b; `co_create` uses `initial_body` to
4284/// stage the body without needing a real `xmove`.
4285pub fn new_thread(state: &mut LuaState, initial_body: Option<LuaValue>) -> Result<(), LuaError> {
4286 state.gc().check_step();
4287
4288 // PORT NOTE: In C, the new thread is GC-allocated as part of the allgc list.
4289 // In Rust (Phase A), we create a plain LuaState; Phase D will wire GC registration.
4290 // TODO(port): allocate via state.gc().new_obj(LuaType::Thread, ...) in Phase D
4291
4292 let global_rc = state.global_rc();
4293 let hookmask = state.hookmask;
4294 let basehookcount = state.basehookcount;
4295
4296 let reserved_id = {
4297 let mut g = state.global_mut();
4298 let id = g.next_thread_id;
4299 g.next_thread_id += 1;
4300 id
4301 };
4302
4303 let mut new_thread = LuaState {
4304 status: LuaStatus::Ok as u8,
4305 allowhook: true,
4306 nci: 0,
4307 top: StackIdx(0),
4308 stack_last: StackIdx(0),
4309 stack: Vec::new(),
4310 ci: CallInfoIdx(0),
4311 call_info: Vec::new(),
4312 openupval: Vec::new(),
4313 tbclist: Vec::new(),
4314 global: global_rc.clone(),
4315 hook: None,
4316 hookmask: 0,
4317 basehookcount: 0,
4318 hookcount: 0,
4319 errfunc: 0,
4320 n_ccalls: 0,
4321 oldpc: 0,
4322 marked: 0,
4323 cached_thread_id: reserved_id,
4324 gc_check_needed: false,
4325 };
4326
4327 preinit_thread(&mut new_thread, global_rc);
4328
4329 new_thread.hookmask = hookmask;
4330 new_thread.basehookcount = basehookcount;
4331 // TODO(port): lua_Hook is Box<dyn FnMut(...)>; not Clone.
4332 // Sharing a hook between threads would require Arc<Mutex<...>> (Phase E debug).
4333 new_thread.reset_hook_count();
4334
4335 // macros.tsv: lua_getextraspace → state.extra_space_mut() → &mut [u8]
4336 // TODO(port): LuaState.extra_space field not yet defined; Phase B
4337
4338 // macros.tsv: luai_userstatethread → (extension hook; drop)
4339
4340 stack_init(&mut new_thread);
4341
4342 if let Some(body) = initial_body {
4343 new_thread.push(body);
4344 }
4345
4346 let thread_ref: Rc<RefCell<LuaState>> = Rc::new(RefCell::new(new_thread));
4347
4348 let value = {
4349 let mut g = state.global_mut();
4350 let id = reserved_id;
4351 let value = GcRef::new(lua_types::value::LuaThread::new(id));
4352 g.threads.insert(
4353 id,
4354 ThreadRegistryEntry { state: thread_ref, value: value.clone() },
4355 );
4356 value
4357 };
4358
4359 state.push(LuaValue::Thread(value));
4360
4361 Ok(())
4362}
4363
4364/// Reset a thread to its base state, closing all to-be-closed variables.
4365///
4366/// Returns the final status code as an `i32` (mirrors the C API).
4367///
4368///
4369/// ```c
4370///
4371/// // CallInfo *ci = L->ci = &L->base_ci;
4372/// // setnilvalue(s2v(L->stack.p));
4373/// // ci->func.p = L->stack.p;
4374/// // ci->callstatus = CIST_C;
4375/// // if (status == LUA_YIELD) status = LUA_OK;
4376/// // L->status = LUA_OK; /* so it can run __close metamethods */
4377/// // status = luaD_closeprotected(L, 1, status);
4378/// // if (status != LUA_OK) luaD_seterrorobj(L, status, L->stack.p + 1);
4379/// // else L->top.p = L->stack.p + 1;
4380/// // ci->top.p = L->top.p + LUA_MINSTACK;
4381/// // luaD_reallocstack(L, cast_int(ci->top.p - L->stack.p), 0);
4382/// // return status;
4383/// // }
4384/// ```
4385pub fn reset_thread(state: &mut LuaState, status: i32) -> i32 {
4386 state.ci = CallInfoIdx(0);
4387 let ci_idx = 0usize;
4388
4389 // macros.tsv: setnilvalue → *o = LuaValue::Nil; s2v → state.stack_at(idx)
4390 if !state.stack.is_empty() {
4391 state.stack[0].val = LuaValue::Nil;
4392 }
4393
4394 state.call_info[ci_idx].func = StackIdx(0);
4395 state.call_info[ci_idx].callstatus = CIST_C;
4396
4397 let mut status = if status == LuaStatus::Yield as i32 {
4398 LuaStatus::Ok as i32
4399 } else {
4400 status
4401 };
4402
4403 state.status = LuaStatus::Ok as u8;
4404
4405 let close_status = crate::do_::close_protected(
4406 state,
4407 StackIdx(1),
4408 LuaStatus::from_raw(status),
4409 );
4410 status = close_status as i32;
4411
4412 if status != LuaStatus::Ok as i32 {
4413 crate::do_::set_error_obj(state, LuaStatus::from_raw(status), StackIdx(1));
4414 } else {
4415 state.top = StackIdx(1);
4416 }
4417
4418 let new_ci_top = StackIdx(state.top.0 + LUA_MINSTACK as u32);
4419 state.call_info[ci_idx].top = new_ci_top;
4420
4421 // TODO(port): crate::do_::realloc_stack(state, new_ci_top.0 as i32, 0) — ldo.c → do_.rs
4422 // For Phase A, grow the stack if needed to at least new_ci_top slots.
4423 let needed = new_ci_top.0 as usize;
4424 if state.stack.len() < needed {
4425 state.stack.resize(needed, StackValue::default());
4426 }
4427
4428 status
4429}
4430
4431/// Close a coroutine thread from the perspective of another thread.
4432///
4433///
4434/// ```c
4435///
4436/// // int status;
4437/// // lua_lock(L);
4438/// // L->n_ccalls = (from) ? getCcalls(from) : 0;
4439/// // status = luaE_resetthread(L, L->status);
4440/// // lua_unlock(L);
4441/// // return status;
4442/// // }
4443/// ```
4444pub fn close_thread(state: &mut LuaState, from: Option<&LuaState>) -> i32 {
4445 // macros.tsv: getCcalls → state.c_calls()
4446 state.n_ccalls = match from {
4447 Some(f) => f.c_calls(),
4448 None => 0,
4449 };
4450 let current_status = state.status as i32;
4451 let result = reset_thread(state, current_status);
4452 result
4453}
4454
4455/// Deprecated wrapper for `close_thread(L, NULL)`.
4456///
4457///
4458/// ```c
4459///
4460/// // return lua_closethread(L, NULL);
4461/// // }
4462/// ```
4463pub fn reset_thread_api(state: &mut LuaState) -> i32 {
4464 close_thread(state, None)
4465}
4466
4467/// Create a new independent Lua state. Returns `None` only on OOM.
4468///
4469///
4470/// PORT NOTE: The C API takes a custom allocator `(f, ud)`. The Rust-native API
4471/// uses the global Rust allocator; those parameters are dropped. Equivalent to
4472/// `LuaState::new()` at the call site.
4473///
4474/// ```c
4475///
4476/// // int i;
4477/// // lua_State *L;
4478/// // global_State *g;
4479/// // LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
4480/// // if (l == NULL) return NULL;
4481/// // L = &l->l.l; g = &l->g;
4482/// // L->tt = LUA_VTHREAD;
4483/// // g->currentwhite = bitmask(WHITE0BIT);
4484/// // L->marked = luaC_white(g);
4485/// // preinit_thread(L, g);
4486/// // g->allgc = obj2gco(L);
4487/// // L->next = NULL;
4488/// // incnny(L);
4489/// // g->frealloc = f; g->ud = ud; g->warnf = NULL; g->ud_warn = NULL;
4490/// // g->mainthread = L; g->seed = luai_makeseed(L);
4491/// // g->gcstp = GCSTPGC;
4492/// // ... (zero-init all GC list pointers and tunables) ...
4493/// // setivalue(&g->nilvalue, 0); /* signal: state not yet built */
4494/// // ... (setgcparam tunables) ...
4495/// // for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
4496/// // if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
4497/// // close_state(L); L = NULL;
4498/// // }
4499/// // return L;
4500/// // }
4501/// ```
4502pub fn new_state() -> Option<LuaState> {
4503 // In Rust, allocation failure panics by default; we use Result internally.
4504
4505 // Build a dummy LuaString for memerrmsg and strcache initialization.
4506 // This is a chicken-and-egg problem: GlobalState.memerrmsg needs to be initialized
4507 // before luaS_init, but luaS_init creates the memerrmsg.
4508 // We use a placeholder Rc<LuaString> that will be replaced by luaS_init.
4509 // TODO(port): this is fragile; Phase B should ensure memerrmsg is properly set by luaS_init.
4510 // TODO(D-1c-bridge): allocation outside state context (new_state() free fn — no LuaState yet)
4511 let placeholder_str = GcRef::new(LuaString::placeholder());
4512
4513 // macros.tsv: bitmask → (1u32 << b); WHITE0BIT = 0 → 1u8
4514 let initial_white = 1u8 << WHITE0BIT;
4515
4516 // macros.tsv: setivalue → *o = LuaValue::Int(x)
4517 // PORT NOTE: non-nil nilvalue signals "state not yet complete"; see is_complete().
4518
4519 let global = GlobalState {
4520 parser_hook: None,
4521 lua_version: lua_types::LuaVersion::default(),
4522 file_loader_hook: None,
4523 file_open_hook: None,
4524 stdout_hook: None,
4525 stderr_hook: None,
4526 stdin_hook: None,
4527 env_hook: None,
4528 unix_time_hook: None,
4529 cpu_clock_hook: None,
4530 local_offset_hook: None,
4531 entropy_hook: None,
4532 temp_name_hook: None,
4533 popen_hook: None,
4534 file_remove_hook: None,
4535 file_rename_hook: None,
4536 os_execute_hook: None,
4537 dynlib_load_hook: None,
4538 dynlib_symbol_hook: None,
4539 dynlib_unload_hook: None,
4540 totalbytes: std::mem::size_of::<GlobalState>() as isize,
4541 sandbox: SandboxLimits::default(),
4542 gc_debt: 0,
4543 gc_estimate: 0,
4544 lastatomic: 0,
4545 strt: StringPool::default(),
4546 l_registry: LuaValue::Nil,
4547 external_roots: ExternalRootSet::default(),
4548 globals: LuaValue::Nil,
4549 loaded: LuaValue::Nil,
4550 nilvalue: LuaValue::Int(0),
4551 seed: make_seed(),
4552 currentwhite: initial_white,
4553 gcstate: GCS_PAUSE,
4554 // macros.tsv: KGC_INC → GcKind::Incremental
4555 gckind: GcKind::Incremental as u8,
4556 gcstopem: false,
4557 genminormul: LUAI_GENMINORMUL,
4558 // macros.tsv: setgcparam → p = v / 4
4559 genmajormul: (LUAI_GENMAJORMUL / 4) as u8,
4560 gcstp: GCSTPGC,
4561 gcemergency: false,
4562 gcpause: (LUAI_GCPAUSE / 4) as u8,
4563 gcstepmul: (LUAI_GCMUL / 4) as u8,
4564 gcstepsize: LUAI_GCSTEPSIZE,
4565 sweepgc_cursor: 0,
4566 weak_tables_registry: Vec::new(),
4567 gc_tracked_long_strings: Vec::new(),
4568 pending_finalizers: Vec::new(),
4569 to_be_finalized: Vec::new(),
4570 twups: Vec::new(),
4571 panic: None,
4572 mainthread: None,
4573 threads: std::collections::HashMap::new(),
4574 main_thread_value: GcRef::new(lua_types::value::LuaThread::new(0)),
4575 current_thread_id: 0,
4576 main_thread_id: 0,
4577 next_thread_id: 1,
4578 memerrmsg: placeholder_str.clone(),
4579 tmname: Vec::new(),
4580 mt: std::array::from_fn(|_| None),
4581 strcache: std::array::from_fn(|_| {
4582 std::array::from_fn(|_| placeholder_str.clone())
4583 }),
4584 interned_lt: std::collections::HashMap::new(),
4585 warnf: None,
4586 c_functions: Vec::new(),
4587 heap: lua_gc::Heap::new(),
4588 cross_thread_upvals: std::collections::HashMap::new(),
4589 suspended_parent_stacks: Vec::new(),
4590 suspended_parent_open_upvals: Vec::new(),
4591 };
4592
4593 let global_rc = Rc::new(RefCell::new(global));
4594
4595 // macros.tsv: luaC_white → g.current_white()
4596 let initial_marked = initial_white;
4597
4598 let mut main_thread = LuaState {
4599 status: LuaStatus::Ok as u8,
4600 allowhook: true,
4601 nci: 0,
4602 top: StackIdx(0),
4603 stack_last: StackIdx(0),
4604 stack: Vec::new(),
4605 ci: CallInfoIdx(0),
4606 call_info: Vec::new(),
4607 openupval: Vec::new(),
4608 tbclist: Vec::new(),
4609 global: global_rc.clone(),
4610 hook: None,
4611 hookmask: 0,
4612 basehookcount: 0,
4613 hookcount: 0,
4614 errfunc: 0,
4615 n_ccalls: 0,
4616 oldpc: 0,
4617 marked: initial_marked,
4618 cached_thread_id: 0,
4619 gc_check_needed: false,
4620 };
4621
4622 preinit_thread(&mut main_thread, global_rc.clone());
4623
4624 // macros.tsv: incnny → state.inc_nny() → L->n_ccalls += 0x10000
4625 main_thread.inc_nny();
4626
4627 // TODO(port): self-referential Rc cycle; Phase D GC handles cycles.
4628 // For Phase A: skip setting mainthread to avoid the cycle.
4629
4630 // TODO(port): Phase D — register main_thread in allgc as a GcRef
4631
4632 // close_state(L); L = NULL; }
4633 // error_sites.tsv: luaD_rawrunprotected → state.run_protected(|s| f(s, ud))
4634 // PORT NOTE: We call lua_open directly since we're not using the protected-call
4635 // machinery yet (ldo.c is not ported). Errors from lua_open propagate as Err.
4636 match lua_open(&mut main_thread) {
4637 Ok(()) => {}
4638 Err(_) => {
4639 close_state(&mut main_thread);
4640 return None;
4641 }
4642 }
4643
4644 Some(main_thread)
4645}
4646
4647/// Close the Lua state and free all resources.
4648///
4649///
4650/// PORT NOTE: In C, `lua_close` gets the main thread via `G(L)->mainthread`
4651/// and closes that regardless of which thread is passed. In Rust, the caller
4652/// should hold the main `LuaState` and drop it (which triggers `close_state`
4653/// via this function or `Drop`).
4654///
4655/// ```c
4656///
4657/// // lua_lock(L);
4658/// // L = G(L)->mainthread; /* only the main thread can be closed */
4659/// // close_state(L);
4660/// // }
4661/// ```
4662pub fn close(mut state: LuaState) {
4663 // PORT NOTE: In Rust, callers must pass the main LuaState directly (or obtain it
4664 // from GlobalState.mainthread). We do not traverse to the main thread here;
4665 // the caller owns the root state.
4666 // TODO(port): assert that `state` is indeed the main thread before closing
4667 close_state(&mut state);
4668}
4669
4670/// Forward a warning message through the configured warning sink.
4671///
4672///
4673/// ```c
4674///
4675/// // lua_WarnFunction wf = G(L)->warnf;
4676/// // if (wf != NULL) wf(G(L)->ud_warn, msg, tocont);
4677/// // }
4678/// ```
4679pub(crate) fn warning(state: &mut LuaState, msg: &[u8], to_cont: bool) {
4680 // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
4681 // types.tsv: global_State.ud_warn → (removed; folded into the closure)
4682 // PORT NOTE: We must drop the RefMut borrow before calling the closure to avoid
4683 // a potential re-entrant borrow_mut() if the closure calls back into Lua.
4684 // We check for the presence of warnf while holding a borrow, then call it.
4685 // TODO(port): if the warning function needs to call back into state (e.g. to push
4686 // a Lua error), this will panic at runtime due to RefCell re-entry. Phase B should
4687 // design a safe re-entrance pattern (e.g. take + restore the warnf closure).
4688 let has_warnf = state.global().warnf.is_some();
4689 if has_warnf {
4690 // Take the warnf closure out to avoid re-entrant borrow.
4691 let mut warnf = state.global_mut().warnf.take();
4692 if let Some(ref mut f) = warnf {
4693 f(msg, to_cont);
4694 }
4695 // Restore the closure.
4696 state.global_mut().warnf = warnf;
4697 }
4698}
4699
4700#[cfg(test)]
4701mod tests {
4702 use super::*;
4703
4704 #[test]
4705 fn external_root_keys_reject_stale_slot_after_reuse() {
4706 let mut roots = ExternalRootSet::default();
4707
4708 let first = roots.insert(LuaValue::Int(1));
4709 assert_eq!(roots.len(), 1);
4710 assert_eq!(roots.get(first), Some(&LuaValue::Int(1)));
4711
4712 assert_eq!(roots.remove(first), Some(LuaValue::Int(1)));
4713 assert!(roots.get(first).is_none());
4714 assert!(roots.remove(first).is_none());
4715 assert_eq!(roots.len(), 0);
4716 assert_eq!(roots.vacant_len(), 1);
4717 assert!(roots.replace(first, LuaValue::Int(9)).is_none());
4718 assert!(roots.is_empty());
4719
4720 let second = roots.insert(LuaValue::Int(2));
4721 assert_eq!(first.index, second.index);
4722 assert_ne!(first, second);
4723 assert!(roots.get(first).is_none());
4724 assert_eq!(roots.get(second), Some(&LuaValue::Int(2)));
4725 assert!(roots.replace(first, LuaValue::Int(3)).is_none());
4726 }
4727
4728 #[test]
4729 fn external_roots_keep_heap_value_alive_until_unrooted() {
4730 let mut state = new_state().expect("state should initialize");
4731 let _heap_guard = {
4732 let g = state.global();
4733 lua_gc::HeapGuard::push(&g.heap)
4734 };
4735
4736 let table = state.new_table();
4737 assert_eq!(state.global().heap.allgc_count(), 1);
4738
4739 let key = state.external_root_value(LuaValue::Table(table));
4740 state.gc().full_collect();
4741 assert_eq!(state.global().heap.allgc_count(), 1);
4742 assert_eq!(state.global().external_roots.len(), 1);
4743
4744 assert!(state.external_unroot_value(key).is_some());
4745 state.gc().full_collect();
4746 assert_eq!(state.global().heap.allgc_count(), 0);
4747 assert!(state.global().external_roots.is_empty());
4748 }
4749}
4750
4751// ──────────────────────────────────────────────────────────────────────────────
4752// PORT STATUS
4753// source: src/lstate.c (445 lines, 25 functions)
4754// src/lstate.h (408 lines; struct definitions merged)
4755// target_crate: lua-vm
4756// confidence: medium
4757// todos: 44
4758// port_notes: 34
4759// unsafe_blocks: 0 (must be 0 outside explicit unsafe-budget crates)
4760// notes: Logic faithfully follows lstate.c. Key structural changes:
4761// (1) LX/LG C layout wrappers dropped; GlobalState is Rc<RefCell<>>.
4762// (2) CallInfo linked list → Vec<CallInfo> with CallInfoIdx indices;
4763// shrink_ci uses truncation rather than node-by-node removal.
4764// (3) lua_State.twups self-reference → membership in GlobalState.twups Vec.
4765// (4) errorJmp/setjmp → removed; errors use Result<T, LuaError>.
4766// (5) Custom allocator (lua_Alloc) → dropped; Rust's allocator handles it.
4767// (6) make_seed: ASLR pointer entropy requires unsafe; time-only for Phase A.
4768// (7) Perf: LuaState.cached_thread_id stores the thread's own id once at
4769// construction; upvalue_get/_set compare against this u64 field
4770// instead of borrowing global.current_thread_id on every read.
4771// Invariant survives coroutine resume because each thread caches its
4772// OWN id, not the global's id (see field doc on cached_thread_id).
4773// (8) Perf: LuaTableRefExt::{raw_set, raw_set_int, get, get_int,
4774// get_short_str, metatable, as_ptr} and table_{raw,set_with_tm,
4775// array_set} carry #[inline] so the per-set dispatch chain
4776// collapses into set_i_value / vm.rs OP_SETI callers. The
4777// historical reject_invalid_table_key precheck moved into
4778// LuaTable::try_raw_set (lua-types) and was dropped at this
4779// layer; raw_set now takes the key by value, eliminating a
4780// 24-byte LuaValue clone per set. gc_barrier_back is invoked
4781// before the store in table_set_with_tm (semantically
4782// equivalent: the barrier only inspects the value's color,
4783// not its location), letting v be moved directly into
4784// table_raw_set without an intermediate clone.
4785// Key TODOs: luaT_init and luaX_init cross-crate calls (Phase B);
4786// init_registry table mutations through Rc (needs RefCell<LuaTable>);
4787// luaD_closeprotected/seterrorobj/reallocstack in reset_thread (ldo.c);
4788// GcRef<LuaState> self-reference for mainthread (Phase D);
4789// LuaString::placeholder() helper needed for GlobalState init;
4790// LuaValue and LuaTable should move to object.rs once that lands.
4791// ──────────────────────────────────────────────────────────────────────────────