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