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