Skip to main content

lua_stdlib/
string_lib.rs

1//! Standard library for string operations and pattern-matching.
2//!
3//! Port of `lstrlib.c` (Lua 5.4.7, 1875 lines, 46 functions).
4//!
5//! Sections:
6//!   1. Basic string operations (byte, char, find, format, gmatch, gsub, len,
7//!      lower, match, rep, reverse, sub, upper)
8//!   2. Pattern-matching engine (MatchState + recursive matcher)
9//!   3. String format (`string.format`)
10//!   4. Pack / unpack (`string.pack`, `string.packsize`, `string.unpack`)
11//!   5. Module registration (`luaopen_string`)
12
13use lua_types::error::LuaError;
14use lua_types::value::LuaValue;
15use lua_types::arith::ArithOp;
16use lua_types::gc::GcRef;
17use lua_types::string::LuaString;
18use lua_types::{LuaType, LuaStatus};
19use lua_vm::state::LuaTableRefExt as _;
20use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index, CompareOp, LuaDebug};
21
22// ────────────────────────────────────────────────────────────────────────────
23// Constants
24// ────────────────────────────────────────────────────────────────────────────
25
26// C: #define LUA_MAXCAPTURES 32
27const LUA_MAX_CAPTURES: usize = 32;
28
29// C: #define MAXCCALLS 200
30const MAX_CC_CALLS: i32 = 200;
31
32// C: #define L_ESC '%'
33const L_ESC: u8 = b'%';
34
35// C: #define SPECIALS "^$*+?.([%-"
36const SPECIALS: &[u8] = b"^$*+?.([%-";
37
38// C: #define CAP_UNFINISHED (-1)
39const CAP_UNFINISHED: isize = -1;
40
41// C: #define CAP_POSITION (-2)
42const CAP_POSITION: isize = -2;
43
44// C: #define MAX_ITEM 120
45const MAX_ITEM: usize = 120;
46
47// C: #define MAX_ITEMF (110 + DBL_MAX_10_EXP) ≈ 110 + 308 = 418
48const MAX_ITEM_F: usize = 418;
49
50// C: #define MAX_FORMAT 32
51const MAX_FORMAT: usize = 32;
52
53// C: #define MAXINTSIZE 16
54const MAX_INT_SIZE: usize = 16;
55
56// C: #define MAXSIZE (sizeof(size_t) < sizeof(int) ? MAX_SIZET : (size_t)(INT_MAX))
57// On platforms where size_t is at least as wide as int (all our targets), this
58// collapses to INT_MAX so that packed sizes round-trip through a Lua integer
59// without ambiguity.
60const PACK_MAXSIZE: usize = i32::MAX as usize;
61
62// C: #define NB CHAR_BIT  (8)
63const NB: u32 = 8;
64
65// C: #define MC ((1 << NB) - 1)
66const MC: u8 = 0xFF;
67
68// C: #define SZINT ((int)sizeof(lua_Integer))
69const SZINT: usize = 8; // sizeof(i64) == 8
70
71// C: #define LUAL_PACKPADBYTE 0x00
72const PACK_PAD_BYTE: u8 = 0x00;
73
74// ────────────────────────────────────────────────────────────────────────────
75// Pattern-matching types
76// ────────────────────────────────────────────────────────────────────────────
77
78/// One capture record inside MatchState.
79///
80/// C: `struct { const char *init; ptrdiff_t len; }`
81/// In Rust, `init` is an index into `MatchState::src`; `len` is either a
82/// non-negative actual length, `CAP_UNFINISHED`, or `CAP_POSITION`.
83#[derive(Copy, Clone)]
84struct Capture {
85    /// Index into the source slice where this capture started.
86    init: usize,
87    /// CAP_UNFINISHED, CAP_POSITION, or non-negative byte count.
88    len: isize,
89}
90
91impl Default for Capture {
92    fn default() -> Self {
93        Capture { init: 0, len: CAP_UNFINISHED }
94    }
95}
96
97/// State threaded through the recursive pattern-matcher.
98///
99/// C: `typedef struct MatchState { ... } MatchState;`
100/// Raw C pointers replaced by indices into `src` / `pat` slices.
101struct MatchState<'a> {
102    /// Source string being searched.
103    src: &'a [u8],
104    /// Pattern string.
105    pat: &'a [u8],
106    /// Recursion depth counter; decremented on entry, incremented on return.
107    matchdepth: i32,
108    /// Number of capture records currently in use.
109    level: u8,
110    /// Capture records indexed `0..level`.
111    captures: [Capture; LUA_MAX_CAPTURES],
112}
113
114impl<'a> MatchState<'a> {
115    fn new(src: &'a [u8], pat: &'a [u8]) -> Self {
116        MatchState {
117            src,
118            pat,
119            matchdepth: MAX_CC_CALLS,
120            level: 0,
121            captures: [Capture::default(); LUA_MAX_CAPTURES],
122        }
123    }
124
125    fn reset_level(&mut self) {
126        self.level = 0;
127        debug_assert!(self.matchdepth == MAX_CC_CALLS);
128    }
129}
130
131/// Iterator state for `string.gmatch`.
132///
133/// C: `typedef struct GMatchState { ... } GMatchState;`
134/// Stored as userdata on the Lua stack in the C implementation; in Phase A we
135/// represent it as a plain Rust struct.
136///
137/// TODO(port): In the real port, this needs to live in a Lua userdata object
138/// so that Lua GC can see it. For now it's a plain struct passed by
139/// `state.to_userdata()`.
140struct GMatchState {
141    /// Current position in `src` (index into the source slice).
142    src_pos: usize,
143    /// The pattern string (owned copy so it survives the closure).
144    pat: Vec<u8>,
145    /// End of the last match (to avoid zero-length infinite loops).
146    last_match: Option<usize>,
147    /// Source string (owned copy).
148    src: Vec<u8>,
149}
150
151// ────────────────────────────────────────────────────────────────────────────
152// Pack/unpack types
153// ────────────────────────────────────────────────────────────────────────────
154
155/// Pack/unpack format option.
156///
157/// C: `typedef enum KOption { Kint, Kuint, ... } KOption;`
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159enum KOption {
160    Int,        // signed integers
161    Uint,       // unsigned integers
162    Float,      // single-precision float (C float)
163    Number,     // Lua native float (lua_Number = f64)
164    Double,     // double-precision float (C double)
165    Char,       // fixed-length string
166    Kstring,    // string with length prefix
167    Zstr,       // zero-terminated string
168    Padding,    // padding byte (x)
169    Paddalign,  // padding to alignment (X)
170    Nop,        // no-op (space, <, >, =, !)
171}
172
173/// Header state for pack/unpack format parsing.
174///
175/// C: `typedef struct Header { lua_State *L; int islittle; int maxalign; } Header;`
176struct Header {
177    is_little: bool,
178    max_align: usize,
179}
180
181impl Header {
182    fn new() -> Self {
183        Header {
184            is_little: cfg!(target_endian = "little"),
185            max_align: 1,
186        }
187    }
188}
189
190// ────────────────────────────────────────────────────────────────────────────
191// §1  Basic string helpers
192// ────────────────────────────────────────────────────────────────────────────
193
194/// Translate a relative initial string position: negative means back from end;
195/// result is clipped to `[1, ∞)`.
196///
197/// C: `static size_t posrelatI(lua_Integer pos, size_t len)`
198fn pos_relat_i(pos: i64, len: usize) -> usize {
199    if pos > 0 {
200        pos as usize
201    } else if pos == 0 {
202        1
203    } else if pos < -(len as i64) {
204        1
205    } else {
206        len.wrapping_add(pos as usize).wrapping_add(1)
207    }
208}
209
210/// Get an optional ending string position from argument `arg`, default `def`.
211/// Negative means back from end; clipped to `[0, len]`.
212///
213/// C: `static size_t getendpos(lua_State *L, int arg, lua_Integer def, size_t len)`
214fn get_end_pos(pos: i64, len: usize) -> usize {
215    if pos > len as i64 {
216        len
217    } else if pos >= 0 {
218        pos as usize
219    } else if pos < -(len as i64) {
220        0
221    } else {
222        len.wrapping_add(pos as usize).wrapping_add(1)
223    }
224}
225
226// ────────────────────────────────────────────────────────────────────────────
227// §2  Exported string functions (registered in strlib[])
228// ────────────────────────────────────────────────────────────────────────────
229
230/// `string.len(s)` — return byte-length of `s`.
231///
232/// C: `static int str_len(lua_State *L)`
233///
234/// Reads only the byte-length, never the bytes themselves, so go through
235/// `to_lua_string_len` (which never copies) rather than `check_arg_string`
236/// (which `to_vec`s the entire payload only for `.len()` to throw it away).
237pub fn str_len(state: &mut LuaState) -> Result<usize, LuaError> {
238    let l = match state.to_lua_string_len(1) {
239        Some(n) => n,
240        None => {
241            state.check_arg_string(1)?;
242            unreachable!("check_arg_string raises when arg #1 is not a string");
243        }
244    };
245    state.push(LuaValue::Int(l as i64));
246    Ok(1)
247}
248
249/// `string.sub(s, i [, j])` — return substring.
250///
251/// C: `static int str_sub(lua_State *L)`
252///
253/// Borrow through `to_lua_string` so the full source string is not copied just
254/// to slice a (typically small) substring out of it. The `GcRef` keeps the
255/// bytes rooted across the `check_arg_integer` / `opt_arg_integer` calls (none
256/// of which can collect the string at arg #1).
257pub fn str_sub(state: &mut LuaState) -> Result<usize, LuaError> {
258    let s_ref = match state.to_lua_string(1) {
259        Some(r) => r,
260        None => {
261            state.check_arg_string(1)?;
262            unreachable!("check_arg_string raises when arg #1 is not a string");
263        }
264    };
265    let s: &[u8] = s_ref.as_bytes();
266    let l = s.len();
267    let start = pos_relat_i(state.check_arg_integer(2)?, l);
268    let end_pos_raw = state.opt_arg_integer(3, -1)?;
269    let end = get_end_pos(end_pos_raw, l);
270    if start <= end {
271        let slice = &s[(start - 1)..end];
272        state.push_string(slice)?;
273    } else {
274        state.push_string(b"")?;
275    }
276    Ok(1)
277}
278
279/// `string.reverse(s)` — return string with bytes reversed.
280///
281/// C: `static int str_reverse(lua_State *L)`
282///
283/// Borrow the source bytes; the previous `check_arg_string` made a full owned
284/// copy that was discarded after the single iteration.
285pub fn str_reverse(state: &mut LuaState) -> Result<usize, LuaError> {
286    let s_ref = match state.to_lua_string(1) {
287        Some(r) => r,
288        None => {
289            state.check_arg_string(1)?;
290            unreachable!("check_arg_string raises when arg #1 is not a string");
291        }
292    };
293    let s: &[u8] = s_ref.as_bytes();
294    let buf: Vec<u8> = s.iter().copied().rev().collect();
295    state.push_bytes(&buf)?;
296    Ok(1)
297}
298
299/// `string.lower(s)` — return lowercase copy.
300///
301/// C: `static int str_lower(lua_State *L)`
302///
303/// Borrow the source bytes; one allocation (the output `Vec`) is unavoidable,
304/// but the intermediate copy from `check_arg_string` was not.
305pub fn str_lower(state: &mut LuaState) -> Result<usize, LuaError> {
306    let s_ref = match state.to_lua_string(1) {
307        Some(r) => r,
308        None => {
309            state.check_arg_string(1)?;
310            unreachable!("check_arg_string raises when arg #1 is not a string");
311        }
312    };
313    let s: &[u8] = s_ref.as_bytes();
314    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_lowercase()).collect();
315    state.push_bytes(&buf)?;
316    Ok(1)
317}
318
319/// `string.upper(s)` — return uppercase copy.
320///
321/// C: `static int str_upper(lua_State *L)`
322///
323/// Borrow the source bytes; called as the `string.gsub` replacement function
324/// in `string_ops_long` ~700k times against `%w+` matches, so the intermediate
325/// copy from `check_arg_string` added up.
326pub fn str_upper(state: &mut LuaState) -> Result<usize, LuaError> {
327    let s_ref = match state.to_lua_string(1) {
328        Some(r) => r,
329        None => {
330            state.check_arg_string(1)?;
331            unreachable!("check_arg_string raises when arg #1 is not a string");
332        }
333    };
334    let s: &[u8] = s_ref.as_bytes();
335    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_uppercase()).collect();
336    state.push_bytes(&buf)?;
337    Ok(1)
338}
339
340/// `string.rep(s, n [, sep])` — return `n` copies of `s` separated by `sep`.
341///
342/// C: `static int str_rep(lua_State *L)`
343///
344/// Borrow `s` through `to_lua_string`. The previous version did the
345/// `check_arg_string` copy and then a second redundant `s.to_vec()` inside the
346/// build loop — that double-copy is gone too.
347pub fn str_rep(state: &mut LuaState) -> Result<usize, LuaError> {
348    let s_ref = match state.to_lua_string(1) {
349        Some(r) => r,
350        None => {
351            state.check_arg_string(1)?;
352            unreachable!("check_arg_string raises when arg #1 is not a string");
353        }
354    };
355    let s: &[u8] = s_ref.as_bytes();
356    let l = s.len();
357    let n = state.check_arg_integer(2)?;
358    let sep_owned = state.opt_arg_string(3, b"")?;
359    let sep: &[u8] = &sep_owned;
360    let lsep = sep.len();
361
362    if n <= 0 {
363        state.push_string(b"")?;
364    } else {
365        const MAXSIZE: usize = i32::MAX as usize;
366        let per = l.checked_add(lsep)
367            .ok_or_else(|| LuaError::runtime(format_args!("resulting string too large")))?;
368        if per > MAXSIZE / (n as usize) {
369            return Err(LuaError::runtime(format_args!("resulting string too large")));
370        }
371        let total = per * (n as usize) - lsep;
372
373        let mut buf: Vec<u8> = Vec::with_capacity(total);
374        for i in 0..(n as usize) {
375            buf.extend_from_slice(s);
376            if i < (n as usize - 1) && lsep > 0 {
377                buf.extend_from_slice(sep);
378            }
379        }
380        state.push_bytes(&buf)?;
381    }
382    Ok(1)
383}
384
385/// `string.byte(s [, i [, j]])` — return numeric codes of characters.
386///
387/// C: `static int str_byte(lua_State *L)`
388///
389/// Borrow the source bytes through `to_lua_string` (returns a `GcRef<LuaString>`)
390/// instead of `check_arg_string` (which copies the entire string into a fresh
391/// `Vec<u8>`). On the `string_ops_long` workload `string.byte` is called 700k
392/// times against the same ~14 KB string, so the previous copy was on the order
393/// of 10 GB of memcpy. The `GcRef` keeps the bytes rooted while the borrow lives.
394pub fn str_byte(state: &mut LuaState) -> Result<usize, LuaError> {
395    let s_ref = match state.to_lua_string(1) {
396        Some(r) => r,
397        None => {
398            state.check_arg_string(1)?;
399            unreachable!("check_arg_string raises when arg #1 is not a string");
400        }
401    };
402    let s: &[u8] = s_ref.as_bytes();
403    let l = s.len();
404    let pi = state.opt_arg_integer(2, 1)?;
405    let posi = pos_relat_i(pi, l);
406    let pose_raw = state.opt_arg_integer(3, pi)?;
407    let pose = get_end_pos(pose_raw, l);
408
409    if posi > pose {
410        return Ok(0);
411    }
412    let count = pose.saturating_sub(posi - 1) + 1;
413    if count > i32::MAX as usize {
414        return Err(LuaError::runtime(format_args!("string slice too long")));
415    }
416    let n = (pose - posi + 1) as usize;
417    state.ensure_stack(n as i32, "string slice too long")?;
418
419    for i in 0..n {
420        state.push(LuaValue::Int(s[posi - 1 + i] as i64));
421    }
422    Ok(n)
423}
424
425/// `string.char(...)` — return string built from character codes.
426///
427/// C: `static int str_char(lua_State *L)`
428pub fn str_char(state: &mut LuaState) -> Result<usize, LuaError> {
429    let n = state.get_top();
430    let mut buf = Vec::with_capacity(n as usize);
431    for i in 1..=n {
432        let c = state.check_arg_integer(i)? as u64;
433        if c > u8::MAX as u64 {
434            return Err(LuaError::arg_error(i, "value out of range"));
435        }
436        buf.push(c as u8);
437    }
438    state.push_bytes(&buf)?;
439    Ok(1)
440}
441
442/// `string.dump(function [, strip])` — serialize a function as binary chunk.
443///
444/// C: `static int str_dump(lua_State *L)`
445/// Uses `lua_dump` internally; the writer callback builds a buffer.
446pub fn str_dump(state: &mut LuaState) -> Result<usize, LuaError> {
447    state.check_arg_type(1, LuaType::Function)?;
448    let strip = state.arg_to_bool(2);
449    // C: lua_settop(L, 1);
450    // PORT NOTE: `state.set_top` (inherent) takes an absolute StackIdx and
451    // would wipe the call frame. `lua_settop` is frame-relative.
452    lua_vm::api::set_top(state, 1)?;
453    // TODO(port): state.dump_function(strip) needs to produce &[u8].
454    // In the C code, lua_dump writes to a writer callback that fills a luaL_Buffer.
455    // In Rust, state.dump() should return Vec<u8> or write to a &mut Vec<u8>.
456    let bytes = state.dump_function(strip)
457        .map_err(|_| LuaError::runtime(format_args!("unable to dump given function")))?;
458    state.push_bytes(&bytes)?;
459    Ok(1)
460}
461
462// ────────────────────────────────────────────────────────────────────────────
463// §3  String metamethods (arithmetic coercion)
464// ────────────────────────────────────────────────────────────────────────────
465
466/// Try to coerce the argument at `arg` to a number, pushing it on the stack.
467/// Returns true on success.
468///
469/// C: `static int tonum(lua_State *L, int arg)`
470fn tonum(state: &mut LuaState, arg: i32) -> Result<bool, LuaError> {
471    if state.type_at(arg) == LuaType::Number {
472        state.push_value_at(arg)?;
473        Ok(true)
474    } else {
475        // check whether it is a numerical string
476        // C: const char *s = lua_tolstring(L, arg, &len);
477        //    return (s != NULL && lua_stringtonumber(L, s) == len + 1);
478        if let Some(s) = state.to_lua_string_bytes(arg) {
479            let len = s.len();
480            // PORT NOTE: string_to_number pushes the number if successful
481            let pushed = state.string_to_number_push(&s)?;
482            Ok(pushed == len + 1)
483        } else {
484            Ok(false)
485        }
486    }
487}
488
489/// Try to invoke the metamethod `mtname` on the two operands.
490///
491/// C: `static void trymt(lua_State *L, const char *mtname)`
492fn trymt(state: &mut LuaState, mtname: &[u8]) -> Result<(), LuaError> {
493    // C: lua_settop(L, 2); /* back to original arguments */
494    // PORT NOTE: `state.set_top` (inherent) takes an absolute StackIdx and
495    // would wipe the call frame's arguments. `lua_settop` is frame-relative
496    // — keep the first two args of the current C function.
497    lua_vm::api::set_top(state, 2)?;
498    // C: if (lua_type(L, 2) == LUA_TSTRING || !luaL_getmetafield(L, 2, mtname))
499    //        luaL_error(...)
500    let t2_is_string = state.type_at(2) == LuaType::String;
501    let has_mm = state.get_meta_field(2, mtname)?;
502    if t2_is_string || !has_mm {
503        // C: luaL_error(L, "attempt to %s a '%s' with a '%s'", mtname + 2, ...)
504        let op = &mtname[2..]; // skip "__"
505        return Err(LuaError::runtime(format_args!(
506            "attempt to {} a '{}' with a '{}'",
507            op.escape_ascii(),
508            state.type_name_at(-2).escape_ascii(),
509            state.type_name_at(-1).escape_ascii(),
510        )));
511    }
512    // C: lua_insert(L, -3); lua_call(L, 2, 1);
513    state.insert(-3)?;
514    state.call(2, 1)?;
515    Ok(())
516}
517
518/// Generic arithmetic helper: coerce both args and call `op`, else try metamethod.
519///
520/// C: `static int arith(lua_State *L, int op, const char *mtname)`
521fn arith(state: &mut LuaState, op: ArithOp, mtname: &[u8]) -> Result<usize, LuaError> {
522    if tonum(state, 1)? && tonum(state, 2)? {
523        state.arith(op)?;
524    } else {
525        trymt(state, mtname)?;
526    }
527    Ok(1)
528}
529
530/// C: `static int arith_add(lua_State *L)`
531pub fn arith_add(state: &mut LuaState) -> Result<usize, LuaError> {
532    arith(state, ArithOp::Add, b"__add")
533}
534/// C: `static int arith_sub(lua_State *L)`
535pub fn arith_sub(state: &mut LuaState) -> Result<usize, LuaError> {
536    arith(state, ArithOp::Sub, b"__sub")
537}
538/// C: `static int arith_mul(lua_State *L)`
539pub fn arith_mul(state: &mut LuaState) -> Result<usize, LuaError> {
540    arith(state, ArithOp::Mul, b"__mul")
541}
542/// C: `static int arith_mod(lua_State *L)`
543pub fn arith_mod(state: &mut LuaState) -> Result<usize, LuaError> {
544    arith(state, ArithOp::Mod, b"__mod")
545}
546/// C: `static int arith_pow(lua_State *L)`
547pub fn arith_pow(state: &mut LuaState) -> Result<usize, LuaError> {
548    arith(state, ArithOp::Pow, b"__pow")
549}
550/// C: `static int arith_div(lua_State *L)`
551pub fn arith_div(state: &mut LuaState) -> Result<usize, LuaError> {
552    arith(state, ArithOp::Div, b"__div")
553}
554/// C: `static int arith_idiv(lua_State *L)`
555pub fn arith_idiv(state: &mut LuaState) -> Result<usize, LuaError> {
556    arith(state, ArithOp::Idiv, b"__idiv")
557}
558/// C: `static int arith_unm(lua_State *L)`
559pub fn arith_unm(state: &mut LuaState) -> Result<usize, LuaError> {
560    arith(state, ArithOp::Unm, b"__unm")
561}
562
563// ────────────────────────────────────────────────────────────────────────────
564// §4  Pattern-matching engine
565// ────────────────────────────────────────────────────────────────────────────
566
567/// Return `true` if `c` belongs to the character class `cl` (a `%x` letter).
568///
569/// C: `static int match_class(int c, int cl)`
570fn match_class(c: u8, cl: u8) -> bool {
571    let res = match cl.to_ascii_lowercase() {
572        b'a' => c.is_ascii_alphabetic(),
573        b'c' => c.is_ascii_control(),
574        b'd' => c.is_ascii_digit(),
575        b'g' => c.is_ascii_graphic(),
576        b'l' => c.is_ascii_lowercase(),
577        b'p' => c.is_ascii_punctuation(),
578        b's' => c.is_ascii_whitespace(),
579        b'u' => c.is_ascii_uppercase(),
580        b'w' => c.is_ascii_alphanumeric(),
581        b'x' => c.is_ascii_hexdigit(),
582        b'z' => c == 0,
583        _    => return cl == c,
584    };
585    if cl.is_ascii_lowercase() { res } else { !res }
586}
587
588/// Match character `c` against a bracket class `[p .. ec-1]`.
589///
590/// C: `static int matchbracketclass(int c, const char *p, const char *ec)`
591/// `p` and `ec` are indices into `pat`.
592fn matchbracketclass(pat: &[u8], c: u8, mut p: usize, ec: usize) -> bool {
593    let sig = if p + 1 < pat.len() && pat[p + 1] == b'^' {
594        p += 1; // skip '^'
595        false
596    } else {
597        true
598    };
599    p += 1; // advance past '[' or '^'
600    while p < ec {
601        if pat[p] == L_ESC {
602            p += 1;
603            if p < ec && match_class(c, pat[p]) {
604                return sig;
605            }
606        } else if p + 1 < ec && pat[p + 1] == b'-' && p + 2 < ec {
607            let lo = pat[p];
608            p += 2;
609            let hi = pat[p];
610            if lo <= c && c <= hi {
611                return sig;
612            }
613        } else if pat[p] == c {
614            return sig;
615        }
616        p += 1;
617    }
618    !sig
619}
620
621/// Return `true` if the single character at `src[s]` matches the pattern
622/// element starting at `pat[p]` with class end at `ep`.
623///
624/// C: `static int singlematch(MatchState *ms, const char *s, const char *p, const char *ep)`
625fn singlematch(ms: &MatchState, s: usize, p: usize, ep: usize) -> bool {
626    if s >= ms.src.len() {
627        return false;
628    }
629    let c = ms.src[s];
630    match ms.pat[p] {
631        b'.' => true,
632        L_ESC => match_class(c, ms.pat[p + 1]),
633        b'[' => matchbracketclass(ms.pat, c, p, ep - 1),
634        pc   => pc == c,
635    }
636}
637
638/// Find the end of the pattern element starting at `pat[p]`.
639/// Returns the index one past the element, or an error for malformed patterns.
640///
641/// C: `static const char *classend(MatchState *ms, const char *p)`
642fn classend(ms: &MatchState, p: usize) -> Result<usize, LuaError> {
643    let pat = ms.pat;
644    match pat.get(p).copied() {
645        Some(L_ESC) => {
646            // C: if (p == ms->p_end) luaL_error(...);
647            if p + 1 >= pat.len() {
648                return Err(LuaError::runtime(format_args!(
649                    "malformed pattern (ends with '%')"
650                )));
651            }
652            Ok(p + 2)
653        }
654        Some(b'[') => {
655            let mut q = p + 1;
656            if q < pat.len() && pat[q] == b'^' {
657                q += 1;
658            }
659            loop {
660                if q >= pat.len() {
661                    return Err(LuaError::runtime(format_args!(
662                        "malformed pattern (missing ']')"
663                    )));
664                }
665                let ch = pat[q];
666                q += 1;
667                if ch == L_ESC && q < pat.len() {
668                    q += 1;
669                }
670                if q < pat.len() && pat[q] == b']' {
671                    return Ok(q + 1);
672                }
673            }
674        }
675        Some(_) => Ok(p + 1),
676        None => Ok(p),
677    }
678}
679
680/// Check that capture `l` (1-based char digit from pattern) is valid.
681/// Returns the 0-based capture index.
682///
683/// C: `static int check_capture(MatchState *ms, int l)`
684fn check_capture(ms: &MatchState, l: u8) -> Result<usize, LuaError> {
685    let signed = (l as i32) - (b'1' as i32);
686    if signed < 0
687        || signed >= ms.level as i32
688        || ms.captures[signed as usize].len == CAP_UNFINISHED
689    {
690        return Err(LuaError::runtime(format_args!(
691            "invalid capture index %{}",
692            signed + 1
693        )));
694    }
695    Ok(signed as usize)
696}
697
698/// Find the most recent unfinished capture to close.
699///
700/// C: `static int capture_to_close(MatchState *ms)`
701fn capture_to_close(ms: &MatchState) -> Result<usize, LuaError> {
702    let mut level = ms.level as usize;
703    while level > 0 {
704        level -= 1;
705        if ms.captures[level].len == CAP_UNFINISHED {
706            return Ok(level);
707        }
708    }
709    Err(LuaError::runtime(format_args!("invalid pattern capture")))
710}
711
712/// Match a balanced string `%bxy` starting at `src[s]`.
713///
714/// C: `static const char *matchbalance(MatchState *ms, const char *s, const char *p)`
715/// Returns the new `s` position after the match, or `None`.
716fn matchbalance(ms: &MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
717    // C: if (p >= ms->p_end - 1) luaL_error(...)
718    if p + 1 >= ms.pat.len() {
719        return Err(LuaError::runtime(format_args!(
720            "malformed pattern (missing arguments to '%b')"
721        )));
722    }
723    let b = ms.pat[p];
724    let e = ms.pat[p + 1];
725    if s >= ms.src.len() || ms.src[s] != b {
726        return Ok(None);
727    }
728    let mut cont = 1i32;
729    let mut s = s + 1;
730    while s < ms.src.len() {
731        if ms.src[s] == e {
732            cont -= 1;
733            if cont == 0 {
734                return Ok(Some(s + 1));
735            }
736        } else if ms.src[s] == b {
737            cont += 1;
738        }
739        s += 1;
740    }
741    Ok(None)
742}
743
744/// Greedy match: match as many as possible, then try the rest of the pattern.
745///
746/// C: `static const char *max_expand(MatchState *ms, const char *s, const char *p, const char *ep)`
747fn max_expand(
748    ms: &mut MatchState,
749    s: usize,
750    p: usize,
751    ep: usize,
752) -> Result<Option<usize>, LuaError> {
753    let mut count: isize = 0;
754    while singlematch(ms, s + count as usize, p, ep) {
755        count += 1;
756    }
757    while count >= 0 {
758        let res = match_pat(ms, s + count as usize, ep + 1)?;
759        if res.is_some() {
760            return Ok(res);
761        }
762        count -= 1;
763    }
764    Ok(None)
765}
766
767/// Lazy match: try the rest of the pattern first, then expand by one.
768///
769/// C: `static const char *min_expand(MatchState *ms, const char *s, const char *p, const char *ep)`
770fn min_expand(
771    ms: &mut MatchState,
772    mut s: usize,
773    p: usize,
774    ep: usize,
775) -> Result<Option<usize>, LuaError> {
776    loop {
777        let res = match_pat(ms, s, ep + 1)?;
778        if res.is_some() {
779            return Ok(res);
780        } else if singlematch(ms, s, p, ep) {
781            s += 1;
782        } else {
783            return Ok(None);
784        }
785    }
786}
787
788/// Open a new capture at `src[s]`.
789///
790/// C: `static const char *start_capture(MatchState *ms, const char *s, const char *p, int what)`
791fn start_capture(
792    ms: &mut MatchState,
793    s: usize,
794    p: usize,
795    what: isize,
796) -> Result<Option<usize>, LuaError> {
797    let level = ms.level as usize;
798    if level >= LUA_MAX_CAPTURES {
799        return Err(LuaError::runtime(format_args!("too many captures")));
800    }
801    ms.captures[level].init = s;
802    ms.captures[level].len = what;
803    ms.level += 1;
804    let res = match_pat(ms, s, p)?;
805    if res.is_none() {
806        ms.level -= 1; // undo capture
807    }
808    Ok(res)
809}
810
811/// Close the most recent open capture at `src[s]`.
812///
813/// C: `static const char *end_capture(MatchState *ms, const char *s, const char *p)`
814fn end_capture(ms: &mut MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
815    let l = capture_to_close(ms)?;
816    ms.captures[l].len = (s - ms.captures[l].init) as isize;
817    let res = match_pat(ms, s, p)?;
818    if res.is_none() {
819        ms.captures[l].len = CAP_UNFINISHED; // undo
820    }
821    Ok(res)
822}
823
824/// Match a back-reference `%n` against `src[s]`.
825///
826/// C: `static const char *match_capture(MatchState *ms, const char *s, int l)`
827fn match_capture(ms: &MatchState, s: usize, l: u8) -> Result<Option<usize>, LuaError> {
828    let idx = check_capture(ms, l)?;
829    let cap_len = ms.captures[idx].len as usize;
830    let cap_init = ms.captures[idx].init;
831    if ms.src.len() - s >= cap_len
832        && &ms.src[s..s + cap_len] == &ms.src[cap_init..cap_init + cap_len]
833    {
834        Ok(Some(s + cap_len))
835    } else {
836        Ok(None)
837    }
838}
839
840/// Core recursive pattern matcher.
841/// Returns `Ok(Some(new_s))` on match, `Ok(None)` on failure, `Err` on error.
842///
843/// C: `static const char *match(MatchState *ms, const char *s, const char *p)`
844/// The C code uses `goto init` for tail calls; here we use a loop.
845fn match_pat(ms: &mut MatchState, mut s: usize, mut p: usize) -> Result<Option<usize>, LuaError> {
846    // C: if (l_unlikely(ms->matchdepth-- == 0)) luaL_error(ms->L, "pattern too complex");
847    ms.matchdepth -= 1;
848    if ms.matchdepth < 0 {
849        ms.matchdepth = 0;
850        return Err(LuaError::runtime(format_args!("pattern too complex")));
851    }
852
853    // Use a loop to simulate `goto init` (tail-call optimization).
854    let result = 'outer: loop {
855        if p >= ms.pat.len() {
856            // end of pattern — full match up to current s
857            break 'outer Ok(Some(s));
858        }
859
860        match ms.pat[p] {
861            b'(' => {
862                // C: start capture
863                let s2 = if p + 1 < ms.pat.len() && ms.pat[p + 1] == b')' {
864                    // position capture
865                    start_capture(ms, s, p + 2, CAP_POSITION)?
866                } else {
867                    start_capture(ms, s, p + 1, CAP_UNFINISHED)?
868                };
869                break 'outer Ok(s2);
870            }
871            b')' => {
872                let s2 = end_capture(ms, s, p + 1)?;
873                break 'outer Ok(s2);
874            }
875            b'$' => {
876                // C: if ((p + 1) != ms->p_end) goto dflt;
877                if p + 1 != ms.pat.len() {
878                    // fall through to default
879                    let ep = classend(ms, p)?;
880                    let s2 = handle_class_with_suffix(ms, s, p, ep)?;
881                    break 'outer Ok(s2);
882                }
883                // C: s = (s == ms->src_end) ? s : NULL;
884                break 'outer Ok(if s == ms.src.len() { Some(s) } else { None });
885            }
886            L_ESC => {
887                match ms.pat.get(p + 1).copied().unwrap_or(0) {
888                    b'b' => {
889                        // C: matchbalance
890                        let s2 = matchbalance(ms, s, p + 2)?;
891                        if let Some(ns) = s2 {
892                            s = ns;
893                            p += 4;
894                            continue 'outer; // tail call: match(ms, s, p+4)
895                        }
896                        break 'outer Ok(None);
897                    }
898                    b'f' => {
899                        // C: frontier pattern
900                        p += 2;
901                        if ms.pat.get(p).copied() != Some(b'[') {
902                            return Err(LuaError::runtime(format_args!(
903                                "missing '[' after '%f' in pattern"
904                            )));
905                        }
906                        let ep = classend(ms, p)?;
907                        let previous = if s == 0 { 0u8 } else { ms.src[s - 1] };
908                        let current = ms.src.get(s).copied().unwrap_or(0);
909                        if !matchbracketclass(ms.pat, previous, p, ep - 1)
910                            && matchbracketclass(ms.pat, current, p, ep - 1)
911                        {
912                            p = ep;
913                            continue 'outer; // tail call: match(ms, s, ep)
914                        }
915                        break 'outer Ok(None);
916                    }
917                    c @ b'0'..=b'9' => {
918                        // C: back-reference %0-%9
919                        let s2 = match_capture(ms, s, c)?;
920                        if let Some(ns) = s2 {
921                            s = ns;
922                            p += 2;
923                            continue 'outer; // tail call: match(ms, s, p+2)
924                        }
925                        break 'outer Ok(None);
926                    }
927                    _ => {
928                        // fall through to default class handling
929                        let ep = classend(ms, p)?;
930                        let s2 = handle_class_with_suffix(ms, s, p, ep)?;
931                        break 'outer Ok(s2);
932                    }
933                }
934            }
935            _ => {
936                // default: pattern class plus optional suffix
937                let ep = classend(ms, p)?;
938                let s2 = handle_class_with_suffix(ms, s, p, ep)?;
939                break 'outer Ok(s2);
940            }
941        }
942    };
943
944    ms.matchdepth += 1;
945    result
946}
947
948/// Handle a pattern class element with an optional repetition suffix (`*`, `+`, `?`, `-`).
949///
950/// PORT NOTE: Factored out from `match_pat`'s `default/dflt` label to share
951/// code between the ESC-default and plain-default paths.
952fn handle_class_with_suffix(
953    ms: &mut MatchState,
954    s: usize,
955    p: usize,
956    ep: usize,
957) -> Result<Option<usize>, LuaError> {
958    let matched_once = singlematch(ms, s, p, ep);
959    if !matched_once {
960        // C: if (*ep == '*' || *ep == '?' || *ep == '-') { p = ep+1; goto init; }
961        //    else s = NULL;
962        match ms.pat.get(ep).copied() {
963            Some(b'*') | Some(b'?') | Some(b'-') => {
964                // Accept zero occurrences: tail-call match(ms, s, ep+1)
965                // We can't do a tail call into match_pat because we're returning
966                // from handle_class_with_suffix, but we can call it directly.
967                return match_pat(ms, s, ep + 1);
968            }
969            _ => return Ok(None),
970        }
971    }
972
973    // Matched at least once
974    match ms.pat.get(ep).copied() {
975        Some(b'?') => {
976            // Optional: try matching with s+1, fall back to ep+1
977            let res = match_pat(ms, s + 1, ep + 1)?;
978            if res.is_some() {
979                Ok(res)
980            } else {
981                match_pat(ms, s, ep + 1)
982            }
983        }
984        Some(b'+') => {
985            // 1 or more: greedy from s+1
986            max_expand(ms, s + 1, p, ep)
987        }
988        Some(b'*') => {
989            // 0 or more: greedy from s
990            max_expand(ms, s, p, ep)
991        }
992        Some(b'-') => {
993            // 0 or more: lazy from s
994            min_expand(ms, s, p, ep)
995        }
996        _ => {
997            // No suffix: match one, advance both s and p
998            match_pat(ms, s + 1, ep)
999        }
1000    }
1001}
1002
1003// ────────────────────────────────────────────────────────────────────────────
1004// §5  Pattern-matching public API helpers
1005// ────────────────────────────────────────────────────────────────────────────
1006
1007/// Find `needle` in `haystack` using a plain memmem-style search.
1008///
1009/// C: `static const char *lmemfind(const char *s1, size_t l1, const char *s2, size_t l2)`
1010/// Returns the byte-offset of the first occurrence, or `None`.
1011fn lmemfind(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1012    if needle.is_empty() {
1013        return Some(0);
1014    }
1015    if needle.len() > haystack.len() {
1016        return None;
1017    }
1018    let first = needle[0];
1019    let rest = &needle[1..];
1020    let limit = haystack.len() - rest.len();
1021    let mut s = 0;
1022    while s <= limit {
1023        if let Some(pos) = haystack[s..].iter().position(|&b| b == first) {
1024            let pos = s + pos;
1025            if pos + 1 + rest.len() <= haystack.len()
1026                && &haystack[pos + 1..pos + 1 + rest.len()] == rest
1027            {
1028                return Some(pos);
1029            }
1030            s = pos + 1;
1031        } else {
1032            break;
1033        }
1034    }
1035    None
1036}
1037
1038/// Check whether the pattern `pat` has no special characters (for plain search).
1039///
1040/// C: `static int nospecials(const char *p, size_t l)`
1041fn nospecials(pat: &[u8]) -> bool {
1042    !pat.iter().any(|b| SPECIALS.contains(b))
1043}
1044
1045/// Information about one capture result.
1046enum CaptureInfo<'a> {
1047    /// A position capture; value is 1-based index.
1048    Position(i64),
1049    /// A string capture (slice of source).
1050    Bytes(&'a [u8]),
1051}
1052
1053/// Get information about the `i`-th capture.
1054/// If there are no captures and `i == 0`, returns the whole match `s..e`.
1055///
1056/// C: `static size_t get_onecapture(MatchState *ms, int i, const char *s, const char *e, const char **cap)`
1057fn get_one_capture<'a>(
1058    ms: &'a MatchState,
1059    i: usize,
1060    s: usize,
1061    e: usize,
1062) -> Result<CaptureInfo<'a>, LuaError> {
1063    if i >= ms.level as usize {
1064        // C: if (i != 0) luaL_error(...)
1065        if i != 0 {
1066            return Err(LuaError::runtime(format_args!(
1067                "invalid capture index %{}",
1068                i + 1
1069            )));
1070        }
1071        // Return whole match
1072        return Ok(CaptureInfo::Bytes(&ms.src[s..e]));
1073    }
1074    let cap = &ms.captures[i];
1075    if cap.len == CAP_UNFINISHED {
1076        return Err(LuaError::runtime(format_args!("unfinished capture")));
1077    }
1078    if cap.len == CAP_POSITION {
1079        // C: lua_pushinteger(ms->L, (ms->capture[i].init - ms->src_init) + 1);
1080        return Ok(CaptureInfo::Position((cap.init + 1) as i64));
1081    }
1082    let len = cap.len as usize;
1083    Ok(CaptureInfo::Bytes(&ms.src[cap.init..cap.init + len]))
1084}
1085
1086/// Push all captures (or whole match if none) onto the stack.
1087/// Returns the number of values pushed.
1088///
1089/// C: `static int push_captures(MatchState *ms, const char *s, const char *e)`
1090fn push_captures(
1091    state: &mut LuaState,
1092    ms: &MatchState,
1093    s: usize,
1094    e: usize,
1095) -> Result<usize, LuaError> {
1096    let nlevels = if ms.level == 0 { 1 } else { ms.level as usize };
1097    state.ensure_stack(nlevels as i32, "too many captures")?;
1098    for i in 0..nlevels {
1099        match get_one_capture(ms, i, s, e)? {
1100            CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1101            CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1102        }
1103    }
1104    Ok(nlevels)
1105}
1106
1107// ────────────────────────────────────────────────────────────────────────────
1108// §6  str_find / str_match / gmatch / gsub
1109// ────────────────────────────────────────────────────────────────────────────
1110
1111/// Shared implementation of `string.find` and `string.match`.
1112///
1113/// C: `static int str_find_aux(lua_State *L, int find)`
1114fn str_find_aux(state: &mut LuaState, find: bool) -> Result<usize, LuaError> {
1115    let s_ref = match state.to_lua_string(1) {
1116        Some(r) => r,
1117        None => {
1118            state.check_arg_string(1)?;
1119            unreachable!("check_arg_string raises when arg #1 is not a string");
1120        }
1121    };
1122    let p_ref = match state.to_lua_string(2) {
1123        Some(r) => r,
1124        None => {
1125            state.check_arg_string(2)?;
1126            unreachable!("check_arg_string raises when arg #2 is not a string");
1127        }
1128    };
1129    let s: &[u8] = s_ref.as_bytes();
1130    let p: &[u8] = p_ref.as_bytes();
1131    let ls = s.len();
1132    let lp = p.len();
1133    let init_raw = state.opt_arg_integer(3, 1)?;
1134    let init = pos_relat_i(init_raw, ls).saturating_sub(1);
1135
1136    if init > ls {
1137        state.push(LuaValue::Nil);
1138        return Ok(1);
1139    }
1140
1141    // C: if (find && (lua_toboolean(L, 4) || nospecials(p, lp)))
1142    if find && (state.arg_to_bool(4) || nospecials(p)) {
1143        // plain search
1144        if let Some(pos) = lmemfind(&s[init..], p) {
1145            let abs = init + pos;
1146            state.push(LuaValue::Int((abs + 1) as i64));
1147            state.push(LuaValue::Int((abs + lp) as i64));
1148            return Ok(2);
1149        }
1150    } else {
1151        let mut ms = MatchState::new(s, p);
1152        let anchor = p.first() == Some(&b'^');
1153        let (p_start, p_slice) = if anchor {
1154            (0, &p[1..])
1155        } else {
1156            (0, p)
1157        };
1158        ms.pat = p_slice;
1159
1160        let mut s1 = init;
1161        loop {
1162            ms.reset_level();
1163            if let Some(res) = match_pat(&mut ms, s1, 0)? {
1164                if find {
1165                    state.push(LuaValue::Int((s1 + 1) as i64));
1166                    state.push(LuaValue::Int(res as i64));
1167                    let nc = push_captures(state, &ms, 0, 0)?;
1168                    return Ok(nc + 2);
1169                } else {
1170                    return push_captures(state, &ms, s1, res);
1171                }
1172            }
1173            if s1 >= ms.src.len() || anchor {
1174                break;
1175            }
1176            s1 += 1;
1177        }
1178    }
1179
1180    // C: luaL_pushfail(L); return 1;
1181    state.push(LuaValue::Nil);
1182    Ok(1)
1183}
1184
1185/// `string.find(s, pattern [, init [, plain]])` — find pattern in `s`.
1186///
1187/// C: `static int str_find(lua_State *L)`
1188pub fn str_find(state: &mut LuaState) -> Result<usize, LuaError> {
1189    str_find_aux(state, true)
1190}
1191
1192/// `string.match(s, pattern [, init])` — match pattern against `s`.
1193///
1194/// C: `static int str_match(lua_State *L)`
1195pub fn str_match(state: &mut LuaState) -> Result<usize, LuaError> {
1196    str_find_aux(state, false)
1197}
1198
1199/// Continuation function for `string.gmatch` iterator closure.
1200///
1201/// C: `static int gmatch_aux(lua_State *L)`
1202///
1203/// PORT NOTE: The C version stores `GMatchState` inside a heap-allocated
1204/// userdata referenced by upvalue 3, then mutates fields via the raw pointer
1205/// each iteration. Our Phase-A `LuaCClosure.upvalues` is immutable, so the
1206/// iterator state lives in a Lua table referenced by upvalue 1 with
1207/// integer-keyed slots:
1208///   t[1] = source bytes (string), t[2] = pattern bytes (string),
1209///   t[3] = current source position (1-based; equals `lastmatch` after a
1210///   successful match), t[4] = end of last match (`0` ≡ NULL in C, meaning
1211///   "no match yet").
1212///
1213/// PERF NOTE: The previous version pushed the upvalue table onto the stack
1214/// and then issued six `raw_geti` / `raw_seti` calls plus four `to_lua_string`
1215/// / `to_integer_x` reads — each of which re-resolves the stack index via
1216/// `index_to_value`. That made `index_to_value` the #1 non-algorithm frame in
1217/// `string_ops_long` at 9.4% of wall. The current version resolves the
1218/// upvalue once via `value_at`, extracts the `GcRef<LuaTable>`, and reads /
1219/// writes its integer-keyed slots directly through `LuaTableRefExt`. This is
1220/// the same shape as C-Lua's `luaH_getint` / `luaH_setint` direct table ops
1221/// against the embedded `GMatchState` struct fields — no stack roundtrip
1222/// per probe.
1223pub fn gmatch_aux(state: &mut LuaState) -> Result<usize, LuaError> {
1224    let upval = state.value_at(upvalue_index(1));
1225    let tbl = match upval {
1226        LuaValue::Table(t) => t,
1227        _ => return Ok(0),
1228    };
1229
1230    let s_val = tbl.get_int(1);
1231    let p_val = tbl.get_int(2);
1232    let (LuaValue::Str(s_str), LuaValue::Str(p_str)) = (&s_val, &p_val) else {
1233        return Ok(0);
1234    };
1235    let s: &[u8] = s_str.as_bytes();
1236    let p: &[u8] = p_str.as_bytes();
1237
1238    let pos = match tbl.get_int(3) {
1239        LuaValue::Int(n) => n,
1240        _ => 1,
1241    };
1242    let lastmatch_raw = match tbl.get_int(4) {
1243        LuaValue::Int(n) => n,
1244        _ => 0,
1245    };
1246    let last_match: Option<usize> = if lastmatch_raw <= 0 {
1247        None
1248    } else {
1249        Some((lastmatch_raw - 1) as usize)
1250    };
1251
1252    let ls = s.len();
1253    let start_pos = if pos < 1 { 0usize } else { (pos - 1) as usize };
1254
1255    let mut ms = MatchState::new(s, p);
1256
1257    let mut src = start_pos;
1258    while src <= ls {
1259        ms.reset_level();
1260        if let Some(e) = match_pat(&mut ms, src, 0)? {
1261            if Some(e) != last_match {
1262                let e_val = LuaValue::Int((e + 1) as i64);
1263                tbl.raw_set_int(state, 3, e_val.clone())?;
1264                tbl.raw_set_int(state, 4, e_val)?;
1265                return push_captures(state, &ms, src, e);
1266            }
1267        }
1268        src += 1;
1269    }
1270
1271    Ok(0)
1272}
1273
1274/// `string.gmatch(s, pattern [, init])` — return an iterator for all matches.
1275///
1276/// C: `static int gmatch(lua_State *L)`
1277///
1278/// PORT NOTE: C uses `lua_newuserdatauv` for the GMatchState plus a 3-upvalue
1279/// C closure. Phase-A LuaCClosure upvalues are immutable, so we collapse the
1280/// state into a 4-element Lua table held in a single upvalue (see
1281/// `gmatch_aux`).
1282pub fn gmatch(state: &mut LuaState) -> Result<usize, LuaError> {
1283    let s_ref = match state.to_lua_string(1) {
1284        Some(r) => r,
1285        None => {
1286            state.check_arg_string(1)?;
1287            unreachable!("check_arg_string raises when arg #1 is not a string");
1288        }
1289    };
1290    let ls = s_ref.len();
1291    match state.to_lua_string(2) {
1292        Some(_) => {}
1293        None => {
1294            state.check_arg_string(2)?;
1295            unreachable!("check_arg_string raises when arg #2 is not a string");
1296        }
1297    };
1298    drop(s_ref);
1299    let init_raw = state.opt_arg_integer(3, 1)?;
1300    let mut init = pos_relat_i(init_raw, ls).saturating_sub(1);
1301    if init > ls {
1302        init = ls + 1;
1303    }
1304
1305    lua_vm::api::set_top(state, 2)?;
1306
1307    state.create_table(4, 0)?;
1308    let tbl_idx = state.top();
1309    state.push_value_at(1)?;
1310    state.raw_seti(tbl_idx, 1)?;
1311    state.push_value_at(2)?;
1312    state.raw_seti(tbl_idx, 2)?;
1313    state.push(LuaValue::Int((init + 1) as i64));
1314    state.raw_seti(tbl_idx, 3)?;
1315    state.push(LuaValue::Int(0));
1316    state.raw_seti(tbl_idx, 4)?;
1317
1318    state.push_c_closure(gmatch_aux, 1)?;
1319    Ok(1)
1320}
1321
1322/// Add a replacement string with `%n` capture references to `buf`.
1323///
1324/// C: `static void add_s(MatchState *ms, luaL_Buffer *b, const char *s, const char *e)`
1325fn add_s(
1326    state: &mut LuaState,
1327    ms: &MatchState,
1328    buf: &mut Vec<u8>,
1329    s: usize,
1330    e: usize,
1331) -> Result<(), LuaError> {
1332    // C: const char *news = lua_tolstring(L, 3, &l);
1333    let news_bytes = state.to_lua_string_bytes(3).unwrap_or_default();
1334    let mut i = 0usize;
1335    while i < news_bytes.len() {
1336        if news_bytes[i] != L_ESC {
1337            buf.push(news_bytes[i]);
1338            i += 1;
1339        } else {
1340            i += 1; // skip ESC
1341            if i >= news_bytes.len() {
1342                break;
1343            }
1344            let c = news_bytes[i];
1345            if c == L_ESC {
1346                buf.push(L_ESC);
1347            } else if c == b'0' {
1348                buf.extend_from_slice(&ms.src[s..e]);
1349            } else if c.is_ascii_digit() {
1350                match get_one_capture(ms, (c - b'1') as usize, s, e)? {
1351                    CaptureInfo::Position(n) => {
1352                        // push position then pop into buf
1353                        // C: luaL_addvalue(b);  -- adds the top of stack
1354                        let formatted = format!("{}", n).into_bytes();
1355                        buf.extend_from_slice(&formatted);
1356                    }
1357                    CaptureInfo::Bytes(b) => {
1358                        buf.extend_from_slice(b);
1359                    }
1360                }
1361            } else {
1362                return Err(LuaError::runtime(format_args!(
1363                    "invalid use of '{}' in replacement string",
1364                    L_ESC as char
1365                )));
1366            }
1367            i += 1;
1368        }
1369    }
1370    Ok(())
1371}
1372
1373/// Add the replacement value (string, table lookup, or function call) to `buf`.
1374/// Returns `true` if the original text was changed.
1375///
1376/// C: `static int add_value(MatchState *ms, luaL_Buffer *b, const char *s, const char *e, int tr)`
1377fn add_value(
1378    state: &mut LuaState,
1379    ms: &MatchState,
1380    buf: &mut Vec<u8>,
1381    s: usize,
1382    e: usize,
1383    tr: LuaType,
1384) -> Result<bool, LuaError> {
1385    match tr {
1386        LuaType::Function => {
1387            // C: lua_pushvalue(L, 3); n = push_captures(...); lua_call(L, n, 1);
1388            state.push_value_at(3)?;
1389            let n = push_captures(state, ms, s, e)?;
1390            state.call(n as i32, 1)?;
1391        }
1392        LuaType::Table => {
1393            // C: push_onecapture(ms, 0, s, e); lua_gettable(L, 3);
1394            match get_one_capture(ms, 0, s, e)? {
1395                CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1396                CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1397            }
1398            state.get_table(3)?;
1399        }
1400        _ => {
1401            // LUA_TNUMBER or LUA_TSTRING: add replacement string directly
1402            add_s(state, ms, buf, s, e)?;
1403            return Ok(true);
1404        }
1405    }
1406
1407    // C: if (!lua_toboolean(L, -1)) { lua_pop(L, 1); addlstring(b, s, e-s); return 0; }
1408    let top_bool = state.arg_to_bool(-1);
1409    if !top_bool {
1410        state.pop_n(1);
1411        buf.extend_from_slice(&ms.src[s..e]);
1412        return Ok(false);
1413    }
1414    if state.type_at(-1) != LuaType::String {
1415        let tname = state.type_name_at(-1).to_owned();
1416        return Err(LuaError::runtime(format_args!(
1417            "invalid replacement value (a {})", tname.escape_ascii()
1418        )));
1419    }
1420    // C: luaL_addvalue(b);
1421    let v = state.to_bytes(-1).unwrap_or_default();
1422    state.pop();
1423    buf.extend_from_slice(&v);
1424    Ok(true)
1425}
1426
1427/// `string.gsub(s, pattern, repl [, n])` — global substitution.
1428///
1429/// C: `static int str_gsub(lua_State *L)`
1430pub fn str_gsub(state: &mut LuaState) -> Result<usize, LuaError> {
1431    let src_bytes = state.check_arg_string(1)?;
1432    let pat_bytes = state.check_arg_string(2)?;
1433    let src_len = src_bytes.len();
1434    let max_s = state.opt_arg_integer(4, (src_len + 1) as i64)?;
1435    let tr = state.type_at(3);
1436
1437    // C: luaL_argexpected(..., tr == TNUMBER || tr == TSTRING || tr == TFUNCTION || tr == TTABLE, ...)
1438    if !matches!(tr, LuaType::Number | LuaType::String | LuaType::Function | LuaType::Table) {
1439        let v = state.arg(3);
1440        return Err(LuaError::type_arg_error(3, "string/function/table", &v));
1441    }
1442
1443    let src_owned = src_bytes;
1444    let pat_owned = pat_bytes;
1445
1446    let anchor = pat_owned.first() == Some(&b'^');
1447    let pat_slice = if anchor { &pat_owned[1..] } else { &pat_owned[..] };
1448
1449    let mut ms = MatchState::new(&src_owned, pat_slice);
1450    let mut buf: Vec<u8> = Vec::new();
1451    let mut src_pos = 0usize;
1452    let mut last_match: Option<usize> = None;
1453    let mut n: i64 = 0;
1454    let mut changed = false;
1455
1456    while n < max_s {
1457        ms.reset_level();
1458        let maybe_e = match_pat(&mut ms, src_pos, 0)?;
1459        if let Some(e) = maybe_e {
1460            if last_match != Some(e) {
1461                n += 1;
1462                let delta = add_value(state, &ms, &mut buf, src_pos, e, tr)?;
1463                changed |= delta;
1464                src_pos = e;
1465                last_match = Some(e);
1466            } else if src_pos < ms.src.len() {
1467                buf.push(ms.src[src_pos]);
1468                src_pos += 1;
1469            } else {
1470                break;
1471            }
1472        } else if src_pos < ms.src.len() {
1473            buf.push(ms.src[src_pos]);
1474            src_pos += 1;
1475        } else {
1476            break;
1477        }
1478        if anchor {
1479            break;
1480        }
1481    }
1482
1483    if !changed {
1484        // C: lua_pushvalue(L, 1); /* return original string */
1485        state.push_value_at(1)?;
1486    } else {
1487        buf.extend_from_slice(&ms.src[src_pos..]);
1488        state.push_bytes(&buf)?;
1489    }
1490    state.push(LuaValue::Int(n));
1491    Ok(2)
1492}
1493
1494// ────────────────────────────────────────────────────────────────────────────
1495// §7  String format (`string.format`)
1496// ────────────────────────────────────────────────────────────────────────────
1497
1498/// Add a hex-float digit to buffer and return the fractional remainder.
1499///
1500/// C: `static lua_Number adddigit(char *buff, int n, lua_Number x)`
1501fn adddigit(buf: &mut Vec<u8>, x: f64) -> f64 {
1502    let dd = x.floor();
1503    let d = dd as i32;
1504    let c = if d < 10 { b'0' + d as u8 } else { b'a' + (d - 10) as u8 };
1505    buf.push(c);
1506    x - dd
1507}
1508
1509/// Convert a float to a hex-float string body (digits only, no sign, no `0x` prefix).
1510///
1511/// Returns `(frac_digits, exponent_string)` for use by `format_hex_float`.
1512///
1513/// C: `static int num2straux(char *buff, int sz, lua_Number x)`
1514fn num2straux(x: f64) -> Vec<u8> {
1515    format_hex_float(x, None)
1516}
1517
1518/// Produce a hex-float string for `x` with optional precision (digits after the point).
1519///
1520/// When `precision` is `None` the minimum number of digits needed for a round-trip
1521/// is emitted (C's default `%a` behaviour). When `precision` is `Some(p)` exactly `p`
1522/// digits follow the radix point; trailing zeros are added as needed, and excess
1523/// digits are discarded (C truncates rather than rounds, matching the C `printf`
1524/// behaviour on the tested platforms).
1525fn format_hex_float(x: f64, precision: Option<usize>) -> Vec<u8> {
1526    if x.is_nan() {
1527        return b"nan".to_vec();
1528    }
1529    if x.is_infinite() {
1530        return if x < 0.0 { b"-inf".to_vec() } else { b"inf".to_vec() };
1531    }
1532    if x == 0.0 {
1533        let sign: &[u8] = if x.is_sign_negative() { b"-" } else { b"" };
1534        return match precision {
1535            None => [sign, b"0x0p+0"].concat(),
1536            Some(0) => [sign, b"0x0p+0"].concat(),
1537            Some(p) => {
1538                let zeros = "0".repeat(p);
1539                [sign, b"0x0.", zeros.as_bytes(), b"p+0"].concat()
1540            }
1541        };
1542    }
1543
1544    let (m_raw, exp) = frexp(x);
1545    let mut buf: Vec<u8> = Vec::new();
1546    let mut m = m_raw;
1547    if m < 0.0 {
1548        buf.push(b'-');
1549        m = -m;
1550    }
1551    buf.extend_from_slice(b"0x");
1552
1553    let nbfd = 1;
1554    m = adddigit(&mut buf, m * (1 << nbfd) as f64);
1555    let e = exp - nbfd;
1556
1557    match precision {
1558        None => {
1559            if m > 0.0 {
1560                buf.push(b'.');
1561                while m > 0.0 {
1562                    m = adddigit(&mut buf, m * 16.0);
1563                }
1564            }
1565        }
1566        Some(0) => {}
1567        Some(p) => {
1568            buf.push(b'.');
1569            for _ in 0..p {
1570                if m > 0.0 {
1571                    m = adddigit(&mut buf, m * 16.0);
1572                } else {
1573                    buf.push(b'0');
1574                }
1575            }
1576        }
1577    }
1578
1579    let exp_str = format!("p{:+}", e);
1580    buf.extend_from_slice(exp_str.as_bytes());
1581    buf
1582}
1583
1584/// Decompose `x` into mantissa in `[-1.0, -0.5] ∪ [0.5, 1.0)` and exponent.
1585///
1586/// Equivalent to C's `frexp`. The sign of `x` is preserved in the returned mantissa
1587/// so that `num2straux` can emit the leading `-` correctly for negative inputs.
1588fn frexp(x: f64) -> (f64, i32) {
1589    if x == 0.0 || x.is_nan() || x.is_infinite() {
1590        return (x, 0);
1591    }
1592    let bits = x.to_bits();
1593    let sign_bit = bits & 0x8000_0000_0000_0000u64;
1594    let exp_bits = ((bits >> 52) & 0x7FF) as i32;
1595    if exp_bits == 0 {
1596        let (m, e) = frexp(x * (1u64 << 52) as f64);
1597        return (m, e - 52);
1598    }
1599    let exp = exp_bits - 1022;
1600    let mantissa_bits = sign_bit | (bits & 0x000F_FFFF_FFFF_FFFF) | 0x3FE0_0000_0000_0000;
1601    (f64::from_bits(mantissa_bits), exp)
1602}
1603
1604/// Convert float `n` to a Lua-readable literal (hex or special representation).
1605///
1606/// C: `static int quotefloat(lua_State *L, char *buff, lua_Number n)`
1607fn quotefloat(n: f64) -> Vec<u8> {
1608    if n == f64::INFINITY {
1609        return b"1e9999".to_vec();
1610    } else if n == f64::NEG_INFINITY {
1611        return b"-1e9999".to_vec();
1612    } else if n.is_nan() {
1613        return b"(0/0)".to_vec();
1614    }
1615    // hex float, ensuring dot separator
1616    let mut buf = num2straux(n);
1617    if !buf.contains(&b'.') && !buf.contains(&b'p') {
1618        // try to find locale decimal point and replace with '.'
1619        // PORT NOTE: We always produce '.' so this branch is not taken.
1620    }
1621    buf
1622}
1623
1624/// Add a quoted Lua string literal to `buf`.
1625///
1626/// C: `static void addquoted(luaL_Buffer *b, const char *s, size_t len)`
1627fn addquoted(buf: &mut Vec<u8>, s: &[u8]) {
1628    buf.push(b'"');
1629    for (idx, &c) in s.iter().enumerate() {
1630        if c == b'"' || c == b'\\' || c == b'\n' {
1631            buf.push(b'\\');
1632            buf.push(c);
1633        } else if c.is_ascii_control() {
1634            let next_is_digit = s.get(idx + 1).map_or(false, |n| n.is_ascii_digit());
1635            let formatted = if next_is_digit {
1636                format!("\\{:03}", c)
1637            } else {
1638                format!("\\{}", c)
1639            };
1640            buf.extend_from_slice(formatted.as_bytes());
1641        } else {
1642            buf.push(c);
1643        }
1644    }
1645    buf.push(b'"');
1646}
1647
1648/// Add a Lua literal representation of arg `n` to `buf`.
1649///
1650/// C: `static void addliteral(lua_State *L, luaL_Buffer *b, int arg)`
1651fn addliteral(state: &mut LuaState, buf: &mut Vec<u8>, arg: i32) -> Result<(), LuaError> {
1652    match state.type_at(arg) {
1653        LuaType::String => {
1654            let s = state.check_arg_string(arg)?.to_vec();
1655            addquoted(buf, &s);
1656        }
1657        LuaType::Number => {
1658            if state.is_integer(arg) {
1659                let n = state.to_integer(arg).unwrap_or(0);
1660                let formatted = if n == i64::MIN {
1661                    format!("0x{:016x}", n as u64)
1662                } else {
1663                    format!("{}", n)
1664                };
1665                buf.extend_from_slice(formatted.as_bytes());
1666            } else {
1667                let n = state.to_number(arg).unwrap_or(0.0);
1668                let hex = quotefloat(n);
1669                buf.extend_from_slice(&hex);
1670            }
1671        }
1672        LuaType::Nil => {
1673            buf.extend_from_slice(b"nil");
1674        }
1675        LuaType::Boolean => {
1676            buf.extend_from_slice(if state.to_boolean(arg) { b"true" } else { b"false" });
1677        }
1678        _ => {
1679            return Err(LuaError::arg_error(arg, "value has no literal form"));
1680        }
1681    }
1682    Ok(())
1683}
1684
1685/// C: `MAX_FORMAT - 10 = 22` threshold from lstrlib.c — format body `spec_body_len + 1` must be < 22.
1686
1687/// Flags allowed per conversion type (matches lstrlib.c constants).
1688const FMT_FLAGS_F: &[u8] = b"-+#0 ";
1689const FMT_FLAGS_X: &[u8] = b"-#0";
1690const FMT_FLAGS_I: &[u8] = b"-+0 ";
1691const FMT_FLAGS_U: &[u8] = b"-0";
1692const FMT_FLAGS_C: &[u8] = b"-";
1693
1694/// Validate a format specifier against allowed flags and width/precision digit counts.
1695///
1696/// `form` is the full specifier slice including the leading `%` and the trailing
1697/// conversion character (e.g. `b"%100.3d"`). `flags` is the allowed-flags byte set for
1698/// this conversion type. `allow_precision` is false for conversions that forbid `.`.
1699///
1700/// Mirrors C `checkformat` in lstrlib.c: consumes flags, then up to 2 width digits,
1701/// then (if allowed) `.` + up to 2 precision digits, then asserts we are at the
1702/// conversion character. Returns `Err("invalid conversion specification")` on failure.
1703fn check_conv_spec(form: &[u8], flags: &[u8], allow_precision: bool) -> Result<(), LuaError> {
1704    let mut i = 1usize; // skip '%'
1705    while i < form.len() && flags.contains(&form[i]) {
1706        i += 1;
1707    }
1708    if i < form.len() && form[i] == b'0' {
1709        return Err(LuaError::runtime(format_args!("invalid conversion specification")));
1710    }
1711    if i < form.len() && form[i].is_ascii_digit() {
1712        i += 1;
1713        if i < form.len() && form[i].is_ascii_digit() {
1714            i += 1;
1715        }
1716    }
1717    if allow_precision && i < form.len() && form[i] == b'.' {
1718        i += 1;
1719        if i < form.len() && form[i].is_ascii_digit() {
1720            i += 1;
1721            if i < form.len() && form[i].is_ascii_digit() {
1722                i += 1;
1723            }
1724        }
1725    }
1726    if i != form.len() - 1 {
1727        return Err(LuaError::runtime(format_args!("invalid conversion specification")));
1728    }
1729    Ok(())
1730}
1731
1732/// Parsed printf-style format specifier (flags, width, precision).
1733#[derive(Default)]
1734struct FmtSpec {
1735    left_align: bool,
1736    plus_sign: bool,
1737    space_sign: bool,
1738    alt_form: bool,
1739    zero_pad: bool,
1740    width: usize,
1741    precision: Option<usize>,
1742}
1743
1744fn parse_fmt_spec(spec: &[u8]) -> FmtSpec {
1745    let mut s = FmtSpec::default();
1746    let mut i = 0;
1747    while i < spec.len() {
1748        match spec[i] {
1749            b'-' => s.left_align = true,
1750            b'+' => s.plus_sign = true,
1751            b' ' => s.space_sign = true,
1752            b'#' => s.alt_form = true,
1753            b'0' => s.zero_pad = true,
1754            _ => break,
1755        }
1756        i += 1;
1757    }
1758    while i < spec.len() && spec[i].is_ascii_digit() {
1759        s.width = s.width * 10 + (spec[i] - b'0') as usize;
1760        i += 1;
1761    }
1762    if i < spec.len() && spec[i] == b'.' {
1763        i += 1;
1764        let mut p = 0usize;
1765        while i < spec.len() && spec[i].is_ascii_digit() {
1766            p = p * 10 + (spec[i] - b'0') as usize;
1767            i += 1;
1768        }
1769        s.precision = Some(p);
1770    }
1771    s
1772}
1773
1774fn pad_str(buf: &mut Vec<u8>, body: &[u8], spec: &FmtSpec) {
1775    let body = match spec.precision {
1776        Some(p) if body.len() > p => &body[..p],
1777        _ => body,
1778    };
1779    if body.len() >= spec.width {
1780        buf.extend_from_slice(body);
1781        return;
1782    }
1783    let pad = spec.width - body.len();
1784    if spec.left_align {
1785        buf.extend_from_slice(body);
1786        for _ in 0..pad { buf.push(b' '); }
1787    } else {
1788        for _ in 0..pad { buf.push(b' '); }
1789        buf.extend_from_slice(body);
1790    }
1791}
1792
1793fn pad_int(buf: &mut Vec<u8>, sign_prefix: &[u8], digits: &[u8], spec: &FmtSpec) {
1794    let min_digits = spec.precision.unwrap_or(0);
1795    let zeroes_for_prec = if digits.len() < min_digits { min_digits - digits.len() } else { 0 };
1796    let core_len = sign_prefix.len() + zeroes_for_prec + digits.len();
1797    if core_len >= spec.width {
1798        buf.extend_from_slice(sign_prefix);
1799        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1800        buf.extend_from_slice(digits);
1801        return;
1802    }
1803    let pad = spec.width - core_len;
1804    let use_zero_pad = spec.zero_pad && !spec.left_align && spec.precision.is_none();
1805    if spec.left_align {
1806        buf.extend_from_slice(sign_prefix);
1807        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1808        buf.extend_from_slice(digits);
1809        for _ in 0..pad { buf.push(b' '); }
1810    } else if use_zero_pad {
1811        buf.extend_from_slice(sign_prefix);
1812        for _ in 0..pad { buf.push(b'0'); }
1813        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1814        buf.extend_from_slice(digits);
1815    } else {
1816        for _ in 0..pad { buf.push(b' '); }
1817        buf.extend_from_slice(sign_prefix);
1818        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1819        buf.extend_from_slice(digits);
1820    }
1821}
1822
1823fn signed_int_parts(n: i64, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
1824    if n == 0 && spec.precision == Some(0) {
1825        return (Vec::new(), Vec::new());
1826    }
1827    let (sign, abs_digits) = if n < 0 {
1828        (b"-".to_vec(), {
1829            let u = (n as i128).unsigned_abs();
1830            format!("{}", u).into_bytes()
1831        })
1832    } else {
1833        let s: Vec<u8> = if spec.plus_sign {
1834            b"+".to_vec()
1835        } else if spec.space_sign {
1836            b" ".to_vec()
1837        } else {
1838            Vec::new()
1839        };
1840        (s, format!("{}", n).into_bytes())
1841    };
1842    (sign, abs_digits)
1843}
1844
1845fn unsigned_int_parts(n: u64, base: u32, upper: bool, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
1846    let digits = if n == 0 && spec.precision == Some(0) {
1847        Vec::new()
1848    } else {
1849        match base {
1850            8 => format!("{:o}", n).into_bytes(),
1851            16 if upper => format!("{:X}", n).into_bytes(),
1852            16 => format!("{:x}", n).into_bytes(),
1853            _ => format!("{}", n).into_bytes(),
1854        }
1855    };
1856    let prefix: Vec<u8> = if spec.alt_form && n != 0 {
1857        match base {
1858            8 => b"0".to_vec(),
1859            16 if upper => b"0X".to_vec(),
1860            16 => b"0x".to_vec(),
1861            _ => Vec::new(),
1862        }
1863    } else {
1864        Vec::new()
1865    };
1866    (prefix, digits)
1867}
1868
1869fn format_float(n: f64, conv: u8, spec: &FmtSpec) -> Vec<u8> {
1870    let prec = spec.precision.unwrap_or(6);
1871    if n.is_nan() {
1872        return if conv.is_ascii_uppercase() { b"NAN".to_vec() } else { b"nan".to_vec() };
1873    }
1874    if n.is_infinite() {
1875        let s: &[u8] = if conv.is_ascii_uppercase() {
1876            if n < 0.0 { b"-INF" } else { b"INF" }
1877        } else if n < 0.0 { b"-inf" } else { b"inf" };
1878        return s.to_vec();
1879    }
1880    match conv {
1881        b'f' | b'F' => {
1882            let mut result = format!("{:.*}", prec, n).into_bytes();
1883            if spec.alt_form && !result.contains(&b'.') {
1884                result.push(b'.');
1885            }
1886            result
1887        }
1888        b'e' => format_exp(n, prec, false, spec.alt_form),
1889        b'E' => {
1890            let mut v = format_exp(n, prec, false, spec.alt_form);
1891            for b in v.iter_mut() { if *b == b'e' { *b = b'E'; } }
1892            v
1893        }
1894        b'g' | b'G' => {
1895            let p = if prec == 0 { 1 } else { prec };
1896            let v = format_g(n, p, spec.alt_form);
1897            if conv == b'G' {
1898                v.into_iter().map(|b| if b == b'e' { b'E' } else { b }).collect()
1899            } else { v }
1900        }
1901        _ => format!("{}", n).into_bytes(),
1902    }
1903}
1904
1905fn format_exp(n: f64, prec: usize, _upper: bool, alt: bool) -> Vec<u8> {
1906    if n == 0.0 {
1907        let mantissa: String = if prec == 0 {
1908            if alt { "0.".to_string() } else { "0".to_string() }
1909        } else {
1910            format!("0.{}", "0".repeat(prec))
1911        };
1912        return format!("{}e+00", mantissa).into_bytes();
1913    }
1914    let abs = n.abs();
1915    let exp = abs.log10().floor() as i32;
1916    let mantissa = n / 10f64.powi(exp);
1917    let mantissa_str = format!("{:.*}", prec, mantissa);
1918    let (mant_final, exp_final) = if let Some(dot_pos) = mantissa_str.find('.') {
1919        let int_part = &mantissa_str[..dot_pos];
1920        let abs_int = int_part.trim_start_matches('-');
1921        if abs_int.len() > 1 {
1922            let new_mant = if prec == 0 {
1923                mantissa_str[..mantissa_str.len()-1].to_string()
1924            } else {
1925                let neg = if int_part.starts_with('-') { "-" } else { "" };
1926                let frac = &mantissa_str[dot_pos+1..];
1927                format!("{}{}.{}{}", neg, &abs_int[..1], &abs_int[1..], frac)
1928            };
1929            (new_mant, exp + (abs_int.len() as i32 - 1))
1930        } else {
1931            (mantissa_str, exp)
1932        }
1933    } else if mantissa_str.trim_start_matches('-').len() > 1 {
1934        let neg = if mantissa_str.starts_with('-') { "-" } else { "" };
1935        let body = mantissa_str.trim_start_matches('-');
1936        let bumped = format!("{}{}.{}", neg, &body[..1], &body[1..]);
1937        (bumped, exp + (body.len() as i32 - 1))
1938    } else {
1939        (mantissa_str, exp)
1940    };
1941    let sign = if exp_final < 0 { '-' } else { '+' };
1942    let mant_out = if alt && !mant_final.contains('.') {
1943        format!("{}.", mant_final)
1944    } else { mant_final };
1945    format!("{}e{}{:02}", mant_out, sign, exp_final.abs()).into_bytes()
1946}
1947
1948fn format_g(n: f64, prec: usize, alt: bool) -> Vec<u8> {
1949    if n == 0.0 {
1950        return if alt { format!("0.{}", "0".repeat(prec.saturating_sub(1))).into_bytes() } else { b"0".to_vec() };
1951    }
1952    let abs = n.abs();
1953    let exp = abs.log10().floor() as i32;
1954    if exp < -4 || exp >= prec as i32 {
1955        let ep = if prec == 0 { 0 } else { prec - 1 };
1956        let mut v = format_exp(n, ep, false, alt);
1957        if !alt {
1958            v = strip_trailing_zeros_exp(&v);
1959        }
1960        v
1961    } else {
1962        let dec_places = (prec as i32 - 1 - exp).max(0) as usize;
1963        let mut v = format!("{:.*}", dec_places, n).into_bytes();
1964        if !alt {
1965            v = strip_trailing_zeros_fixed(&v);
1966        }
1967        v
1968    }
1969}
1970
1971fn strip_trailing_zeros_fixed(s: &[u8]) -> Vec<u8> {
1972    if !s.contains(&b'.') { return s.to_vec(); }
1973    let mut end = s.len();
1974    while end > 0 && s[end-1] == b'0' { end -= 1; }
1975    if end > 0 && s[end-1] == b'.' { end -= 1; }
1976    s[..end].to_vec()
1977}
1978
1979fn strip_trailing_zeros_exp(s: &[u8]) -> Vec<u8> {
1980    let e_pos = match s.iter().position(|&b| b == b'e' || b == b'E') {
1981        Some(p) => p,
1982        None => return s.to_vec(),
1983    };
1984    let mantissa = &s[..e_pos];
1985    let exp_part = &s[e_pos..];
1986    if !mantissa.contains(&b'.') {
1987        let mut out = mantissa.to_vec();
1988        out.extend_from_slice(exp_part);
1989        return out;
1990    }
1991    let mut end = mantissa.len();
1992    while end > 0 && mantissa[end-1] == b'0' { end -= 1; }
1993    if end > 0 && mantissa[end-1] == b'.' { end -= 1; }
1994    let mut out = mantissa[..end].to_vec();
1995    out.extend_from_slice(exp_part);
1996    out
1997}
1998
1999/// `string.format(fmt, ...)` — C-style string formatting.
2000///
2001/// C: `static int str_format(lua_State *L)`
2002pub fn str_format(state: &mut LuaState) -> Result<usize, LuaError> {
2003    let top = state.get_top();
2004    let mut arg = 1i32;
2005    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2006    let mut buf: Vec<u8> = Vec::new();
2007    let mut i = 0usize;
2008
2009    while i < fmt_bytes.len() {
2010        let c = fmt_bytes[i];
2011        if c != L_ESC {
2012            buf.push(c);
2013            i += 1;
2014            continue;
2015        }
2016        i += 1;
2017        if i >= fmt_bytes.len() {
2018            break;
2019        }
2020        if fmt_bytes[i] == L_ESC {
2021            buf.push(L_ESC);
2022            i += 1;
2023            continue;
2024        }
2025
2026        // Parse a format specifier
2027        arg += 1;
2028        if arg > top {
2029            return Err(LuaError::arg_error(arg, "no value"));
2030        }
2031
2032        // Collect flags, width, precision
2033        let spec_start = i - 1; // includes the initial '%'
2034        // Skip flags: -, +, #, 0, space
2035        while i < fmt_bytes.len() && b"-+#0 ".contains(&fmt_bytes[i]) {
2036            i += 1;
2037        }
2038        // Skip width digits
2039        if i < fmt_bytes.len() && fmt_bytes[i] != b'0' {
2040            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2041                i += 1;
2042            }
2043        }
2044        // Skip precision
2045        if i < fmt_bytes.len() && fmt_bytes[i] == b'.' {
2046            i += 1;
2047            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2048                i += 1;
2049            }
2050        }
2051
2052        if i >= fmt_bytes.len() {
2053            return Err(LuaError::runtime(format_args!("invalid conversion specification")));
2054        }
2055
2056        let conv = fmt_bytes[i];
2057        i += 1;
2058
2059        let spec_slice = &fmt_bytes[spec_start + 1..i - 1];
2060        let form = &fmt_bytes[spec_start..i];
2061
2062        // C: getformat rejects spec bodies >= MAX_FORMAT - 10 = 22 bytes.
2063        // Must check before parse_fmt_spec to avoid overflow on huge widths.
2064        if spec_slice.len() + 1 >= 22 {
2065            return Err(LuaError::runtime(format_args!("invalid format (too long)")));
2066        }
2067
2068        let spec = parse_fmt_spec(spec_slice);
2069
2070        match conv {
2071            b'c' => {
2072                check_conv_spec(form, FMT_FLAGS_C, false)?;
2073                let n = state.check_arg_integer(arg)?;
2074                let body = vec![n as u8];
2075                pad_str(&mut buf, &body, &spec);
2076            }
2077            b'd' | b'i' => {
2078                check_conv_spec(form, FMT_FLAGS_I, true)?;
2079                let n = state.check_arg_integer(arg)?;
2080                let (sign, digits) = signed_int_parts(n, &spec);
2081                pad_int(&mut buf, &sign, &digits, &spec);
2082            }
2083            b'u' => {
2084                check_conv_spec(form, FMT_FLAGS_U, true)?;
2085                let n = state.check_arg_integer(arg)? as u64;
2086                let (prefix, digits) = unsigned_int_parts(n, 10, false, &spec);
2087                pad_int(&mut buf, &prefix, &digits, &spec);
2088            }
2089            b'o' => {
2090                check_conv_spec(form, FMT_FLAGS_X, true)?;
2091                let n = state.check_arg_integer(arg)? as u64;
2092                let (prefix, digits) = unsigned_int_parts(n, 8, false, &spec);
2093                pad_int(&mut buf, &prefix, &digits, &spec);
2094            }
2095            b'x' => {
2096                check_conv_spec(form, FMT_FLAGS_X, true)?;
2097                let n = state.check_arg_integer(arg)? as u64;
2098                let (prefix, digits) = unsigned_int_parts(n, 16, false, &spec);
2099                pad_int(&mut buf, &prefix, &digits, &spec);
2100            }
2101            b'X' => {
2102                check_conv_spec(form, FMT_FLAGS_X, true)?;
2103                let n = state.check_arg_integer(arg)? as u64;
2104                let (prefix, digits) = unsigned_int_parts(n, 16, true, &spec);
2105                pad_int(&mut buf, &prefix, &digits, &spec);
2106            }
2107            b'a' | b'A' => {
2108                check_conv_spec(form, FMT_FLAGS_F, true)?;
2109                let n = state.check_arg_number(arg)?;
2110                let body = format_hex_float(n, spec.precision);
2111                let body: Vec<u8> = if conv == b'A' {
2112                    body.into_iter().map(|b| b.to_ascii_uppercase()).collect()
2113                } else {
2114                    body
2115                };
2116                let (sign, digits): (Vec<u8>, Vec<u8>) =
2117                    if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2118                        (vec![body[0]], body[1..].to_vec())
2119                    } else if spec.plus_sign {
2120                        (b"+".to_vec(), body)
2121                    } else if spec.space_sign {
2122                        (b" ".to_vec(), body)
2123                    } else {
2124                        (Vec::new(), body)
2125                    };
2126                let no_prec_spec = FmtSpec {
2127                    left_align: spec.left_align,
2128                    plus_sign: spec.plus_sign,
2129                    space_sign: spec.space_sign,
2130                    alt_form: spec.alt_form,
2131                    zero_pad: spec.zero_pad,
2132                    width: spec.width,
2133                    precision: None,
2134                };
2135                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2136            }
2137            b'f' | b'e' | b'E' | b'g' | b'G' => {
2138                check_conv_spec(form, FMT_FLAGS_F, true)?;
2139                let n = state.check_arg_number(arg)?;
2140                let body = format_float(n, conv, &spec);
2141                let (sign, digits): (Vec<u8>, Vec<u8>) = if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2142                    (vec![body[0]], body[1..].to_vec())
2143                } else if n >= 0.0 && spec.plus_sign {
2144                    (b"+".to_vec(), body)
2145                } else if n >= 0.0 && spec.space_sign {
2146                    (b" ".to_vec(), body)
2147                } else {
2148                    (Vec::new(), body)
2149                };
2150                let no_prec_spec = FmtSpec {
2151                    left_align: spec.left_align,
2152                    plus_sign: spec.plus_sign,
2153                    space_sign: spec.space_sign,
2154                    alt_form: spec.alt_form,
2155                    zero_pad: spec.zero_pad,
2156                    width: spec.width,
2157                    precision: None,
2158                };
2159                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2160            }
2161            b'p' => {
2162                check_conv_spec(form, FMT_FLAGS_C, false)?;
2163                let s: Vec<u8> = match lua_vm::api::to_pointer(state, arg) {
2164                    Some(p) => format!("0x{:x}", p).into_bytes(),
2165                    None => b"(null)".to_vec(),
2166                };
2167                pad_str(&mut buf, &s, &FmtSpec { precision: None, ..spec });
2168            }
2169            b'q' => {
2170                if form.len() > 2 {
2171                    return Err(LuaError::runtime(format_args!(
2172                        "specifier '%q' cannot have modifiers"
2173                    )));
2174                }
2175                addliteral(state, &mut buf, arg)?;
2176            }
2177            b's' => {
2178                check_conv_spec(form, FMT_FLAGS_C, true)?;
2179                let s = state.to_display_string(arg)?;
2180                let has_modifiers = spec.width != 0 || spec.precision.is_some();
2181                if has_modifiers && s.contains(&0u8) {
2182                    return Err(LuaError::arg_error(
2183                        arg,
2184                        "string contains zeros",
2185                    ));
2186                }
2187                pad_str(&mut buf, &s, &spec);
2188                state.pop_n(1);
2189            }
2190            _ => {
2191                return Err(LuaError::runtime(format_args!(
2192                    "invalid conversion '%{}' to 'format'", conv as char
2193                )));
2194            }
2195        }
2196    }
2197
2198    state.push_bytes(&buf)?;
2199    Ok(1)
2200}
2201
2202// ────────────────────────────────────────────────────────────────────────────
2203// §8  Pack / unpack
2204// ────────────────────────────────────────────────────────────────────────────
2205
2206/// Return `true` if `c` is an ASCII digit.
2207fn is_digit(c: u8) -> bool {
2208    c.is_ascii_digit()
2209}
2210
2211/// Read an optional integer from the format string, returning `df` if absent.
2212///
2213/// C: `static int getnum(const char **fmt, int df)`
2214fn getnum(fmt: &[u8], pos: &mut usize, df: i32) -> i32 {
2215    if *pos >= fmt.len() || !is_digit(fmt[*pos]) {
2216        return df;
2217    }
2218    let mut a = 0i32;
2219    while *pos < fmt.len() && is_digit(fmt[*pos]) {
2220        a = a * 10 + (fmt[*pos] - b'0') as i32;
2221        *pos += 1;
2222        if a > (i32::MAX - 9) / 10 {
2223            break;
2224        }
2225    }
2226    a
2227}
2228
2229/// Read an integer from the format string, error if out of `[1, MAXINTSIZE]`.
2230///
2231/// C: `static int getnumlimit(Header *h, const char **fmt, int df)`
2232fn getnumlimit(fmt: &[u8], pos: &mut usize, df: i32) -> Result<usize, LuaError> {
2233    let sz = getnum(fmt, pos, df);
2234    if sz > MAX_INT_SIZE as i32 || sz <= 0 {
2235        return Err(LuaError::runtime(format_args!(
2236            "integral size ({}) out of limits [1,{}]",
2237            sz, MAX_INT_SIZE
2238        )));
2239    }
2240    Ok(sz as usize)
2241}
2242
2243/// Read and classify the next pack format option, filling `size`.
2244///
2245/// C: `static KOption getoption(Header *h, const char **fmt, int *size)`
2246fn getoption(h: &mut Header, fmt: &[u8], pos: &mut usize, size: &mut usize) -> Result<KOption, LuaError> {
2247    // C: struct cD { char c; union { LUAI_MAXALIGN; } u; };
2248    // In Rust, the native max-align of a union of f64/void*/size_t is 8 on 64-bit.
2249    const NATIVE_MAX_ALIGN: usize = std::mem::align_of::<f64>();
2250
2251    if *pos >= fmt.len() {
2252        return Ok(KOption::Nop);
2253    }
2254    let opt = fmt[*pos];
2255    *pos += 1;
2256    *size = 0;
2257
2258    match opt {
2259        b'b' => { *size = 1; Ok(KOption::Int) }
2260        b'B' => { *size = 1; Ok(KOption::Uint) }
2261        b'h' => { *size = 2; Ok(KOption::Int) }
2262        b'H' => { *size = 2; Ok(KOption::Uint) }
2263        b'l' => { *size = 8; Ok(KOption::Int) }  // sizeof(long) on 64-bit
2264        b'L' => { *size = 8; Ok(KOption::Uint) }
2265        b'j' => { *size = SZINT; Ok(KOption::Int) }
2266        b'J' => { *size = SZINT; Ok(KOption::Uint) }
2267        b'T' => { *size = std::mem::size_of::<usize>(); Ok(KOption::Uint) }
2268        b'f' => { *size = 4; Ok(KOption::Float) }
2269        b'n' => { *size = 8; Ok(KOption::Number) }  // sizeof(lua_Number) = sizeof(f64) = 8
2270        b'd' => { *size = 8; Ok(KOption::Double) }  // sizeof(double) = 8
2271        b'i' => { *size = getnumlimit(fmt, pos, 4)?; Ok(KOption::Int) }
2272        b'I' => { *size = getnumlimit(fmt, pos, 4)?; Ok(KOption::Uint) }
2273        b's' => { *size = getnumlimit(fmt, pos, std::mem::size_of::<usize>()  as i32)?; Ok(KOption::Kstring) }
2274        b'c' => {
2275            let n = getnum(fmt, pos, -1);
2276            if n == -1 {
2277                return Err(LuaError::runtime(format_args!("missing size for format option 'c'")));
2278            }
2279            *size = n as usize;
2280            Ok(KOption::Char)
2281        }
2282        b'z' => Ok(KOption::Zstr),
2283        b'x' => { *size = 1; Ok(KOption::Padding) }
2284        b'X' => Ok(KOption::Paddalign),
2285        b' ' => Ok(KOption::Nop),
2286        b'<' => { h.is_little = true; Ok(KOption::Nop) }
2287        b'>' => { h.is_little = false; Ok(KOption::Nop) }
2288        b'=' => { h.is_little = cfg!(target_endian = "little"); Ok(KOption::Nop) }
2289        b'!' => {
2290            let n = getnum(fmt, pos, NATIVE_MAX_ALIGN as i32);
2291            h.max_align = getnumlimit(fmt, pos, n)?;
2292            Ok(KOption::Nop)
2293        }
2294        _ => Err(LuaError::runtime(format_args!("invalid format option '{}'", opt as char)))
2295    }
2296}
2297
2298/// Get full details about the next format option, including alignment padding.
2299///
2300/// C: `static KOption getdetails(Header *h, size_t totalsize, const char **fmt, int *psize, int *ntoalign)`
2301fn getdetails(
2302    h: &mut Header,
2303    total_size: usize,
2304    fmt: &[u8],
2305    pos: &mut usize,
2306    psize: &mut usize,
2307    ntoalign: &mut usize,
2308) -> Result<KOption, LuaError> {
2309    let opt = getoption(h, fmt, pos, psize)?;
2310    let mut align = *psize;
2311
2312    if opt == KOption::Paddalign {
2313        // C: if (**fmt == '\0' || getoption(h, fmt, &align) == Kchar || align == 0) argerror
2314        if *pos >= fmt.len() {
2315            return Err(LuaError::arg_error(1, "invalid next option for option 'X'"));
2316        }
2317        let mut dummy_size = 0usize;
2318        let next_opt = getoption(h, fmt, pos, &mut dummy_size)?;
2319        align = dummy_size;
2320        if next_opt == KOption::Char || align == 0 {
2321            return Err(LuaError::arg_error(1, "invalid next option for option 'X'"));
2322        }
2323    }
2324
2325    if align <= 1 || opt == KOption::Char {
2326        *ntoalign = 0;
2327    } else {
2328        if align > h.max_align {
2329            align = h.max_align;
2330        }
2331        if (align & (align - 1)) != 0 {
2332            return Err(LuaError::arg_error(1, "format asks for alignment not power of 2"));
2333        }
2334        *ntoalign = (align - (total_size & (align - 1))) & (align - 1);
2335    }
2336    Ok(opt)
2337}
2338
2339/// Pack integer `n` with `size` bytes into `buf` with given endianness.
2340///
2341/// C: `static void packint(luaL_Buffer *b, lua_Unsigned n, int islittle, int size, int neg)`
2342fn packint(buf: &mut Vec<u8>, mut n: u64, is_little: bool, size: usize, neg: bool) {
2343    let start = buf.len();
2344    buf.resize(start + size, 0);
2345    let slice = &mut buf[start..start + size];
2346    // Write LSB first (little-endian), then swap if big-endian
2347    for i in 0..size {
2348        slice[if is_little { i } else { size - 1 - i }] = (n & MC as u64) as u8;
2349        n >>= NB;
2350    }
2351    // Sign extension for negative numbers larger than lua_Integer
2352    if neg && size > SZINT {
2353        for i in SZINT..size {
2354            slice[if is_little { i } else { size - 1 - i }] = MC;
2355        }
2356    }
2357}
2358
2359/// Copy bytes with endianness correction.
2360///
2361/// C: `static void copywithendian(char *dest, const char *src, int size, int islittle)`
2362fn copywithendian(dest: &mut [u8], src: &[u8], is_little: bool) {
2363    debug_assert_eq!(dest.len(), src.len());
2364    if is_little == cfg!(target_endian = "little") {
2365        dest.copy_from_slice(src);
2366    } else {
2367        for (d, s) in dest.iter_mut().zip(src.iter().rev()) {
2368            *d = *s;
2369        }
2370    }
2371}
2372
2373/// Unpack a (possibly signed) integer from `data[0..size]`.
2374///
2375/// C: `static lua_Integer unpackint(lua_State *L, const char *str, int islittle, int size, int issigned)`
2376fn unpackint(state: &LuaState, data: &[u8], is_little: bool, size: usize, is_signed: bool) -> Result<i64, LuaError> {
2377    let limit = size.min(SZINT);
2378    let mut res: u64 = 0;
2379    for i in (0..limit).rev() {
2380        res <<= NB;
2381        let byte_idx = if is_little { i } else { size - 1 - i };
2382        res |= data[byte_idx] as u64;
2383    }
2384
2385    if size < SZINT {
2386        if is_signed {
2387            let mask: u64 = 1u64 << (size * NB as usize - 1);
2388            res = (res ^ mask).wrapping_sub(mask);
2389        }
2390    } else if size > SZINT {
2391        let mask = if !is_signed || (res as i64) >= 0 { 0u8 } else { MC };
2392        for i in limit..size {
2393            let byte_idx = if is_little { i } else { size - 1 - i };
2394            if data[byte_idx] != mask {
2395                return Err(LuaError::runtime(format_args!(
2396                    "{}-byte integer does not fit into Lua Integer", size
2397                )));
2398            }
2399        }
2400    }
2401    Ok(res as i64)
2402}
2403
2404/// `string.pack(fmt, ...)` — pack values into a binary string.
2405///
2406/// C: `static int str_pack(lua_State *L)`
2407pub fn str_pack(state: &mut LuaState) -> Result<usize, LuaError> {
2408    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2409    let fmt = &fmt_bytes[..];
2410    let mut h = Header::new();
2411    let mut arg = 1i32;
2412    let mut total_size = 0usize;
2413    let mut buf: Vec<u8> = Vec::new();
2414    let mut pos = 0usize;
2415
2416    while pos < fmt.len() {
2417        let mut size = 0usize;
2418        let mut ntoalign = 0usize;
2419        let opt = getdetails(&mut h, total_size, fmt, &mut pos, &mut size, &mut ntoalign)?;
2420        total_size += ntoalign + size;
2421        for _ in 0..ntoalign {
2422            buf.push(PACK_PAD_BYTE);
2423        }
2424        arg += 1;
2425
2426        match opt {
2427            KOption::Int => {
2428                let n = state.check_arg_integer(arg)?;
2429                if size < SZINT {
2430                    let lim: i64 = 1i64 << (size * NB as usize - 1);
2431                    if !(-lim <= n && n < lim) {
2432                        return Err(LuaError::arg_error(arg, "integer overflow"));
2433                    }
2434                }
2435                packint(&mut buf, n as u64, h.is_little, size, n < 0);
2436            }
2437            KOption::Uint => {
2438                let n = state.check_arg_integer(arg)?;
2439                if size < SZINT {
2440                    let lim: u64 = 1u64 << (size * NB as usize);
2441                    if (n as u64) >= lim {
2442                        return Err(LuaError::arg_error(arg, "unsigned overflow"));
2443                    }
2444                }
2445                packint(&mut buf, n as u64, h.is_little, size, false);
2446            }
2447            KOption::Float => {
2448                let f = state.check_arg_number(arg)? as f32;
2449                let start = buf.len();
2450                buf.resize(start + 4, 0);
2451                copywithendian(&mut buf[start..start + 4], &f.to_bits().to_ne_bytes(), h.is_little);
2452            }
2453            KOption::Number => {
2454                let f = state.check_arg_number(arg)?;
2455                let start = buf.len();
2456                buf.resize(start + 8, 0);
2457                copywithendian(&mut buf[start..start + 8], &f.to_bits().to_ne_bytes(), h.is_little);
2458            }
2459            KOption::Double => {
2460                let f = state.check_arg_number(arg)? as f64;
2461                let start = buf.len();
2462                buf.resize(start + 8, 0);
2463                copywithendian(&mut buf[start..start + 8], &f.to_bits().to_ne_bytes(), h.is_little);
2464            }
2465            KOption::Char => {
2466                let s = state.check_arg_string(arg)?.to_vec();
2467                if s.len() > size {
2468                    return Err(LuaError::arg_error(arg, "string longer than given size"));
2469                }
2470                buf.extend_from_slice(&s);
2471                let pad = size - s.len();
2472                for _ in 0..pad {
2473                    buf.push(PACK_PAD_BYTE);
2474                }
2475            }
2476            KOption::Kstring => {
2477                let s = state.check_arg_string(arg)?.to_vec();
2478                let len = s.len();
2479                if size < SZINT && len >= (1usize << (size * 8)) {
2480                    return Err(LuaError::arg_error(arg, "string length does not fit in given size"));
2481                }
2482                packint(&mut buf, len as u64, h.is_little, size, false);
2483                buf.extend_from_slice(&s);
2484                total_size += len;
2485            }
2486            KOption::Zstr => {
2487                let s = state.check_arg_string(arg)?.to_vec();
2488                if s.contains(&0) {
2489                    return Err(LuaError::arg_error(arg, "string contains zeros"));
2490                }
2491                buf.extend_from_slice(&s);
2492                buf.push(0);
2493                total_size += s.len() + 1;
2494            }
2495            KOption::Padding => {
2496                buf.push(PACK_PAD_BYTE);
2497                arg -= 1; // undo increment
2498            }
2499            KOption::Paddalign | KOption::Nop => {
2500                arg -= 1; // undo increment
2501            }
2502        }
2503    }
2504
2505    state.push_bytes(&buf)?;
2506    Ok(1)
2507}
2508
2509/// `string.packsize(fmt)` — return the byte-size the format would produce.
2510///
2511/// C: `static int str_packsize(lua_State *L)`
2512pub fn str_packsize(state: &mut LuaState) -> Result<usize, LuaError> {
2513    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2514    let fmt = &fmt_bytes[..];
2515    let mut h = Header::new();
2516    let mut total_size = 0usize;
2517    let mut pos = 0usize;
2518
2519    while pos < fmt.len() {
2520        let mut size = 0usize;
2521        let mut ntoalign = 0usize;
2522        let opt = getdetails(&mut h, total_size, fmt, &mut pos, &mut size, &mut ntoalign)?;
2523        if opt == KOption::Kstring || opt == KOption::Zstr {
2524            return Err(LuaError::arg_error(1, "variable-length format"));
2525        }
2526        let space = ntoalign + size;
2527        if total_size > PACK_MAXSIZE - space {
2528            return Err(LuaError::arg_error(1, "format result too large"));
2529        }
2530        total_size += space;
2531    }
2532    state.push(LuaValue::Int(total_size as i64));
2533    Ok(1)
2534}
2535
2536/// `string.unpack(fmt, s [, pos])` — unpack binary data from string.
2537///
2538/// C: `static int str_unpack(lua_State *L)`
2539pub fn str_unpack(state: &mut LuaState) -> Result<usize, LuaError> {
2540    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2541    let data_bytes = state.check_arg_string(2)?.to_vec();
2542    let ld = data_bytes.len();
2543    let pos_raw = state.opt_arg_integer(3, 1)?;
2544    let mut pos = pos_relat_i(pos_raw, ld).saturating_sub(1);
2545
2546    if pos > ld {
2547        return Err(LuaError::arg_error(3, "initial position out of string"));
2548    }
2549
2550    let fmt = &fmt_bytes[..];
2551    let data = &data_bytes[..];
2552    let mut h = Header::new();
2553    let mut fmt_pos = 0usize;
2554    let mut n = 0usize;
2555
2556    while fmt_pos < fmt.len() {
2557        let mut size = 0usize;
2558        let mut ntoalign = 0usize;
2559        let opt = getdetails(&mut h, pos, fmt, &mut fmt_pos, &mut size, &mut ntoalign)?;
2560
2561        if ntoalign + size > ld - pos {
2562            return Err(LuaError::arg_error(2, "data string too short"));
2563        }
2564        pos += ntoalign;
2565        state.ensure_stack(2, "too many results")?;
2566        n += 1;
2567
2568        match opt {
2569            KOption::Int => {
2570                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, true)?;
2571                state.push(LuaValue::Int(v));
2572            }
2573            KOption::Uint => {
2574                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, false)?;
2575                state.push(LuaValue::Int(v));
2576            }
2577            KOption::Float => {
2578                let mut bytes = [0u8; 4];
2579                copywithendian(&mut bytes, &data[pos..pos + 4], h.is_little);
2580                let f = f32::from_bits(u32::from_ne_bytes(bytes));
2581                state.push(LuaValue::Float(f as f64));
2582            }
2583            KOption::Number => {
2584                let mut bytes = [0u8; 8];
2585                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
2586                let f = f64::from_bits(u64::from_ne_bytes(bytes));
2587                state.push(LuaValue::Float(f));
2588            }
2589            KOption::Double => {
2590                let mut bytes = [0u8; 8];
2591                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
2592                let f = f64::from_bits(u64::from_ne_bytes(bytes));
2593                state.push(LuaValue::Float(f));
2594            }
2595            KOption::Char => {
2596                state.push_bytes(&data[pos..pos + size])?;
2597            }
2598            KOption::Kstring => {
2599                let len = unpackint(state, &data[pos..pos + size], h.is_little, size, false)? as usize;
2600                if len > ld - pos - size {
2601                    return Err(LuaError::arg_error(2, "data string too short"));
2602                }
2603                state.push_bytes(&data[pos + size..pos + size + len])?;
2604                pos += len;
2605            }
2606            KOption::Zstr => {
2607                let end = data[pos..].iter().position(|&b| b == 0)
2608                    .ok_or_else(|| LuaError::arg_error(2, "unfinished string for format 'z'"))?;
2609                if pos + end >= ld {
2610                    return Err(LuaError::arg_error(2, "unfinished string for format 'z'"));
2611                }
2612                state.push_bytes(&data[pos..pos + end])?;
2613                pos += end + 1;
2614            }
2615            KOption::Paddalign | KOption::Padding | KOption::Nop => {
2616                n -= 1; // undo increment
2617            }
2618        }
2619        pos += size;
2620    }
2621
2622    state.push(LuaValue::Int((pos + 1) as i64));
2623    Ok(n + 1)
2624}
2625
2626// ────────────────────────────────────────────────────────────────────────────
2627// §9  Module registration
2628// ────────────────────────────────────────────────────────────────────────────
2629
2630/// Function table for `string` library.
2631///
2632/// C: `static const luaL_Reg strlib[]`
2633pub const STRING_LIB: &[(&[u8], lua_CFunction)] = &[
2634    (b"byte",     str_byte),
2635    (b"char",     str_char),
2636    (b"dump",     str_dump),
2637    (b"find",     str_find),
2638    (b"format",   str_format),
2639    (b"gmatch",   gmatch),
2640    (b"gsub",     str_gsub),
2641    (b"len",      str_len),
2642    (b"lower",    str_lower),
2643    (b"match",    str_match),
2644    (b"rep",      str_rep),
2645    (b"reverse",  str_reverse),
2646    (b"sub",      str_sub),
2647    (b"upper",    str_upper),
2648    (b"pack",     str_pack),
2649    (b"packsize", str_packsize),
2650    (b"unpack",   str_unpack),
2651];
2652
2653/// Metamethods to install on the string metatable.
2654///
2655/// C: `static const luaL_Reg stringmetamethods[]`
2656pub const STRING_META_METHODS: &[(&[u8], lua_CFunction)] = &[
2657    (b"__add",  arith_add),
2658    (b"__sub",  arith_sub),
2659    (b"__mul",  arith_mul),
2660    (b"__mod",  arith_mod),
2661    (b"__pow",  arith_pow),
2662    (b"__div",  arith_div),
2663    (b"__idiv", arith_idiv),
2664    (b"__unm",  arith_unm),
2665];
2666
2667/// Create the string metatable and set it as the metatable for all strings.
2668///
2669/// C: `static void createmetatable(lua_State *L)`
2670pub fn createmetatable(state: &mut LuaState) -> Result<(), LuaError> {
2671    // C: luaL_newlibtable(L, stringmetamethods);
2672    state.new_lib_table(STRING_META_METHODS)?;
2673    // C: luaL_setfuncs(L, stringmetamethods, 0);
2674    state.set_funcs(STRING_META_METHODS, 0)?;
2675    // C: lua_pushliteral(L, ""); lua_pushvalue(L, -2); lua_setmetatable(L, -2);
2676    state.push_string(b"")?;
2677    let mt_idx = state.top_idx() - 2;
2678    let mt = state.get_at(mt_idx);
2679    state.push(mt);
2680    state.set_metatable(-2)?;
2681    // C: lua_pop(L, 1);
2682    state.pop_n(1);
2683    // C: lua_pushvalue(L, -2); lua_setfield(L, -2, "__index");
2684    let strlib_idx = state.top_idx() - 2;
2685    let strlib = state.get_at(strlib_idx);
2686    state.push(strlib);
2687    state.set_field(-2, b"__index")?;
2688    // C: lua_pop(L, 1);
2689    state.pop_n(1);
2690    Ok(())
2691}
2692
2693/// `luaopen_string` — open the string library.
2694///
2695/// C: `LUAMOD_API int luaopen_string(lua_State *L)`
2696pub fn luaopen_string(state: &mut LuaState) -> Result<usize, LuaError> {
2697    // C: luaL_newlib(L, strlib);
2698    state.new_lib(STRING_LIB)?;
2699    createmetatable(state)?;
2700    Ok(1)
2701}
2702
2703// ────────────────────────────────────────────────────────────────────────────
2704// PORT STATUS
2705//   source:        src/lstrlib.c  (1875 lines, 46 functions)
2706//   target_crate:  lua-stdlib
2707//   confidence:    medium
2708//   todos:         13
2709//   port_notes:    6
2710//   unsafe_blocks: 0
2711//   notes:         Pattern engine uses index-based MatchState (not raw ptrs).
2712//                  string.format delegates numeric widths/precision/flags to
2713//                  Phase B (a sprintf-compatible crate or manual impl).
2714//                  gmatch iterator state holds a 4-element Lua table in the
2715//                  closure's single upvalue (src, pat, pos, lastmatch) instead
2716//                  of the C-Lua GMatchState userdata, because Phase-A
2717//                  LuaCClosure upvalues are immutable. See gmatch_aux.
2718//                  copywithendian uses safe byte-level swapping (no transmute).
2719//                  unpackint sign-extension uses two's-complement bit tricks;
2720//                  logic review needed in Phase B.
2721//                  str_dump requires state.dump_function() which is not yet
2722//                  defined; Phase B wires up the ldump.c port.
2723//                  addquoted uses 3-digit escape for all control chars (slight
2724//                  deviation from C which uses 1-digit when safe); benign.
2725//                  str_len/str_sub/str_byte/str_reverse/str_lower/str_upper/
2726//                  str_rep/gmatch/str_find_aux borrow source bytes through
2727//                  to_lua_string (GcRef) instead of copying via
2728//                  check_arg_string, mirroring the gmatch_aux fix (685482d).
2729//                  string_ops 3.00x→2.00x, string_ops_long 2.25x→1.48x on
2730//                  best-of-5 (Apple M3 Max).
2731//                  gmatch_aux reads / writes its 4-slot state table directly
2732//                  through LuaTableRefExt::{get_int, raw_set_int} after a
2733//                  single value_at(upvalue_index(1)) resolution, replacing
2734//                  six raw_geti / raw_seti + four to_lua_string / to_integer_x
2735//                  calls that each re-resolved the stack index via
2736//                  index_to_value. Drops string_ops_long 1.58x→1.38x
2737//                  (below the 1.5x parity threshold) and index_to_value share
2738//                  9.4%→2.0% on Apple M3 Max best-of-5.
2739// ────────────────────────────────────────────────────────────────────────────