Skip to main content

lua_stdlib/
string_lib.rs

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