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