Skip to main content

lua_vm/
object.rs

1//! Generic functions over Lua objects.
2//!
3//! Ported from `reference/lua-5.4.7/src/lobject.c` (602 lines, ~20 functions).
4
5// TODO(port): resolve import paths — all `crate::*` paths below are speculative;
6// Phase B will reconcile against the actual module tree.
7use crate::state::LuaState;
8#[allow(unused_imports)] use crate::prelude::*;
9use lua_types::{LuaValue, GcRef, LuaString, StackIdx};
10use lua_types::error::LuaError;
11use lua_types::arith::ArithOp;
12use lua_types::value::F2Imod;
13
14// ──────────────────────────────────────────────────────────────────────────
15// Module-level constants
16// ──────────────────────────────────────────────────────────────────────────
17
18/// Maximum number of significant hex digits to read (avoids overflow even for
19/// single-precision floats).
20/// C: `#define MAXSIGDIG 30`
21const MAX_SIG_DIG: usize = 30;
22
23/// Maximum length of a numeral string accepted for conversion to a number.
24/// C: `#define L_MAXLENNUM 200`
25const L_MAX_LEN_NUM: usize = 200;
26
27/// Maximum size of a number-to-string conversion buffer.
28/// Accommodates both `%.14g` float formatting and `%lld` integer formatting.
29/// C: `#define MAXNUMBER2STR 44`
30pub const MAX_NUMBER_2_STR: usize = 44;
31
32/// Buffer size (bytes) for UTF-8 encoding; encoded backwards into this buffer.
33/// C: `#define UTF8BUFFSZ 8`
34pub const UTF8_BUF_SZ: usize = 8;
35
36/// Maximum length of a chunk source identifier in error messages.
37/// C: `LUA_IDSIZE` (typically 60 in luaconf.h).
38// TODO(port): verify against luaconf.h; defaulting to 60 here.
39pub const LUA_ID_SIZE: usize = 60;
40
41/// Internal buffer size for `push_vfstring`.
42/// C: `#define BUFVFS (LUA_IDSIZE + MAXNUMBER2STR + 95)`
43const BUF_VFS: usize = LUA_ID_SIZE + MAX_NUMBER_2_STR + 95;
44
45/// Truncation marker for long chunk source strings.
46/// C: `#define RETS "..."`
47const RETS: &[u8] = b"...";
48
49/// Prefix for [string "..."] chunk identifiers.
50/// C: `#define PRE "[string \""`
51const PRE: &[u8] = b"[string \"";
52
53/// Suffix for [string "..."] chunk identifiers.
54/// C: `#define POS "\"]"`
55const POS: &[u8] = b"\"]";
56
57// ──────────────────────────────────────────────────────────────────────────
58// ceil_log2
59// ──────────────────────────────────────────────────────────────────────────
60
61/// Computes `ceil(log2(x))`; returns the minimum `k` such that `2^k >= x`.
62///
63/// C: `int luaO_ceillog2 (unsigned int x)`
64pub fn ceil_log2(x: u32) -> i32 {
65    // C: static const lu_byte log_2[256] = { /* log_2[i] = ceil(log2(i - 1)) */ ... }
66    static LOG_2: [u8; 256] = [
67        0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
68        6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
69        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
70        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
71        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
72        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
73        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
74        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
75    ];
76    // C: int l = 0; x--; while (x >= 256) { l += 8; x >>= 8; } return l + log_2[x];
77    let mut l: i32 = 0;
78    let mut x = x.wrapping_sub(1);
79    while x >= 256 {
80        l += 8;
81        x >>= 8;
82    }
83    l + LOG_2[x as usize] as i32
84}
85
86// ──────────────────────────────────────────────────────────────────────────
87// Integer arithmetic dispatcher
88// ──────────────────────────────────────────────────────────────────────────
89
90/// Performs integer arithmetic for opcode `op` on operands `v1`, `v2`.
91/// Returns `Result` because floor-mod and floor-div can raise on zero divisor.
92///
93/// C: `static lua_Integer intarith (lua_State *L, int op, lua_Integer v1, lua_Integer v2)`
94fn int_arith(state: &mut LuaState, op: ArithOp, v1: i64, v2: i64) -> Result<i64, LuaError> {
95    match op {
96        // C: case LUA_OPADD: return intop(+, v1, v2);
97        ArithOp::Add => Ok((v1 as u64).wrapping_add(v2 as u64) as i64),
98        // C: case LUA_OPSUB: return intop(-, v1, v2);
99        ArithOp::Sub => Ok((v1 as u64).wrapping_sub(v2 as u64) as i64),
100        // C: case LUA_OPMUL: return intop(*, v1, v2);
101        ArithOp::Mul => Ok((v1 as u64).wrapping_mul(v2 as u64) as i64),
102        // C: case LUA_OPMOD: return luaV_mod(L, v1, v2);
103        // TODO(port): confirm function path for integer floor-mod in lvm.rs
104        ArithOp::Mod => crate::vm::int_floor_mod(state, v1, v2),
105        // C: case LUA_OPIDIV: return luaV_idiv(L, v1, v2);
106        // TODO(port): confirm function path for integer floor-div in lvm.rs
107        ArithOp::Idiv => crate::vm::int_floor_div(state, v1, v2),
108        // C: case LUA_OPBAND: return intop(&, v1, v2);
109        ArithOp::Band => Ok(v1 & v2),
110        // C: case LUA_OPBOR:  return intop(|, v1, v2);
111        ArithOp::Bor => Ok(v1 | v2),
112        // C: case LUA_OPBXOR: return intop(^, v1, v2);
113        ArithOp::Bxor => Ok(v1 ^ v2),
114        // C: case LUA_OPSHL: return luaV_shiftl(v1, v2);
115        // TODO(port): confirm function path for shift-left in lvm.rs
116        ArithOp::Shl => Ok(crate::vm::shiftl(v1, v2)),
117        // C: case LUA_OPSHR: return luaV_shiftr(v1, v2);  [which is shiftl(v1, -v2)]
118        ArithOp::Shr => Ok(crate::vm::shiftl(v1, -v2)),
119        // C: case LUA_OPUNM: return intop(-, 0, v1);
120        ArithOp::Unm => Ok((0u64).wrapping_sub(v1 as u64) as i64),
121        // C: case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1);
122        //    l_castS2U(0) → 0u64, ~0u64 = 0xFFFFFFFFFFFFFFFF = !0u64
123        ArithOp::Bnot => Ok((!0u64 ^ v1 as u64) as i64),
124        // C: default: lua_assert(0); return 0;
125        _ => {
126            debug_assert!(false, "int_arith called with non-integer op");
127            Ok(0)
128        }
129    }
130}
131
132// ──────────────────────────────────────────────────────────────────────────
133// Float arithmetic dispatcher
134// ──────────────────────────────────────────────────────────────────────────
135
136/// Performs float arithmetic for opcode `op` on operands `v1`, `v2`.
137/// Returns `Result` because float floor-mod can raise on zero divisor.
138///
139/// C: `static lua_Number numarith (lua_State *L, int op, lua_Number v1, lua_Number v2)`
140fn float_arith(state: &mut LuaState, op: ArithOp, v1: f64, v2: f64) -> Result<f64, LuaError> {
141    match op {
142        // C: case LUA_OPADD: return luai_numadd(L, v1, v2);
143        ArithOp::Add => Ok(v1 + v2),
144        // C: case LUA_OPSUB: return luai_numsub(L, v1, v2);
145        ArithOp::Sub => Ok(v1 - v2),
146        // C: case LUA_OPMUL: return luai_nummul(L, v1, v2);
147        ArithOp::Mul => Ok(v1 * v2),
148        // C: case LUA_OPDIV: return luai_numdiv(L, v1, v2);
149        ArithOp::Div => Ok(v1 / v2),
150        // C: case LUA_OPPOW: return luai_numpow(L, v1, v2);
151        ArithOp::Pow => Ok(if v2 == 2.0 { v1 * v1 } else { v1.powf(v2) }),
152        // C: case LUA_OPIDIV: return luai_numidiv(L, v1, v2);
153        ArithOp::Idiv => Ok((v1 / v2).floor()),
154        // C: case LUA_OPUNM: return luai_numunm(L, v1);
155        ArithOp::Unm => Ok(-v1),
156        // C: case LUA_OPMOD: return luaV_modf(L, v1, v2);
157        // TODO(port): confirm function path for float floor-mod in lvm.rs
158        ArithOp::Mod => crate::vm::float_floor_mod(state, v1, v2),
159        // C: default: lua_assert(0); return 0;
160        _ => {
161            debug_assert!(false, "float_arith called with non-float op");
162            Ok(0.0)
163        }
164    }
165}
166
167// ──────────────────────────────────────────────────────────────────────────
168// Raw arithmetic (no metamethods)
169// ──────────────────────────────────────────────────────────────────────────
170
171/// Attempts raw (no-metamethod) arithmetic on two Lua values.
172/// Writes the result to `res` and returns `true` on success, `false` if the
173/// operation cannot be performed with the given types (caller should invoke
174/// a metamethod instead).
175///
176/// C: `int luaO_rawarith (lua_State *L, int op, const TValue *p1, const TValue *p2, TValue *res)`
177pub fn raw_arith(
178    state: &mut LuaState,
179    op: ArithOp,
180    p1: &LuaValue,
181    p2: &LuaValue,
182    res: &mut LuaValue,
183) -> Result<bool, LuaError> {
184    match op {
185        // C: case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR:
186        // case LUA_OPSHL: case LUA_OPSHR: case LUA_OPBNOT: — integer-only ops
187        ArithOp::Band | ArithOp::Bor | ArithOp::Bxor
188        | ArithOp::Shl | ArithOp::Shr | ArithOp::Bnot => {
189            // C: if (tointegerns(p1, &i1) && tointegerns(p2, &i2)) {
190            //        setivalue(res, intarith(L, op, i1, i2));  return 1; }
191            //    else return 0;
192            if let (Some(i1), Some(i2)) = (
193                p1.to_integer_no_strconv(),
194                p2.to_integer_no_strconv(),
195            ) {
196                *res = LuaValue::Int(int_arith(state, op, i1, i2)?);
197                Ok(true)
198            } else {
199                Ok(false)
200            }
201        }
202
203        // C: case LUA_OPDIV: case LUA_OPPOW: — float-only ops
204        ArithOp::Div | ArithOp::Pow => {
205            // C: if (tonumberns(p1, n1) && tonumberns(p2, n2)) {
206            //        setfltvalue(res, numarith(L, op, n1, n2));  return 1; }
207            //    else return 0;
208            if let (Some(n1), Some(n2)) = (
209                p1.to_number_no_strconv(),
210                p2.to_number_no_strconv(),
211            ) {
212                *res = LuaValue::Float(float_arith(state, op, n1, n2)?);
213                Ok(true)
214            } else {
215                Ok(false)
216            }
217        }
218
219        // C: default: — prefer integer if both operands are integers; else try float.
220        _ => {
221            // C: if (ttisinteger(p1) && ttisinteger(p2)) {
222            //        setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2)));  return 1; }
223            if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (p1, p2) {
224                *res = LuaValue::Int(int_arith(state, op, *i1, *i2)?);
225                return Ok(true);
226            }
227            // C: else if (tonumberns(p1, n1) && tonumberns(p2, n2)) { ... }
228            if let (Some(n1), Some(n2)) = (
229                p1.to_number_no_strconv(),
230                p2.to_number_no_strconv(),
231            ) {
232                *res = LuaValue::Float(float_arith(state, op, n1, n2)?);
233                Ok(true)
234            } else {
235                // C: else return 0;
236                Ok(false)
237            }
238        }
239    }
240}
241
242// ──────────────────────────────────────────────────────────────────────────
243// Arithmetic (with metamethod fallback)
244// ──────────────────────────────────────────────────────────────────────────
245
246/// Performs arithmetic for opcode `op`, writing the result to the stack slot
247/// `res`.  Falls back to a binary tag-method if raw arithmetic is not possible.
248///
249/// C: `void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2, StkId res)`
250pub fn arith(
251    state: &mut LuaState,
252    op: ArithOp,
253    p1: &LuaValue,
254    p2: &LuaValue,
255    res: StackIdx,
256) -> Result<(), LuaError> {
257    // C: if (!luaO_rawarith(L, op, p1, p2, s2v(res))) {
258    //        luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD)); }
259    //
260    // PORT NOTE: raw_arith writes to a local `temp` first; we then set the stack
261    // slot.  This avoids holding a &mut borrow into the stack across try_bin_tm,
262    // which would violate the StackIdx rule (PORTING.md §2 #5).
263    let mut temp = LuaValue::Nil;
264    if raw_arith(state, op, p1, p2, &mut temp)? {
265        state.set_at(res, temp);
266    } else {
267        let _ = (p1, p2);
268        return Err(LuaError::runtime(format_args!(
269            "arithmetic metamethod dispatch not yet implemented for opcode {:?}", op
270        )));
271    }
272    Ok(())
273}
274
275// ──────────────────────────────────────────────────────────────────────────
276// hex_value
277// ──────────────────────────────────────────────────────────────────────────
278
279/// Converts a hexadecimal digit byte to its numeric value (0–15).
280/// Caller must ensure `c` is a valid hex digit.
281///
282/// C: `int luaO_hexavalue (int c)`
283pub fn hex_value(c: u8) -> u8 {
284    // C: if (lisdigit(c)) return c - '0'; else return (ltolower(c) - 'a') + 10;
285    if c.is_ascii_digit() {
286        c - b'0'
287    } else {
288        c.to_ascii_lowercase() - b'a' + 10
289    }
290}
291
292// ──────────────────────────────────────────────────────────────────────────
293// Sign helper
294// ──────────────────────────────────────────────────────────────────────────
295
296/// Checks for and consumes a leading sign byte (`+` or `-`) in `s` starting
297/// at `*idx`.  Returns `true` if a minus sign was consumed.
298///
299/// C: `static int isneg (const char **s)`
300fn is_neg(s: &[u8], idx: &mut usize) -> bool {
301    // C: if (**s == '-') { (*s)++; return 1; }
302    //    else if (**s == '+') (*s)++;
303    //    return 0;
304    if *idx < s.len() && s[*idx] == b'-' {
305        *idx += 1;
306        return true;
307    }
308    if *idx < s.len() && s[*idx] == b'+' {
309        *idx += 1;
310    }
311    false
312}
313
314// ──────────────────────────────────────────────────────────────────────────
315// Hexadecimal float parser
316// ──────────────────────────────────────────────────────────────────────────
317
318/// Converts a hexadecimal float literal (C99 `0x…p…` form) in `s` to `f64`.
319/// Returns `Some((value, end_index))` on success, `None` on failure.
320///
321/// C: `static lua_Number lua_strx2number (const char *s, char **endptr)`
322/// (conditionally compiled when the platform doesn't provide it)
323fn str_x2number(s: &[u8]) -> Option<(f64, usize)> {
324    let mut idx = 0;
325    // C: while (lisspace(cast_uchar(*s))) s++;  — skip leading spaces
326    while idx < s.len() && s[idx].is_ascii_whitespace() {
327        idx += 1;
328    }
329    // C: neg = isneg(&s);
330    let neg = is_neg(s, &mut idx);
331    // C: if (!(*s == '0' && (*(s + 1) == 'x' || *(s + 1) == 'X'))) return 0.0;
332    if idx + 1 >= s.len() || s[idx] != b'0' || (s[idx + 1] != b'x' && s[idx + 1] != b'X') {
333        return None;
334    }
335    // C: for (s += 2; ; s++) { ... }  — skip '0x' and read mantissa digits
336    idx += 2;
337    let mut r: f64 = 0.0;
338    let mut sigdig: usize = 0;
339    let mut nosigdig: usize = 0;
340    let mut e: i32 = 0;
341    let mut hasdot = false;
342
343    // PORT NOTE: `lua_getlocaledecpoint()` returns the locale decimal separator.
344    // Rust has no locale; we always treat '.' as the separator here.
345    let dot = b'.';
346
347    loop {
348        if idx >= s.len() {
349            break;
350        }
351        let ch = s[idx];
352        if ch == dot {
353            // C: if (hasdot) break; else hasdot = 1;
354            if hasdot {
355                break;
356            }
357            hasdot = true;
358        } else if ch.is_ascii_hexdigit() {
359            // C: if (sigdig == 0 && *s == '0') nosigdig++;
360            //    else if (++sigdig <= MAXSIGDIG) r = (r * 16.0) + luaO_hexavalue(*s);
361            //    else e++;
362            //    if (hasdot) e--;
363            if sigdig == 0 && ch == b'0' {
364                nosigdig += 1;
365            } else if {
366                sigdig += 1;
367                sigdig <= MAX_SIG_DIG
368            } {
369                r = r * 16.0 + hex_value(ch) as f64;
370            } else {
371                e += 1;
372            }
373            if hasdot {
374                e -= 1;
375            }
376        } else {
377            break;
378        }
379        idx += 1;
380    }
381
382    // C: if (nosigdig + sigdig == 0) return 0.0;  — no digits at all
383    if nosigdig + sigdig == 0 {
384        return None;
385    }
386    // `idx` is now the valid end so far
387    let valid_end = idx;
388    // C: e *= 4;  — each hex digit is 4 bits
389    e *= 4;
390
391    // C: if (*s == 'p' || *s == 'P') { ... read exponent ... }
392    if idx < s.len() && (s[idx] == b'p' || s[idx] == b'P') {
393        idx += 1; // skip 'p'/'P'
394        let neg1 = is_neg(s, &mut idx);
395        // C: if (!lisdigit(cast_uchar(*s))) return 0.0;
396        if idx >= s.len() || !s[idx].is_ascii_digit() {
397            return None;
398        }
399        let mut exp1: i32 = 0;
400        // C: while (lisdigit(cast_uchar(*s))) exp1 = exp1 * 10 + *(s++) - '0';
401        while idx < s.len() && s[idx].is_ascii_digit() {
402            exp1 = exp1 * 10 + (s[idx] - b'0') as i32;
403            idx += 1;
404        }
405        if neg1 {
406            exp1 = -exp1;
407        }
408        e += exp1;
409        // update valid end: the exponent consumed up to here
410        // (valid_end is updated to idx below)
411    }
412    // C: if (neg) r = -r;
413    // C: return l_mathop(ldexp)(r, e);
414    let result = if neg { -r } else { r };
415    Some((result * (2.0f64).powi(e), idx))
416}
417
418// ──────────────────────────────────────────────────────────────────────────
419// String-to-float helpers
420// ──────────────────────────────────────────────────────────────────────────
421
422/// Inner conversion: tries to parse the bytes `s` as a float using the given
423/// `mode` (`b'x'` for hex, anything else for decimal).
424/// Returns `Some((value, end_index))` or `None`.
425///
426/// C: `static const char *l_str2dloc (const char *s, lua_Number *result, int mode)`
427fn str2dloc(s: &[u8], mode: u8) -> Option<(f64, usize)> {
428    // C: *result = (mode == 'x') ? lua_strx2number(s, &endptr) : lua_str2number(s, &endptr);
429    let (result, end) = if mode == b'x' {
430        str_x2number(s)?
431    } else {
432        // C: lua_str2number(s, &endptr)  — essentially strtod.
433        // PORT NOTE: from_utf8 used here because numeric string literals are
434        // guaranteed to be ASCII (a strict subset of UTF-8).
435        // TODO(port): replace with a bytes-native float parser in Phase B
436        // (e.g., the `fast-float` crate) to satisfy the from_utf8 ban fully.
437        let text = core::str::from_utf8(s).ok()?;
438        let trimmed = text.trim();
439        // Reject "inf", "infinity", "nan" — Lua does not accept these.
440        let lower = trimmed.to_ascii_lowercase();
441        if lower.starts_with("inf") || lower.starts_with("nan") {
442            return None;
443        }
444        let f: f64 = trimmed.parse().ok()?;
445        (f, s.len()) // strtod parses as many chars as possible; we consumed all
446    };
447    // C: if (endptr == s) return NULL;  — nothing recognized
448    if end == 0 {
449        return None;
450    }
451    // C: while (lisspace(cast_uchar(*endptr))) endptr++;
452    let mut end2 = end;
453    while end2 < s.len() && s[end2].is_ascii_whitespace() {
454        end2 += 1;
455    }
456    // C: return (*endptr == '\0') ? endptr : NULL;  — OK iff no trailing chars
457    if end2 == s.len() {
458        Some((result, end2))
459    } else {
460        None
461    }
462}
463
464/// Converts bytes `s` to a Lua float value.
465/// Returns `Some((value, end_index))` on success, `None` on failure.
466///
467/// C: `static const char *l_str2d (const char *s, lua_Number *result)`
468fn str2d(s: &[u8]) -> Option<(f64, usize)> {
469    // C: const char *pmode = strpbrk(s, ".xXnN");
470    //    int mode = pmode ? ltolower(cast_uchar(*pmode)) : 0;
471    let pmode = s.iter().position(|&b| {
472        b == b'.' || b == b'x' || b == b'X' || b == b'n' || b == b'N'
473    });
474    let mode = pmode.map(|i| s[i].to_ascii_lowercase()).unwrap_or(0);
475
476    // C: if (mode == 'n') return NULL;  — reject 'inf' and 'nan'
477    if mode == b'n' {
478        return None;
479    }
480
481    // C: endptr = l_str2dloc(s, result, mode);
482    if let Some(result) = str2dloc(s, mode) {
483        return Some(result);
484    }
485
486    // C: if (endptr == NULL) { ... try replacing '.' with locale decimal point ... }
487    // PORT NOTE: Lua retries by replacing '.' with the locale decimal separator.
488    // Rust has no locale support; we skip this retry path and always use '.'.
489    // TODO(port): add locale retry if locale-aware float parsing is needed.
490
491    None
492}
493
494// ──────────────────────────────────────────────────────────────────────────
495// String-to-integer helper
496// ──────────────────────────────────────────────────────────────────────────
497
498/// Converts bytes `s` to a Lua integer value (decimal or `0x` hex).
499/// Returns `Some(value)` on success (the entire byte slice was consumed),
500/// `None` on failure or overflow.
501///
502/// C: `static const char *l_str2int (const char *s, lua_Integer *result)`
503fn str2int(s: &[u8]) -> Option<i64> {
504    let mut idx = 0;
505    // C: while (lisspace(cast_uchar(*s))) s++;
506    while idx < s.len() && s[idx].is_ascii_whitespace() {
507        idx += 1;
508    }
509    // C: neg = isneg(&s);
510    let neg = is_neg(s, &mut idx);
511
512    let mut a: u64 = 0;
513    let mut empty = true;
514
515    if idx + 1 < s.len() && s[idx] == b'0' && (s[idx + 1] == b'x' || s[idx + 1] == b'X') {
516        // C: s += 2; for (; lisxdigit(cast_uchar(*s)); s++) { a = a * 16 + ...; empty = 0; }
517        idx += 2;
518        while idx < s.len() && s[idx].is_ascii_hexdigit() {
519            a = a.wrapping_mul(16).wrapping_add(hex_value(s[idx]) as u64);
520            empty = false;
521            idx += 1;
522        }
523    } else {
524        // C: decimal loop with overflow check:
525        //    MAXBY10 = cast(lua_Unsigned, LUA_MAXINTEGER / 10)
526        //    MAXLASTD = cast_int(LUA_MAXINTEGER % 10)
527        //    if (a >= MAXBY10 && (a > MAXBY10 || d > MAXLASTD + neg)) return NULL;
528        const MAX_BY10: u64 = (i64::MAX / 10) as u64;
529        const MAX_LAST_D: u64 = (i64::MAX % 10) as u64;
530        while idx < s.len() && s[idx].is_ascii_digit() {
531            let d = (s[idx] - b'0') as u64;
532            if a >= MAX_BY10 && (a > MAX_BY10 || d > MAX_LAST_D + if neg { 1 } else { 0 }) {
533                return None; // overflow
534            }
535            a = a.wrapping_mul(10).wrapping_add(d);
536            empty = false;
537            idx += 1;
538        }
539    }
540
541    // C: while (lisspace(cast_uchar(*s))) s++;
542    while idx < s.len() && s[idx].is_ascii_whitespace() {
543        idx += 1;
544    }
545    // C: if (empty || *s != '\0') return NULL;
546    if empty || idx != s.len() {
547        return None;
548    }
549    // C: *result = l_castU2S((neg) ? 0u - a : a);
550    let result = if neg { (0u64).wrapping_sub(a) as i64 } else { a as i64 };
551    Some(result)
552}
553
554// ──────────────────────────────────────────────────────────────────────────
555// str2num — main public string-to-number conversion
556// ──────────────────────────────────────────────────────────────────────────
557
558/// Tries to convert the byte string `s` to a Lua number (integer first, then
559/// float).  Writes the result to `o` and returns `consumed_bytes + 1` on
560/// success (matching the C convention of including the null terminator in the
561/// count), or `0` on failure.
562///
563/// C: `size_t luaO_str2num (const char *s, TValue *o)`
564pub fn str2num(s: &[u8], o: &mut LuaValue) -> usize {
565    // C: if ((e = l_str2int(s, &i)) != NULL) { setivalue(o, i); }
566    if let Some(i) = str2int(s) {
567        *o = LuaValue::Int(i);
568        return s.len() + 1; // entire string consumed; +1 for C null-terminator convention
569    }
570    // C: else if ((e = l_str2d(s, &n)) != NULL) { setfltvalue(o, n); }
571    if let Some((n, end)) = str2d(s) {
572        *o = LuaValue::Float(n);
573        return end + 1;
574    }
575    // C: else return 0;
576    0
577}
578
579// ──────────────────────────────────────────────────────────────────────────
580// UTF-8 encoder
581// ──────────────────────────────────────────────────────────────────────────
582
583/// Encodes Unicode codepoint `x` as UTF-8 into `buff` (filled backwards from
584/// index `UTF8_BUF_SZ - 1`).  Returns the number of bytes written.
585/// The valid bytes occupy `buff[UTF8_BUF_SZ - n .. UTF8_BUF_SZ]`.
586///
587/// C: `int luaO_utf8esc (char *buff, unsigned long x)`
588pub fn utf8_esc(buff: &mut [u8; UTF8_BUF_SZ], x: u32) -> usize {
589    // C: lua_assert(x <= 0x7FFFFFFFu);
590    debug_assert!(x <= 0x7FFF_FFFF, "codepoint out of range");
591    let mut n: usize = 1;
592    if x < 0x80 {
593        // C: buff[UTF8BUFFSZ - 1] = cast_char(x);
594        buff[UTF8_BUF_SZ - 1] = x as u8;
595    } else {
596        // C: unsigned int mfb = 0x3f;  — max-fits-in-first-byte mask
597        let mut mfb: u32 = 0x3f;
598        let mut x = x;
599        loop {
600            // C: buff[UTF8BUFFSZ - (n++)] = cast_char(0x80 | (x & 0x3f));
601            buff[UTF8_BUF_SZ - n] = 0x80 | (x & 0x3f) as u8;
602            n += 1;
603            x >>= 6;
604            mfb >>= 1;
605            // C: while (x > mfb);
606            if x <= mfb {
607                break;
608            }
609        }
610        // C: buff[UTF8BUFFSZ - n] = cast_char((~mfb << 1) | x);
611        buff[UTF8_BUF_SZ - n] = ((!mfb << 1) | x) as u8;
612    }
613    n
614}
615
616// ──────────────────────────────────────────────────────────────────────────
617// Number → string conversion
618// ──────────────────────────────────────────────────────────────────────────
619
620/// Formats `f` as C's `printf("%.14g", f)` would, returning the bytes.
621///
622/// PORT NOTE: Rust has no built-in `%g` format. This replicates the C99
623/// `%g` algorithm with precision 14: pick scientific or fixed-point based
624/// on the value's exponent, strip trailing zeros, normalize the exponent
625/// to `e[+-]NN` with at least two digits (matching C's output).
626fn fmt_g14(f: f64) -> Vec<u8> {
627    if f.is_nan() {
628        return b"nan".to_vec();
629    }
630    if f.is_infinite() {
631        return if f > 0.0 { b"inf".to_vec() } else { b"-inf".to_vec() };
632    }
633    if f == 0.0 {
634        return if f.is_sign_negative() { b"-0".to_vec() } else { b"0".to_vec() };
635    }
636
637    let precision: i32 = 14;
638    let abs = f.abs();
639    let exp = abs.log10().floor() as i32;
640
641    let s = if exp < -4 || exp >= precision {
642        let mantissa_decimals = (precision - 1) as usize;
643        let raw = format!("{:.*e}", mantissa_decimals, f);
644        let e_idx = raw.find('e').expect("Rust scientific format always contains 'e'");
645        let mantissa = strip_fixed_trailing_zeros(&raw[..e_idx]);
646        let exp_num: i32 = raw[e_idx + 1..].parse().expect("Rust formats integer exponents");
647        let sign = if exp_num < 0 { '-' } else { '+' };
648        let abs_exp = exp_num.abs();
649        if abs_exp < 10 {
650            format!("{}e{}0{}", mantissa, sign, abs_exp)
651        } else {
652            format!("{}e{}{}", mantissa, sign, abs_exp)
653        }
654    } else {
655        let decimals = (precision - 1 - exp).max(0) as usize;
656        let raw = format!("{:.*}", decimals, f);
657        strip_fixed_trailing_zeros(&raw)
658    };
659
660    s.into_bytes()
661}
662
663fn strip_fixed_trailing_zeros(s: &str) -> String {
664    if !s.contains('.') {
665        return s.to_string();
666    }
667    let mut out = s.to_string();
668    while out.ends_with('0') {
669        out.pop();
670    }
671    if out.ends_with('.') {
672        out.pop();
673    }
674    out
675}
676
677/// Formats the numeric `LuaValue` `val` (must be Int or Float) into a byte
678/// buffer and returns it.
679///
680/// C: `static int tostringbuff (TValue *obj, char *buff)`
681fn number_to_str_buf(val: &LuaValue) -> Vec<u8> {
682    // C: lua_assert(ttisnumber(obj));
683    debug_assert!(
684        matches!(val, LuaValue::Int(_) | LuaValue::Float(_)),
685        "number_to_str_buf: value is not a number"
686    );
687
688    match val {
689        LuaValue::Int(i) => {
690            // C: len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
691            // lua_integer2str → l_sprintf with LUA_INTEGER_FMT ("%lld")
692            // PORT NOTE: using Rust's default i64 Display formatting, which
693            // matches C's `%lld` for all values in [i64::MIN, i64::MAX].
694            let s = format!("{}", i);
695            s.into_bytes()
696        }
697        LuaValue::Float(f) => {
698            let mut bytes = fmt_g14(*f);
699
700            let looks_like_int = bytes.iter().all(|&b| b == b'-' || b.is_ascii_digit());
701            if looks_like_int {
702                bytes.push(b'.');
703                bytes.push(b'0');
704            }
705            bytes
706        }
707        // Unreachable — guarded by debug_assert above.
708        _ => Vec::new(),
709    }
710}
711
712/// Converts a numeric `LuaValue` to an interned `LuaString`, returning a
713/// `GcRef<LuaString>` handle.  Callers are responsible for updating the
714/// `LuaValue` (or stack slot) with `LuaValue::Str(s)`.
715///
716/// C: `void luaO_tostring (lua_State *L, TValue *obj)` which modifies `obj`
717/// in place; in Rust we return the string because holding `&mut LuaValue`
718/// across a `state.intern_str` call would borrow `state` twice.
719pub fn num_to_string(state: &mut LuaState, val: &LuaValue) -> Result<GcRef<LuaString>, LuaError> {
720    // C: char buff[MAXNUMBER2STR];
721    //    int len = tostringbuff(obj, buff);
722    //    setsvalue(L, obj, luaS_newlstr(L, buff, len));
723    let bytes = number_to_str_buf(val);
724    // TODO(port): state.intern_str path needs to be confirmed in lua-vm
725    state.intern_str(&bytes)
726}
727
728// ──────────────────────────────────────────────────────────────────────────
729// push_vfstring infrastructure
730// ──────────────────────────────────────────────────────────────────────────
731
732/// Typed format argument for `push_vfstring`.
733///
734/// PORT NOTE: replaces the C `va_list` variadic interface.  C callers of
735/// `luaO_pushfstring(L, fmt, ...)` must be updated to pass structured
736/// `FmtArg` slices.  The format-string scanning logic is preserved in
737/// `push_vfstring`; only the argument-list type changes.
738pub enum FmtArg<'a> {
739    /// `%s` — a byte string (replaces `const char *` from va_list).
740    Str(&'a [u8]),
741    /// `%c` — a single byte character.
742    Char(u8),
743    /// `%d` — a 32-bit integer.
744    Int(i32),
745    /// `%I` — a Lua integer (i64).
746    LuaInt(i64),
747    /// `%f` — a Lua float (f64).
748    Float(f64),
749    /// `%U` — a Unicode codepoint (u32), encoded as UTF-8.
750    Utf8Codepoint(u32),
751    // TODO(port): %p (pointer) omitted — raw pointer in safe Rust is not allowed
752    // outside explicit unsafe-budget crates.  Callers that need pointer formatting must handle
753    // it separately and pass the pre-formatted bytes as FmtArg::Str.
754}
755
756/// Internal accumulator for `push_vfstring`.
757///
758/// C: `typedef struct BuffFS { lua_State *L; int pushed; int blen; char space[BUFVFS]; } BuffFS;`
759///
760/// PORT NOTE: `space` is a `Vec<u8>` rather than a fixed-size array; the
761/// BUF_VFS threshold is still respected for flushing behaviour.
762struct BufFs {
763    /// Whether at least one partial result has been pushed onto the stack.
764    pushed: bool,
765    /// Accumulated bytes not yet pushed to the stack.
766    space: Vec<u8>,
767}
768
769impl BufFs {
770    fn new() -> Self {
771        BufFs {
772            pushed: false,
773            space: Vec::with_capacity(BUF_VFS),
774        }
775    }
776}
777
778/// Pushes the byte string `str_bytes` to the Lua stack and concatenates with
779/// any prior partial result.
780///
781/// C: `static void pushstr (BuffFS *buff, const char *str, size_t lstr)`
782fn pushstr(buf: &mut BufFs, state: &mut LuaState, str_bytes: &[u8]) -> Result<(), LuaError> {
783    // C: setsvalue2s(L, L->top.p, luaS_newlstr(L, str, lstr));
784    //    L->top.p++;
785    //    if (!buff->pushed) buff->pushed = 1;
786    //    else luaV_concat(L, 2);
787    let s = state.intern_str(str_bytes)?;
788    state.push(LuaValue::Str(s));
789    if !buf.pushed {
790        buf.pushed = true;
791    } else {
792        // C: luaV_concat(L, 2);
793        // TODO(port): confirm path to string concatenation helper in lvm.rs
794        crate::vm::concat(state, 2)?;
795    }
796    Ok(())
797}
798
799/// Flushes the internal buffer to the Lua stack.
800///
801/// C: `static void clearbuff (BuffFS *buff)`
802fn clearbuff(buf: &mut BufFs, state: &mut LuaState) -> Result<(), LuaError> {
803    // C: pushstr(buff, buff->space, buff->blen); buff->blen = 0;
804    let bytes: Vec<u8> = buf.space.drain(..).collect();
805    pushstr(buf, state, &bytes)
806}
807
808/// Adds `str_bytes` to the internal buffer, flushing first if it won't fit.
809///
810/// C: `static void addstr2buff (BuffFS *buff, const char *str, size_t slen)`
811fn addstr2buff(buf: &mut BufFs, state: &mut LuaState, str_bytes: &[u8]) -> Result<(), LuaError> {
812    // C: if (slen <= BUFVFS) { ... memcpy ... addsize(buff, slen); }
813    //    else { clearbuff; pushstr directly; }
814    if str_bytes.len() <= BUF_VFS {
815        // C: if (sz > BUFVFS - buff->blen) clearbuff(buff);
816        if str_bytes.len() > BUF_VFS - buf.space.len() {
817            clearbuff(buf, state)?;
818        }
819        buf.space.extend_from_slice(str_bytes);
820    } else {
821        clearbuff(buf, state)?;
822        pushstr(buf, state, str_bytes)?;
823    }
824    Ok(())
825}
826
827/// Formats the numeric value `num` and appends it to the buffer.
828///
829/// C: `static void addnum2buff (BuffFS *buff, TValue *num)`
830fn addnum2buff(buf: &mut BufFs, state: &mut LuaState, num: &LuaValue) -> Result<(), LuaError> {
831    // C: char *numbuff = getbuff(buff, MAXNUMBER2STR);
832    //    int len = tostringbuff(num, numbuff);
833    //    addsize(buff, len);
834    let bytes = number_to_str_buf(num);
835    addstr2buff(buf, state, &bytes)
836}
837
838// ──────────────────────────────────────────────────────────────────────────
839// push_vfstring / push_fstring
840// ──────────────────────────────────────────────────────────────────────────
841
842/// Builds a formatted Lua string from a format byte string and structured
843/// arguments, pushes it onto the stack, and returns the top-of-stack value.
844///
845/// Supported format specifiers (same subset as C's `luaO_pushvfstring`):
846/// `%s`, `%c`, `%d`, `%I`, `%f`, `%U`, `%%`.
847/// `%p` is **not** supported; see [`FmtArg`] documentation.
848///
849/// C: `const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp)`
850///
851/// PORT NOTE: `va_list` replaced by `&[FmtArg]`.  Call sites that previously
852/// passed variadic arguments must be updated to build a `&[FmtArg]` slice.
853pub fn push_vfstring<'a>(
854    state: &mut LuaState,
855    fmt: &[u8],
856    args: &[FmtArg<'a>],
857) -> Result<GcRef<LuaString>, LuaError> {
858    let mut buf = BufFs::new();
859    let mut arg_idx = 0usize;
860    let mut pos = 0usize;
861
862    // C: while ((e = strchr(fmt, '%')) != NULL) { ... }
863    while let Some(rel) = fmt[pos..].iter().position(|&b| b == b'%') {
864        let e = pos + rel;
865        // C: addstr2buff(&buff, fmt, e - fmt);
866        addstr2buff(&mut buf, state, &fmt[pos..e])?;
867
868        // C: switch (*(e + 1)) { ... }
869        let spec = if e + 1 < fmt.len() { fmt[e + 1] } else { 0 };
870        match spec {
871            b's' => {
872                // C: const char *s = va_arg(argp, char *); if (!s) s = "(null)";
873                //    addstr2buff(&buff, s, strlen(s));
874                let s = match args.get(arg_idx) {
875                    Some(FmtArg::Str(b)) => *b,
876                    None => b"(null)",
877                    _ => b"(null)",
878                };
879                arg_idx += 1;
880                addstr2buff(&mut buf, state, s)?;
881            }
882            b'c' => {
883                // C: char c = cast_uchar(va_arg(argp, int));
884                //    addstr2buff(&buff, &c, sizeof(char));
885                let c = match args.get(arg_idx) {
886                    Some(FmtArg::Char(b)) => *b,
887                    _ => b'?',
888                };
889                arg_idx += 1;
890                addstr2buff(&mut buf, state, &[c])?;
891            }
892            b'd' => {
893                // C: TValue num; setivalue(&num, va_arg(argp, int)); addnum2buff(&buff, &num);
894                let n = match args.get(arg_idx) {
895                    Some(FmtArg::Int(i)) => *i as i64,
896                    _ => 0,
897                };
898                arg_idx += 1;
899                addnum2buff(&mut buf, state, &LuaValue::Int(n))?;
900            }
901            b'I' => {
902                // C: TValue num; setivalue(&num, cast(lua_Integer, va_arg(argp, l_uacInt)));
903                //    addnum2buff(&buff, &num);
904                let n = match args.get(arg_idx) {
905                    Some(FmtArg::LuaInt(i)) => *i,
906                    _ => 0,
907                };
908                arg_idx += 1;
909                addnum2buff(&mut buf, state, &LuaValue::Int(n))?;
910            }
911            b'f' => {
912                // C: TValue num; setfltvalue(&num, cast_num(va_arg(argp, l_uacNumber)));
913                //    addnum2buff(&buff, &num);
914                let f = match args.get(arg_idx) {
915                    Some(FmtArg::Float(f)) => *f,
916                    _ => 0.0,
917                };
918                arg_idx += 1;
919                addnum2buff(&mut buf, state, &LuaValue::Float(f))?;
920            }
921            b'p' => {
922                // C: void *p = va_arg(argp, void *); int len = lua_pointer2str(bf, sz, p);
923                // TODO(port): %p pointer formatting not implemented in safe Rust;
924                // callers that need it should pre-format the pointer and pass FmtArg::Str.
925                arg_idx += 1; // consume the argument slot
926                addstr2buff(&mut buf, state, b"<ptr>")?;
927            }
928            b'U' => {
929                // C: char bf[UTF8BUFFSZ]; int len = luaO_utf8esc(bf, va_arg(argp, long));
930                //    addstr2buff(&buff, bf + UTF8BUFFSZ - len, len);
931                let cp = match args.get(arg_idx) {
932                    Some(FmtArg::Utf8Codepoint(u)) => *u,
933                    _ => b'?' as u32,
934                };
935                arg_idx += 1;
936                let mut bf = [0u8; UTF8_BUF_SZ];
937                let n = utf8_esc(&mut bf, cp);
938                addstr2buff(&mut buf, state, &bf[UTF8_BUF_SZ - n..])?;
939            }
940            b'%' => {
941                // C: addstr2buff(&buff, "%", 1);
942                addstr2buff(&mut buf, state, b"%")?;
943            }
944            other => {
945                // C: luaG_runerror(L, "invalid option '%%%c' to 'lua_pushfstring'", *(e + 1));
946                return Err(LuaError::runtime(format_args!(
947                    "invalid option '%%{}' to 'lua_pushfstring'",
948                    other as char
949                )));
950            }
951        }
952        // C: fmt = e + 2;  — skip '%' and the specifier
953        pos = e + 2;
954    }
955
956    // C: addstr2buff(&buff, fmt, strlen(fmt));  — rest of format string
957    addstr2buff(&mut buf, state, &fmt[pos..])?;
958    // C: clearbuff(&buff);
959    clearbuff(&mut buf, state)?;
960    // C: lua_assert(buff.pushed == 1);
961    debug_assert!(buf.pushed, "push_vfstring: no string was pushed");
962
963    // C: return getstr(tsvalue(s2v(L->top.p - 1)));
964    // Return the interned string at the top of the stack.
965    // PORT NOTE: in C this returns a `const char *` into the TString; in Rust
966    // we return the GcRef<LuaString> directly.
967    // TODO(port): state.peek_string_at_top() path needs to be confirmed.
968    Ok(state.peek_string_at_top())
969}
970
971/// Variadic entry point; delegates to `push_vfstring`.
972///
973/// C: `const char *luaO_pushfstring (lua_State *L, const char *fmt, ...)`
974///
975/// PORT NOTE: callers that previously used `luaO_pushfstring` for error
976/// messages should collapse the call into `LuaError::runtime(format_args!(...))`;
977/// see PORTING.md §4.2 and error_sites.tsv.
978pub fn push_fstring<'a>(
979    state: &mut LuaState,
980    fmt: &[u8],
981    args: &[FmtArg<'a>],
982) -> Result<GcRef<LuaString>, LuaError> {
983    // C: va_start(argp, fmt); msg = luaO_pushvfstring(L, fmt, argp); va_end(argp);
984    push_vfstring(state, fmt, args)
985}
986
987// ──────────────────────────────────────────────────────────────────────────
988// chunk_id — human-readable chunk identifier
989// ──────────────────────────────────────────────────────────────────────────
990
991/// Fills `out` with a human-readable identifier derived from `source` and
992/// returns the number of bytes written (not including any null terminator).
993///
994/// Rules (matching C):
995/// - `=...`  → literal text (everything after `=`), truncated to `LUA_ID_SIZE - 1`.
996/// - `@...`  → file name (everything after `@`), prefixed with `...` if too long.
997/// - anything else → `[string "..."]`, with the first line truncated.
998///
999/// C: `void luaO_chunkid (char *out, const char *source, size_t srclen)`
1000pub fn chunk_id(out: &mut [u8], source: &[u8]) -> usize {
1001    let bufflen = LUA_ID_SIZE;
1002    let mut written = 0usize;
1003
1004    let mut write_bytes = |out: &mut [u8], written: &mut usize, bytes: &[u8]| {
1005        let avail = out.len().saturating_sub(*written);
1006        let n = bytes.len().min(avail);
1007        out[*written..*written + n].copy_from_slice(&bytes[..n]);
1008        *written += n;
1009    };
1010
1011    let first = source.first().copied();
1012    let srclen = source.len();
1013
1014    match first {
1015        Some(b'=') => {
1016            let body = &source[1..];
1017            if srclen <= bufflen {
1018                write_bytes(out, &mut written, body);
1019            } else {
1020                write_bytes(out, &mut written, &body[..bufflen - 1]);
1021                if written < out.len() {
1022                    out[written] = 0;
1023                }
1024            }
1025        }
1026        Some(b'@') => {
1027            let body = &source[1..];
1028            if srclen <= bufflen {
1029                write_bytes(out, &mut written, body);
1030            } else {
1031                write_bytes(out, &mut written, RETS);
1032                let tail_len = bufflen - RETS.len() - 1;
1033                let tail_start = body.len() - tail_len;
1034                write_bytes(out, &mut written, &body[tail_start..tail_start + tail_len]);
1035            }
1036        }
1037        _ => {
1038            let nl_pos = source.iter().position(|&b| b == b'\n');
1039            write_bytes(out, &mut written, PRE);
1040            let reserved = PRE.len() + RETS.len() + POS.len() + 1;
1041            let inner_limit = bufflen.saturating_sub(reserved);
1042
1043            if srclen < inner_limit && nl_pos.is_none() {
1044                write_bytes(out, &mut written, source);
1045            } else {
1046                let take = nl_pos.unwrap_or(srclen).min(inner_limit);
1047                write_bytes(out, &mut written, &source[..take]);
1048                write_bytes(out, &mut written, RETS);
1049            }
1050            write_bytes(out, &mut written, POS);
1051        }
1052    }
1053
1054    written
1055}
1056
1057// ──────────────────────────────────────────────────────────────────────────
1058// PORT STATUS
1059//   source:        src/lobject.c  (602 lines, ~20 functions)
1060//   target_crate:  lua-vm
1061//   confidence:    medium
1062//   todos:         15
1063//   port_notes:    12
1064//   unsafe_blocks: 0
1065//   notes:         All import paths are speculative (crate::state, lua_types::*);
1066//                  Phase B must reconcile.  va_list replaced by FmtArg enum —
1067//                  call sites of push_fstring/push_vfstring need updating.
1068//                  Float formatting (%.14g) is approximated with {:.14e}; needs
1069//                  proper %g in Phase B.  Locale decimal-point handling is
1070//                  stubbed (always '.').  str2dloc uses from_utf8 for ASCII
1071//                  number strings (flagged TODO).  int_floor_mod, int_floor_div,
1072//                  shiftl, float_floor_mod, concat are assumed to exist in
1073//                  crate::vm; Phase B must confirm or create them.
1074// ──────────────────────────────────────────────────────────────────────────