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