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