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