Skip to main content

lua_stdlib/
string_lib.rs

1//! Standard library for string operations and pattern-matching — `string.*`.
2//!
3//! C source: `reference/lua-5.4.7/src/lstrlib.c`.
4//!
5//! The recursive pattern matcher (§2) is the hot, CPI-load-bearing core: its
6//! `goto`-derived `'outer: loop` and per-char dispatch are pinned by the
7//! behavioral net and must not be refactored (see the PORT STATUS trailer and
8//! `GRADUATED.md` "string"). Version seams live in two single-source helpers:
9//! [`matcher_bounds_depth`] (the 5.2+ "pattern too complex" guard, absent on
10//! 5.1) and [`matcher_dedups_empty_match`] (the 5.3.3 empty-match rule).
11//!
12//! Sections:
13//!   1. Basic string operations (byte, char, find, format, gmatch, gsub, len,
14//!      lower, match, rep, reverse, sub, upper)
15//!   2. Pattern-matching engine (MatchState + recursive matcher)
16//!   3. String format (`string.format`)
17//!   4. Pack / unpack (`string.pack`, `string.packsize`, `string.unpack`)
18//!   5. Module registration (`luaopen_string`)
19
20use std::any::Any;
21use std::cell::RefCell;
22use std::rc::Rc;
23
24use crate::state_stub::{lua_CFunction, upvalue_index, LuaState, LuaStateStubExt as _};
25use lua_types::arith::ArithOp;
26use lua_types::error::LuaError;
27use lua_types::value::LuaValue;
28use lua_types::LuaType;
29
30// ────────────────────────────────────────────────────────────────────────────
31// Constants
32// ────────────────────────────────────────────────────────────────────────────
33
34const LUA_MAX_CAPTURES: usize = 32;
35
36const MAX_CC_CALLS: i32 = 200;
37
38/// The initial `matchdepth` used on Lua 5.1, whose matcher has no recursion
39/// guard. Set high enough that the explicit "pattern too complex" bound never
40/// fires (the native stack overflows first, as it does in the 5.1 reference),
41/// while still leaving headroom for the per-call decrement to stay non-negative.
42const NO_DEPTH_LIMIT: i32 = i32::MAX;
43
44const L_ESC: u8 = b'%';
45
46const SPECIALS: &[u8] = b"^$*+?.([%-";
47
48const CAP_UNFINISHED: isize = -1;
49
50const CAP_POSITION: isize = -2;
51
52const MAX_INT_SIZE: usize = 16;
53
54/// The largest packed size accepted by `string.pack`. On platforms where
55/// `size_t` is at least as wide as `int` (all our targets) this collapses to
56/// `INT_MAX`, so packed sizes round-trip through a Lua integer without ambiguity.
57const PACK_MAXSIZE: usize = i32::MAX as usize;
58
59const NB: u32 = 8;
60
61const MC: u8 = 0xFF;
62
63const SZINT: usize = 8; // sizeof(i64) == 8
64
65const PACK_PAD_BYTE: u8 = 0x00;
66
67// ────────────────────────────────────────────────────────────────────────────
68// Pattern-matching types
69// ────────────────────────────────────────────────────────────────────────────
70
71/// One capture record inside MatchState.
72///
73/// In Rust, `init` is an index into `MatchState::src`; `len` is either a
74/// non-negative actual length, `CAP_UNFINISHED`, or `CAP_POSITION`.
75#[derive(Copy, Clone)]
76struct Capture {
77    /// Index into the source slice where this capture started.
78    init: usize,
79    /// CAP_UNFINISHED, CAP_POSITION, or non-negative byte count.
80    len: isize,
81}
82
83impl Default for Capture {
84    fn default() -> Self {
85        Capture {
86            init: 0,
87            len: CAP_UNFINISHED,
88        }
89    }
90}
91
92/// State threaded through the recursive pattern-matcher.
93///
94/// Raw C pointers replaced by indices into `src` / `pat` slices.
95struct MatchState<'a> {
96    /// Source string being searched.
97    src: &'a [u8],
98    /// Pattern string.
99    pat: &'a [u8],
100    /// Recursion depth counter; decremented on entry, incremented on return.
101    /// Initialized to `MAX_CC_CALLS` on 5.2+ (the "pattern too complex" guard)
102    /// or `NO_DEPTH_LIMIT` on 5.1, whose `lstrlib.c` `match()` has no depth
103    /// counter at all — there a too-deep pattern simply matches (only a
104    /// pathologically deep one overflows the native stack, exactly as the 5.1
105    /// reference does). The field is `i32` and the struct layout is identical to
106    /// the single-version baseline; only the initial value is version-selected.
107    matchdepth: i32,
108    /// Number of capture records currently in use.
109    level: u8,
110    /// Capture records indexed `0..level`.
111    captures: [Capture; LUA_MAX_CAPTURES],
112    /// Total `match_pat` invocations across the whole operation. Used to bound
113    /// catastrophic backtracking under a sandbox; charged against the
114    /// instruction budget by the caller.
115    steps: u64,
116    /// Maximum `steps` before the matcher stops. `0` means unlimited (no active
117    /// instruction budget), preserving non-sandboxed behavior exactly.
118    step_limit: u64,
119    /// Set when `step_limit` is reached; the matcher then unwinds to the caller,
120    /// which charges the budget and raises the uncatchable sandbox abort.
121    aborted: bool,
122}
123
124impl<'a> MatchState<'a> {
125    /// Build a matcher state. `bound_depth` is `true` on 5.2+ (apply the
126    /// `MAX_CC_CALLS` "pattern too complex" guard) and `false` on 5.1 (no guard
127    /// — 5.1's `match()` has no `matchdepth` field). `#[inline]` so a caller
128    /// passing a constant `bound_depth` folds the `matchdepth` select away.
129    #[inline]
130    fn new(src: &'a [u8], pat: &'a [u8], step_limit: u64, bound_depth: bool) -> Self {
131        let matchdepth = if bound_depth {
132            MAX_CC_CALLS
133        } else {
134            NO_DEPTH_LIMIT
135        };
136        MatchState {
137            src,
138            pat,
139            matchdepth,
140            level: 0,
141            captures: [Capture::default(); LUA_MAX_CAPTURES],
142            steps: 0,
143            step_limit,
144            aborted: false,
145        }
146    }
147
148    fn reset_level(&mut self) {
149        self.level = 0;
150        debug_assert!(self.matchdepth == MAX_CC_CALLS || self.matchdepth == NO_DEPTH_LIMIT);
151    }
152}
153
154struct GMatchIterState {
155    /// Current source position as a zero-based byte index.
156    pos: usize,
157    /// End of the last match, used to avoid zero-length infinite loops.
158    last_match: Option<usize>,
159    /// The 5.2+ `MAX_CC_CALLS` "pattern too complex" guard, resolved ONCE at
160    /// iterator creation so the per-match step never re-reads `state.global()`
161    /// (a `RefCell` borrow). The empty-match-dedup seam is bound separately, by
162    /// the choice of [`gmatch_aux`] vs [`gmatch_aux_legacy`].
163    bound_depth: bool,
164}
165
166// ────────────────────────────────────────────────────────────────────────────
167// Pack/unpack types
168// ────────────────────────────────────────────────────────────────────────────
169
170/// Pack/unpack format option.
171///
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173enum KOption {
174    Int,       // signed integers
175    Uint,      // unsigned integers
176    Float,     // single-precision float (C float)
177    Number,    // Lua native float (lua_Number = f64)
178    Double,    // double-precision float (C double)
179    Char,      // fixed-length string
180    Kstring,   // string with length prefix
181    Zstr,      // zero-terminated string
182    Padding,   // padding byte (x)
183    Paddalign, // padding to alignment (X)
184    Nop,       // no-op (space, <, >, =, !)
185}
186
187/// Header state for pack/unpack format parsing.
188///
189struct Header {
190    is_little: bool,
191    max_align: usize,
192    /// 5.5 widened `c`/`s`-size parsing from `int` (5.3/5.4) to `size_t`, so
193    /// `c<huge>` numerals that overflowed `int` (and tripped "invalid format
194    /// option '<digit>'") are now accepted up to `LUA_MAXINTEGER`.
195    wide_size: bool,
196}
197
198impl Header {
199    fn new(wide_size: bool) -> Self {
200        Header {
201            is_little: cfg!(target_endian = "little"),
202            max_align: 1,
203            wide_size,
204        }
205    }
206}
207
208// ────────────────────────────────────────────────────────────────────────────
209// §1  Basic string helpers
210// ────────────────────────────────────────────────────────────────────────────
211
212/// Translate a relative initial string position: negative means back from end;
213/// result is clipped to `[1, ∞)`.
214///
215fn pos_relat_i(pos: i64, len: usize) -> usize {
216    if pos > 0 {
217        pos as usize
218    } else if pos == 0 {
219        1
220    } else if pos < -(len as i64) {
221        1
222    } else {
223        len.wrapping_add(pos as usize).wrapping_add(1)
224    }
225}
226
227/// Translate a relative position using Lua 5.3's `posrelat` (`lstrlib.c` 5.3):
228/// non-negatives pass through, an out-of-range negative clamps to `0`, and an
229/// in-range negative counts back from the end. Unlike `posrelat_i`, `0` stays
230/// `0`; `string.unpack` then subtracts one, underflowing into the
231/// "initial position out of string" guard exactly as the 5.3 reference does.
232///
233fn posrelat_53(pos: i64, len: usize) -> usize {
234    if pos >= 0 {
235        pos as usize
236    } else if (pos as i128).unsigned_abs() > len as u128 {
237        0
238    } else {
239        (len as i64 + pos + 1) as usize
240    }
241}
242
243/// Get an optional ending string position from argument `arg`, default `def`.
244/// Negative means back from end; clipped to `[0, len]`.
245///
246fn get_end_pos(pos: i64, len: usize) -> usize {
247    if pos > len as i64 {
248        len
249    } else if pos >= 0 {
250        pos as usize
251    } else if pos < -(len as i64) {
252        0
253    } else {
254        len.wrapping_add(pos as usize).wrapping_add(1)
255    }
256}
257
258/// Whether the matcher applies the `MAX_CC_CALLS` recursion bound (the "pattern
259/// too complex" guard). The guard was added in 5.2; 5.1's `lstrlib.c` `match()`
260/// has no `matchdepth` field, so a too-deep pattern matches there (only a
261/// pathologically deep one overflows the native stack). Single source of truth
262/// for that seam — verified against the 5.1.5 source (no `MAXCCALLS`).
263fn matcher_bounds_depth(version: lua_types::LuaVersion) -> bool {
264    version != lua_types::LuaVersion::V51
265}
266
267/// Whether `gmatch`/`gsub` suppress a redundant empty match at the end of the
268/// previous match (the `e != lastmatch` guard). Added in 5.3.3 (present in
269/// 5.3/5.4/5.5, absent in 5.1/5.2). Without it, `gsub(" *", "-")` doubles to
270/// `-a--b--c-d-` and `gmatch("%a*")` emits spurious empty captures. Single
271/// source of truth for that seam — verified against the 5.2.4 vs 5.3.6 sources.
272fn matcher_dedups_empty_match(version: lua_types::LuaVersion) -> bool {
273    matches!(
274        version,
275        lua_types::LuaVersion::V53 | lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55
276    )
277}
278
279// ────────────────────────────────────────────────────────────────────────────
280// §2  Exported string functions (registered in strlib[])
281// ────────────────────────────────────────────────────────────────────────────
282
283/// `string.len(s)` — return byte-length of `s`.
284///
285///
286/// Reads only the byte-length, never the bytes themselves, so go through
287/// `to_lua_string_len` (which never copies) rather than `check_arg_string`
288/// (which `to_vec`s the entire payload only for `.len()` to throw it away).
289pub fn str_len(state: &mut LuaState) -> Result<usize, LuaError> {
290    let l = match state.to_lua_string_len(1) {
291        Some(n) => n,
292        None => {
293            state.check_arg_string(1)?;
294            unreachable!("check_arg_string raises when arg #1 is not a string");
295        }
296    };
297    state.push(LuaValue::Int(l as i64));
298    Ok(1)
299}
300
301/// `string.sub(s, i [, j])` — return substring.
302///
303///
304/// Borrow through `to_lua_string` so the full source string is not copied just
305/// to slice a (typically small) substring out of it. The `GcRef` keeps the
306/// bytes rooted across the `check_arg_integer` / `opt_arg_integer` calls (none
307/// of which can collect the string at arg #1).
308pub fn str_sub(state: &mut LuaState) -> Result<usize, LuaError> {
309    let s_ref = match state.to_lua_string(1) {
310        Some(r) => r,
311        None => {
312            state.check_arg_string(1)?;
313            unreachable!("check_arg_string raises when arg #1 is not a string");
314        }
315    };
316    let s: &[u8] = s_ref.as_bytes();
317    let l = s.len();
318    let start = pos_relat_i(state.check_arg_integer(2)?, l);
319    let end_pos_raw = state.opt_arg_integer(3, -1)?;
320    let end = get_end_pos(end_pos_raw, l);
321    if start <= end {
322        let slice = &s[(start - 1)..end];
323        state.push_string(slice)?;
324    } else {
325        state.push_string(b"")?;
326    }
327    Ok(1)
328}
329
330/// `string.reverse(s)` — return string with bytes reversed.
331///
332///
333/// Borrow the source bytes; the previous `check_arg_string` made a full owned
334/// copy that was discarded after the single iteration.
335pub fn str_reverse(state: &mut LuaState) -> Result<usize, LuaError> {
336    let s_ref = match state.to_lua_string(1) {
337        Some(r) => r,
338        None => {
339            state.check_arg_string(1)?;
340            unreachable!("check_arg_string raises when arg #1 is not a string");
341        }
342    };
343    let s: &[u8] = s_ref.as_bytes();
344    let buf: Vec<u8> = s.iter().copied().rev().collect();
345    state.push_bytes(&buf)?;
346    Ok(1)
347}
348
349/// `string.lower(s)` — return lowercase copy.
350///
351///
352/// Borrow the source bytes; one allocation (the output `Vec`) is unavoidable,
353/// but the intermediate copy from `check_arg_string` was not.
354pub fn str_lower(state: &mut LuaState) -> Result<usize, LuaError> {
355    let s_ref = match state.to_lua_string(1) {
356        Some(r) => r,
357        None => {
358            state.check_arg_string(1)?;
359            unreachable!("check_arg_string raises when arg #1 is not a string");
360        }
361    };
362    let s: &[u8] = s_ref.as_bytes();
363    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_lowercase()).collect();
364    state.push_bytes(&buf)?;
365    Ok(1)
366}
367
368/// `string.upper(s)` — return uppercase copy.
369///
370///
371/// Borrow the source bytes; called as the `string.gsub` replacement function
372/// in `string_ops_long` ~700k times against `%w+` matches, so the intermediate
373/// copy from `check_arg_string` added up.
374pub fn str_upper(state: &mut LuaState) -> Result<usize, LuaError> {
375    let s_ref = match state.to_lua_string(1) {
376        Some(r) => r,
377        None => {
378            state.check_arg_string(1)?;
379            unreachable!("check_arg_string raises when arg #1 is not a string");
380        }
381    };
382    let s: &[u8] = s_ref.as_bytes();
383    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_uppercase()).collect();
384    state.push_bytes(&buf)?;
385    Ok(1)
386}
387
388/// `string.rep(s, n [, sep])` — return `n` copies of `s` separated by `sep`.
389///
390/// The separator argument was added in Lua 5.2; 5.1's `string.rep(s, n)` ignores
391/// any 3rd argument, so the separator is unconditionally empty on 5.1.
392///
393/// Borrow `s` through `to_lua_string`. The previous version did the
394/// `check_arg_string` copy and then a second redundant `s.to_vec()` inside the
395/// build loop — that double-copy is gone too.
396pub fn str_rep(state: &mut LuaState) -> Result<usize, LuaError> {
397    let s_ref = match state.to_lua_string(1) {
398        Some(r) => r,
399        None => {
400            state.check_arg_string(1)?;
401            unreachable!("check_arg_string raises when arg #1 is not a string");
402        }
403    };
404    let s: &[u8] = s_ref.as_bytes();
405    let l = s.len();
406    let n = state.check_arg_integer(2)?;
407    let sep_owned = if state.global().lua_version == lua_types::LuaVersion::V51 {
408        Vec::new()
409    } else {
410        state.opt_arg_string(3, b"")?
411    };
412    let sep: &[u8] = &sep_owned;
413    let lsep = sep.len();
414
415    if n <= 0 {
416        state.push_string(b"")?;
417    } else {
418        const MAXSIZE: usize = i32::MAX as usize;
419        let per = l
420            .checked_add(lsep)
421            .ok_or_else(|| LuaError::runtime(format_args!("resulting string too large")))?;
422        if per > MAXSIZE / (n as usize) {
423            return Err(LuaError::runtime(format_args!(
424                "resulting string too large"
425            )));
426        }
427        let total = per * (n as usize) - lsep;
428
429        if let Some(err) = state.sandbox_reserve(total) {
430            return Err(err);
431        }
432
433        let mut buf: Vec<u8> = Vec::with_capacity(total);
434        for i in 0..(n as usize) {
435            buf.extend_from_slice(s);
436            if i < (n as usize - 1) && lsep > 0 {
437                buf.extend_from_slice(sep);
438            }
439        }
440        state.push_bytes(&buf)?;
441    }
442    Ok(1)
443}
444
445/// `string.byte(s [, i [, j]])` — return numeric codes of characters.
446///
447///
448/// Borrow the source bytes through `to_lua_string` (returns a `GcRef<LuaString>`)
449/// instead of `check_arg_string` (which copies the entire string into a fresh
450/// `Vec<u8>`). On the `string_ops_long` workload `string.byte` is called 700k
451/// times against the same ~14 KB string, so the previous copy was on the order
452/// of 10 GB of memcpy. The `GcRef` keeps the bytes rooted while the borrow lives.
453pub fn str_byte(state: &mut LuaState) -> Result<usize, LuaError> {
454    let s_ref = match state.to_lua_string(1) {
455        Some(r) => r,
456        None => {
457            state.check_arg_string(1)?;
458            unreachable!("check_arg_string raises when arg #1 is not a string");
459        }
460    };
461    let s: &[u8] = s_ref.as_bytes();
462    let l = s.len();
463    let pi = state.opt_arg_integer(2, 1)?;
464    let posi = pos_relat_i(pi, l);
465    let pose_raw = state.opt_arg_integer(3, pi)?;
466    let pose = get_end_pos(pose_raw, l);
467
468    if posi > pose {
469        return Ok(0);
470    }
471    let count = pose.saturating_sub(posi - 1) + 1;
472    if count > i32::MAX as usize {
473        return Err(LuaError::runtime(format_args!("string slice too long")));
474    }
475    let n = (pose - posi + 1) as usize;
476    state.ensure_stack(n as i32, "string slice too long")?;
477
478    for i in 0..n {
479        state.push(LuaValue::Int(s[posi - 1 + i] as i64));
480    }
481    Ok(n)
482}
483
484/// `string.char(...)` — return string built from character codes.
485///
486pub fn str_char(state: &mut LuaState) -> Result<usize, LuaError> {
487    let n = state.get_top();
488    let mut buf = Vec::with_capacity(n as usize);
489    for i in 1..=n {
490        let c = state.check_arg_integer(i)? as u64;
491        if c > u8::MAX as u64 {
492            return Err(lua_vm::debug::arg_error_impl(
493                state,
494                i,
495                b"value out of range",
496            ));
497        }
498        buf.push(c as u8);
499    }
500    state.push_bytes(&buf)?;
501    Ok(1)
502}
503
504/// `string.dump(function [, strip])` — serialize a function as binary chunk.
505///
506/// Uses `lua_dump` internally; the writer callback builds a buffer.
507pub fn str_dump(state: &mut LuaState) -> Result<usize, LuaError> {
508    state.check_arg_type(1, LuaType::Function)?;
509    let strip = state.arg_to_bool(2);
510    // Use the frame-relative `lua_vm::api::set_top`, not `state.set_top`: the
511    // inherent method takes an absolute StackIdx and would wipe the call frame.
512    lua_vm::api::set_top(state, 1)?;
513    let bytes = state
514        .dump_function(strip)
515        .map_err(|_| LuaError::runtime(format_args!("unable to dump given function")))?;
516    state.push_bytes(&bytes)?;
517    Ok(1)
518}
519
520// ────────────────────────────────────────────────────────────────────────────
521// §3  String metamethods (arithmetic coercion)
522// ────────────────────────────────────────────────────────────────────────────
523
524/// Try to coerce the argument at `arg` to a number, pushing it on the stack.
525/// Returns true on success.
526///
527fn tonum(state: &mut LuaState, arg: i32) -> Result<bool, LuaError> {
528    if state.type_at(arg) == LuaType::Number {
529        state.push_value_at(arg)?;
530        Ok(true)
531    } else {
532        if let Some(s) = state.to_lua_string_bytes(arg) {
533            let len = s.len();
534            let pushed = state.string_to_number_push(&s)?;
535            let ok = pushed == len + 1;
536            // Lua 5.1–5.3: a string coerced in an arithmetic operation always
537            // yields a float (`('16') + 0` is a float in 5.3, an integer in
538            // 5.4). This metamethod path is arithmetic-only, so the promotion
539            // never touches bitwise ops. Verified vs the 5.3.6/5.4.7 oracle.
540            if ok
541                && matches!(
542                    state.global().lua_version,
543                    lua_types::LuaVersion::V51
544                        | lua_types::LuaVersion::V52
545                        | lua_types::LuaVersion::V53
546                )
547            {
548                if let Some(f) = lua_vm::api::to_number_x(state, -1) {
549                    state.pop();
550                    state.push(LuaValue::Float(f));
551                }
552            }
553            Ok(ok)
554        } else {
555            Ok(false)
556        }
557    }
558}
559
560/// Try to invoke the metamethod `mtname` on the two operands.
561///
562fn trymt(state: &mut LuaState, mtname: &[u8]) -> Result<(), LuaError> {
563    // Use the frame-relative `lua_vm::api::set_top`, not `state.set_top` (which
564    // takes an absolute StackIdx and would wipe the frame's arguments) — keep
565    // the first two operands for the error formatter below.
566    lua_vm::api::set_top(state, 2)?;
567    let t2_is_string = state.type_at(2) == LuaType::String;
568    // The string-or-metafield test must short-circuit: when arg2 is a string,
569    // `get_meta_field` is never called, so the stack stays `[arg1, arg2]` for
570    // the error formatter. Calling it unconditionally would push the string
571    // metatable's own metamethod and shift the operands read by
572    // `type_name_at(-2)/(-1)`.
573    if t2_is_string || !state.get_meta_field(2, mtname)? {
574        let op = &mtname[2..]; // skip "__"
575        let msg = format!(
576            "attempt to {} a '{}' with a '{}'",
577            op.escape_ascii(),
578            state.type_name_at(-2).escape_ascii(),
579            state.type_name_at(-1).escape_ascii(),
580        );
581        return crate::auxlib::lua_error(state, msg.as_bytes()).map(|_| ());
582    }
583    state.insert(-3)?;
584    state.call(2, 1)?;
585    Ok(())
586}
587
588/// Generic arithmetic helper: coerce both args and call `op`, else try metamethod.
589///
590fn arith(state: &mut LuaState, op: ArithOp, mtname: &[u8]) -> Result<usize, LuaError> {
591    if tonum(state, 1)? && tonum(state, 2)? {
592        state.arith(op)?;
593    } else {
594        trymt(state, mtname)?;
595    }
596    Ok(1)
597}
598
599pub fn arith_add(state: &mut LuaState) -> Result<usize, LuaError> {
600    arith(state, ArithOp::Add, b"__add")
601}
602pub fn arith_sub(state: &mut LuaState) -> Result<usize, LuaError> {
603    arith(state, ArithOp::Sub, b"__sub")
604}
605pub fn arith_mul(state: &mut LuaState) -> Result<usize, LuaError> {
606    arith(state, ArithOp::Mul, b"__mul")
607}
608pub fn arith_mod(state: &mut LuaState) -> Result<usize, LuaError> {
609    arith(state, ArithOp::Mod, b"__mod")
610}
611pub fn arith_pow(state: &mut LuaState) -> Result<usize, LuaError> {
612    arith(state, ArithOp::Pow, b"__pow")
613}
614pub fn arith_div(state: &mut LuaState) -> Result<usize, LuaError> {
615    arith(state, ArithOp::Div, b"__div")
616}
617pub fn arith_idiv(state: &mut LuaState) -> Result<usize, LuaError> {
618    arith(state, ArithOp::Idiv, b"__idiv")
619}
620pub fn arith_unm(state: &mut LuaState) -> Result<usize, LuaError> {
621    arith(state, ArithOp::Unm, b"__unm")
622}
623
624// ────────────────────────────────────────────────────────────────────────────
625// §4  Pattern-matching engine
626// ────────────────────────────────────────────────────────────────────────────
627
628/// Return `true` if `c` belongs to the character class `cl` (a `%x` letter).
629///
630#[inline(always)]
631fn match_class(c: u8, cl: u8) -> bool {
632    let res = match cl.to_ascii_lowercase() {
633        b'a' => c.is_ascii_alphabetic(),
634        b'c' => c.is_ascii_control(),
635        b'd' => c.is_ascii_digit(),
636        b'g' => c.is_ascii_graphic(),
637        b'l' => c.is_ascii_lowercase(),
638        b'p' => c.is_ascii_punctuation(),
639        b's' => c.is_ascii_whitespace(),
640        b'u' => c.is_ascii_uppercase(),
641        b'w' => c.is_ascii_alphanumeric(),
642        b'x' => c.is_ascii_hexdigit(),
643        b'z' => c == 0,
644        _ => return cl == c,
645    };
646    if cl.is_ascii_lowercase() {
647        res
648    } else {
649        !res
650    }
651}
652
653/// Match character `c` against a bracket class `[p .. ec-1]`.
654///
655/// `p` and `ec` are indices into `pat`.
656#[inline]
657fn matchbracketclass(pat: &[u8], c: u8, mut p: usize, ec: usize) -> bool {
658    let sig = if p + 1 < pat.len() && pat[p + 1] == b'^' {
659        p += 1; // skip '^'
660        false
661    } else {
662        true
663    };
664    p += 1; // advance past '[' or '^'
665    while p < ec {
666        if pat[p] == L_ESC {
667            p += 1;
668            if p < ec && match_class(c, pat[p]) {
669                return sig;
670            }
671        } else if p + 1 < ec && pat[p + 1] == b'-' && p + 2 < ec {
672            let lo = pat[p];
673            p += 2;
674            let hi = pat[p];
675            if lo <= c && c <= hi {
676                return sig;
677            }
678        } else if pat[p] == c {
679            return sig;
680        }
681        p += 1;
682    }
683    !sig
684}
685
686/// Return `true` if the single character at `src[s]` matches the pattern
687/// element starting at `pat[p]` with class end at `ep`.
688///
689#[inline(always)]
690fn singlematch(ms: &MatchState, s: usize, p: usize, ep: usize) -> bool {
691    if s >= ms.src.len() {
692        return false;
693    }
694    let c = ms.src[s];
695    match ms.pat[p] {
696        b'.' => true,
697        L_ESC => match_class(c, ms.pat[p + 1]),
698        b'[' => matchbracketclass(ms.pat, c, p, ep - 1),
699        pc => pc == c,
700    }
701}
702
703/// Find the end of the pattern element starting at `pat[p]`.
704/// Returns the index one past the element, or an error for malformed patterns.
705///
706#[inline(always)]
707fn classend(ms: &MatchState, p: usize) -> Result<usize, LuaError> {
708    let pat = ms.pat;
709    match pat.get(p).copied() {
710        Some(L_ESC) => {
711            if p + 1 >= pat.len() {
712                return Err(LuaError::runtime(format_args!(
713                    "malformed pattern (ends with '%')"
714                )));
715            }
716            Ok(p + 2)
717        }
718        Some(b'[') => {
719            let mut q = p + 1;
720            if q < pat.len() && pat[q] == b'^' {
721                q += 1;
722            }
723            loop {
724                if q >= pat.len() {
725                    return Err(LuaError::runtime(format_args!(
726                        "malformed pattern (missing ']')"
727                    )));
728                }
729                let ch = pat[q];
730                q += 1;
731                if ch == L_ESC && q < pat.len() {
732                    q += 1;
733                }
734                if q < pat.len() && pat[q] == b']' {
735                    return Ok(q + 1);
736                }
737            }
738        }
739        Some(_) => Ok(p + 1),
740        None => Ok(p),
741    }
742}
743
744/// Check that capture `l` (1-based char digit from pattern) is valid.
745/// Returns the 0-based capture index.
746///
747fn check_capture(ms: &MatchState, l: u8) -> Result<usize, LuaError> {
748    let signed = (l as i32) - (b'1' as i32);
749    if signed < 0 || signed >= ms.level as i32 || ms.captures[signed as usize].len == CAP_UNFINISHED
750    {
751        return Err(LuaError::runtime(format_args!(
752            "invalid capture index %{}",
753            signed + 1
754        )));
755    }
756    Ok(signed as usize)
757}
758
759/// Find the most recent unfinished capture to close.
760///
761fn capture_to_close(ms: &MatchState) -> Result<usize, LuaError> {
762    let mut level = ms.level as usize;
763    while level > 0 {
764        level -= 1;
765        if ms.captures[level].len == CAP_UNFINISHED {
766            return Ok(level);
767        }
768    }
769    Err(LuaError::runtime(format_args!("invalid pattern capture")))
770}
771
772/// Match a balanced string `%bxy` starting at `src[s]`.
773///
774/// Returns the new `s` position after the match, or `None`.
775fn matchbalance(ms: &MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
776    if p + 1 >= ms.pat.len() {
777        return Err(LuaError::runtime(format_args!(
778            "malformed pattern (missing arguments to '%b')"
779        )));
780    }
781    let b = ms.pat[p];
782    let e = ms.pat[p + 1];
783    if s >= ms.src.len() || ms.src[s] != b {
784        return Ok(None);
785    }
786    let mut cont = 1i32;
787    let mut s = s + 1;
788    while s < ms.src.len() {
789        if ms.src[s] == e {
790            cont -= 1;
791            if cont == 0 {
792                return Ok(Some(s + 1));
793            }
794        } else if ms.src[s] == b {
795            cont += 1;
796        }
797        s += 1;
798    }
799    Ok(None)
800}
801
802/// Greedy match: match as many as possible, then try the rest of the pattern.
803///
804fn max_expand(
805    ms: &mut MatchState,
806    s: usize,
807    p: usize,
808    ep: usize,
809) -> Result<Option<usize>, LuaError> {
810    let mut count: isize = 0;
811    while singlematch(ms, s + count as usize, p, ep) {
812        count += 1;
813    }
814    while count >= 0 {
815        let res = match_pat(ms, s + count as usize, ep + 1)?;
816        if res.is_some() {
817            return Ok(res);
818        }
819        count -= 1;
820    }
821    Ok(None)
822}
823
824/// Lazy match: try the rest of the pattern first, then expand by one.
825///
826fn min_expand(
827    ms: &mut MatchState,
828    mut s: usize,
829    p: usize,
830    ep: usize,
831) -> Result<Option<usize>, LuaError> {
832    loop {
833        let res = match_pat(ms, s, ep + 1)?;
834        if res.is_some() {
835            return Ok(res);
836        } else if singlematch(ms, s, p, ep) {
837            s += 1;
838        } else {
839            return Ok(None);
840        }
841    }
842}
843
844/// Open a new capture at `src[s]`.
845///
846fn start_capture(
847    ms: &mut MatchState,
848    s: usize,
849    p: usize,
850    what: isize,
851) -> Result<Option<usize>, LuaError> {
852    let level = ms.level as usize;
853    if level >= LUA_MAX_CAPTURES {
854        return Err(LuaError::runtime(format_args!("too many captures")));
855    }
856    ms.captures[level].init = s;
857    ms.captures[level].len = what;
858    ms.level += 1;
859    let res = match_pat(ms, s, p)?;
860    if res.is_none() {
861        ms.level -= 1; // undo capture
862    }
863    Ok(res)
864}
865
866/// Close the most recent open capture at `src[s]`.
867///
868fn end_capture(ms: &mut MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
869    let l = capture_to_close(ms)?;
870    ms.captures[l].len = (s - ms.captures[l].init) as isize;
871    let res = match_pat(ms, s, p)?;
872    if res.is_none() {
873        ms.captures[l].len = CAP_UNFINISHED; // undo
874    }
875    Ok(res)
876}
877
878/// Match a back-reference `%n` against `src[s]`.
879///
880fn match_capture(ms: &MatchState, s: usize, l: u8) -> Result<Option<usize>, LuaError> {
881    let idx = check_capture(ms, l)?;
882    let cap_len = ms.captures[idx].len as usize;
883    let cap_init = ms.captures[idx].init;
884    if ms.src.len() - s >= cap_len
885        && &ms.src[s..s + cap_len] == &ms.src[cap_init..cap_init + cap_len]
886    {
887        Ok(Some(s + cap_len))
888    } else {
889        Ok(None)
890    }
891}
892
893/// Core recursive pattern matcher: returns `Ok(Some(new_s))` on match,
894/// `Ok(None)` on failure, `Err` on a malformed pattern.
895///
896/// **Load-bearing, CPI-critical — do not restructure.** This is the hot inner
897/// loop of `find`/`match`/`gmatch`/`gsub`. The `'outer: loop` is the faithful
898/// translation of C's `goto init` tail-call (a self-`continue` re-enters at the
899/// new `s`/`p` without growing the Rust stack); the per-byte `match ms.pat[p]`
900/// is the dispatch; the remaining recursion (capture open/close, the expand
901/// helpers) mirrors the C call graph. Idiomatizing this — extracting helpers
902/// (adds calls), converting the loop to recursion, or replacing the dispatch —
903/// regresses the matcher's instruction count / branch behavior. The matcher is
904/// pinned by the behavioral net (pm.lua, strings.lua, the P2c oracle gates) and
905/// guarded by the Ir/branch-sim perf arbiter; only renames and doc-comments are
906/// admissible here.
907fn match_pat(ms: &mut MatchState, mut s: usize, mut p: usize) -> Result<Option<usize>, LuaError> {
908    if ms.aborted {
909        return Ok(None);
910    }
911    ms.steps += 1;
912    if ms.step_limit != 0 && ms.steps > ms.step_limit {
913        ms.aborted = true;
914        return Ok(None);
915    }
916    ms.matchdepth -= 1;
917    if ms.matchdepth < 0 {
918        ms.matchdepth = 0;
919        return Err(LuaError::runtime(format_args!("pattern too complex")));
920    }
921
922    // Use a loop to simulate `goto init` (tail-call optimization).
923    let result = 'outer: loop {
924        if p >= ms.pat.len() {
925            // end of pattern — full match up to current s
926            break 'outer Ok(Some(s));
927        }
928
929        match ms.pat[p] {
930            b'(' => {
931                let s2 = if p + 1 < ms.pat.len() && ms.pat[p + 1] == b')' {
932                    // position capture
933                    start_capture(ms, s, p + 2, CAP_POSITION)?
934                } else {
935                    start_capture(ms, s, p + 1, CAP_UNFINISHED)?
936                };
937                break 'outer Ok(s2);
938            }
939            b')' => {
940                let s2 = end_capture(ms, s, p + 1)?;
941                break 'outer Ok(s2);
942            }
943            b'$' => {
944                if p + 1 != ms.pat.len() {
945                    // fall through to default
946                    let ep = classend(ms, p)?;
947                    let s2 = handle_class_with_suffix(ms, s, p, ep)?;
948                    break 'outer Ok(s2);
949                }
950                break 'outer Ok(if s == ms.src.len() { Some(s) } else { None });
951            }
952            L_ESC => {
953                match ms.pat.get(p + 1).copied().unwrap_or(0) {
954                    b'b' => {
955                        let s2 = matchbalance(ms, s, p + 2)?;
956                        if let Some(ns) = s2 {
957                            s = ns;
958                            p += 4;
959                            continue 'outer; // tail call: match(ms, s, p+4)
960                        }
961                        break 'outer Ok(None);
962                    }
963                    b'f' => {
964                        p += 2;
965                        if ms.pat.get(p).copied() != Some(b'[') {
966                            return Err(LuaError::runtime(format_args!(
967                                "missing '[' after '%f' in pattern"
968                            )));
969                        }
970                        let ep = classend(ms, p)?;
971                        let previous = if s == 0 { 0u8 } else { ms.src[s - 1] };
972                        let current = ms.src.get(s).copied().unwrap_or(0);
973                        if !matchbracketclass(ms.pat, previous, p, ep - 1)
974                            && matchbracketclass(ms.pat, current, p, ep - 1)
975                        {
976                            p = ep;
977                            continue 'outer; // tail call: match(ms, s, ep)
978                        }
979                        break 'outer Ok(None);
980                    }
981                    c @ b'0'..=b'9' => {
982                        let s2 = match_capture(ms, s, c)?;
983                        if let Some(ns) = s2 {
984                            s = ns;
985                            p += 2;
986                            continue 'outer; // tail call: match(ms, s, p+2)
987                        }
988                        break 'outer Ok(None);
989                    }
990                    _ => {
991                        // fall through to default class handling
992                        let ep = classend(ms, p)?;
993                        let s2 = handle_class_with_suffix(ms, s, p, ep)?;
994                        break 'outer Ok(s2);
995                    }
996                }
997            }
998            _ => {
999                // default: pattern class plus optional suffix
1000                let ep = classend(ms, p)?;
1001                let s2 = handle_class_with_suffix(ms, s, p, ep)?;
1002                break 'outer Ok(s2);
1003            }
1004        }
1005    };
1006
1007    ms.matchdepth += 1;
1008    result
1009}
1010
1011/// Handle a pattern class element with an optional repetition suffix
1012/// (`*`, `+`, `?`, `-`). Shared by both the escape-class and plain-class
1013/// branches of [`match_pat`]; `#[inline(always)]` so the matcher's hot dispatch
1014/// pays no call overhead for it.
1015#[inline(always)]
1016fn handle_class_with_suffix(
1017    ms: &mut MatchState,
1018    s: usize,
1019    p: usize,
1020    ep: usize,
1021) -> Result<Option<usize>, LuaError> {
1022    let matched_once = singlematch(ms, s, p, ep);
1023    if !matched_once {
1024        match ms.pat.get(ep).copied() {
1025            Some(b'*') | Some(b'?') | Some(b'-') => {
1026                // Accept zero occurrences: tail-call match(ms, s, ep+1)
1027                // We can't do a tail call into match_pat because we're returning
1028                // from handle_class_with_suffix, but we can call it directly.
1029                return match_pat(ms, s, ep + 1);
1030            }
1031            _ => return Ok(None),
1032        }
1033    }
1034
1035    // Matched at least once
1036    match ms.pat.get(ep).copied() {
1037        Some(b'?') => {
1038            // Optional: try matching with s+1, fall back to ep+1
1039            let res = match_pat(ms, s + 1, ep + 1)?;
1040            if res.is_some() {
1041                Ok(res)
1042            } else {
1043                match_pat(ms, s, ep + 1)
1044            }
1045        }
1046        Some(b'+') => {
1047            // 1 or more: greedy from s+1
1048            max_expand(ms, s + 1, p, ep)
1049        }
1050        Some(b'*') => {
1051            // 0 or more: greedy from s
1052            max_expand(ms, s, p, ep)
1053        }
1054        Some(b'-') => {
1055            // 0 or more: lazy from s
1056            min_expand(ms, s, p, ep)
1057        }
1058        _ => {
1059            // No suffix: match one, advance both s and p
1060            match_pat(ms, s + 1, ep)
1061        }
1062    }
1063}
1064
1065// ────────────────────────────────────────────────────────────────────────────
1066// §5  Pattern-matching public API helpers
1067// ────────────────────────────────────────────────────────────────────────────
1068
1069/// Find `needle` in `haystack` using a plain memmem-style search.
1070///
1071/// Returns the byte-offset of the first occurrence, or `None`.
1072fn lmemfind(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1073    if needle.is_empty() {
1074        return Some(0);
1075    }
1076    if needle.len() > haystack.len() {
1077        return None;
1078    }
1079    let first = needle[0];
1080    let rest = &needle[1..];
1081    let limit = haystack.len() - rest.len();
1082    let mut s = 0;
1083    while s <= limit {
1084        if let Some(pos) = haystack[s..].iter().position(|&b| b == first) {
1085            let pos = s + pos;
1086            if pos + 1 + rest.len() <= haystack.len()
1087                && &haystack[pos + 1..pos + 1 + rest.len()] == rest
1088            {
1089                return Some(pos);
1090            }
1091            s = pos + 1;
1092        } else {
1093            break;
1094        }
1095    }
1096    None
1097}
1098
1099fn required_start_byte(pat: &[u8]) -> Option<u8> {
1100    let (byte, ep) = match pat.first().copied()? {
1101        L_ESC => {
1102            let escaped = *pat.get(1)?;
1103            if escaped.is_ascii_alphanumeric() {
1104                return None;
1105            }
1106            (escaped, 2)
1107        }
1108        c if !SPECIALS.contains(&c) => (c, 1),
1109        _ => return None,
1110    };
1111    match pat.get(ep).copied() {
1112        Some(b'*') | Some(b'?') | Some(b'-') => None,
1113        _ => Some(byte),
1114    }
1115}
1116
1117fn next_start_with_byte(src: &[u8], pos: usize, byte: u8) -> Option<usize> {
1118    src.get(pos..)?
1119        .iter()
1120        .position(|&c| c == byte)
1121        .map(|offset| pos + offset)
1122}
1123
1124/// Check whether the pattern `pat` has no special characters (for plain search).
1125///
1126fn nospecials(pat: &[u8]) -> bool {
1127    !pat.iter().any(|b| SPECIALS.contains(b))
1128}
1129
1130/// Information about one capture result.
1131enum CaptureInfo<'a> {
1132    /// A position capture; value is 1-based index.
1133    Position(i64),
1134    /// A string capture (slice of source).
1135    Bytes(&'a [u8]),
1136}
1137
1138/// Get information about the `i`-th capture.
1139/// If there are no captures and `i == 0`, returns the whole match `s..e`.
1140///
1141fn get_one_capture<'a>(
1142    ms: &'a MatchState,
1143    i: usize,
1144    s: usize,
1145    e: usize,
1146) -> Result<CaptureInfo<'a>, LuaError> {
1147    if i >= ms.level as usize {
1148        if i != 0 {
1149            return Err(LuaError::runtime(format_args!(
1150                "invalid capture index %{}",
1151                i + 1
1152            )));
1153        }
1154        // Return whole match
1155        return Ok(CaptureInfo::Bytes(&ms.src[s..e]));
1156    }
1157    let cap = &ms.captures[i];
1158    if cap.len == CAP_UNFINISHED {
1159        return Err(LuaError::runtime(format_args!("unfinished capture")));
1160    }
1161    if cap.len == CAP_POSITION {
1162        return Ok(CaptureInfo::Position((cap.init + 1) as i64));
1163    }
1164    let len = cap.len as usize;
1165    Ok(CaptureInfo::Bytes(&ms.src[cap.init..cap.init + len]))
1166}
1167
1168/// Push all captures onto the stack, returning the number of values pushed.
1169///
1170/// `span` mirrors upstream's `const char *s` argument: `Some((s, e))` means a
1171/// whole-match span is available (so a zero-capture pattern pushes the whole
1172/// match), while `None` mirrors a `NULL s` and pushes nothing when there are no
1173/// explicit captures. Upstream guard: `nlevels = (ms->level == 0 && s) ? 1 : ms->level`.
1174///
1175fn push_captures(
1176    state: &mut LuaState,
1177    ms: &MatchState,
1178    span: Option<(usize, usize)>,
1179) -> Result<usize, LuaError> {
1180    let nlevels = if ms.level == 0 && span.is_some() {
1181        1
1182    } else {
1183        ms.level as usize
1184    };
1185    state.ensure_stack(nlevels as i32, "too many captures")?;
1186    let (s, e) = span.unwrap_or((0, 0));
1187    for i in 0..nlevels {
1188        match get_one_capture(ms, i, s, e)? {
1189            CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1190            CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1191        }
1192    }
1193    Ok(nlevels)
1194}
1195
1196// ────────────────────────────────────────────────────────────────────────────
1197// §6  str_find / str_match / gmatch / gsub
1198// ────────────────────────────────────────────────────────────────────────────
1199
1200/// Shared implementation of `string.find` and `string.match`.
1201///
1202fn str_find_aux(state: &mut LuaState, find: bool) -> Result<usize, LuaError> {
1203    let s_ref = match state.to_lua_string(1) {
1204        Some(r) => r,
1205        None => {
1206            state.check_arg_string(1)?;
1207            unreachable!("check_arg_string raises when arg #1 is not a string");
1208        }
1209    };
1210    let p_ref = match state.to_lua_string(2) {
1211        Some(r) => r,
1212        None => {
1213            state.check_arg_string(2)?;
1214            unreachable!("check_arg_string raises when arg #2 is not a string");
1215        }
1216    };
1217    let s: &[u8] = s_ref.as_bytes();
1218    let p: &[u8] = p_ref.as_bytes();
1219    let ls = s.len();
1220    let lp = p.len();
1221    let init_raw = state.opt_arg_integer(3, 1)?;
1222    let init = pos_relat_i(init_raw, ls).saturating_sub(1);
1223
1224    if init > ls {
1225        state.push(LuaValue::Nil);
1226        return Ok(1);
1227    }
1228
1229    if find && (state.arg_to_bool(4) || nospecials(p)) {
1230        // plain search
1231        if let Some(pos) = lmemfind(&s[init..], p) {
1232            let abs = init + pos;
1233            state.push(LuaValue::Int((abs + 1) as i64));
1234            state.push(LuaValue::Int((abs + lp) as i64));
1235            return Ok(2);
1236        }
1237    } else {
1238        let step_limit = state.sandbox_match_step_limit();
1239        let bound_depth = matcher_bounds_depth(state.global().lua_version);
1240        let mut ms = MatchState::new(s, p, step_limit, bound_depth);
1241        let anchor = p.first() == Some(&b'^');
1242        let p_slice = if anchor { &p[1..] } else { p };
1243        ms.pat = p_slice;
1244        let start_byte = if anchor {
1245            None
1246        } else {
1247            required_start_byte(ms.pat)
1248        };
1249
1250        let mut s1 = init;
1251        let mut matched: Option<usize> = None;
1252        loop {
1253            if let Some(byte) = start_byte {
1254                let Some(next) = next_start_with_byte(ms.src, s1, byte) else {
1255                    break;
1256                };
1257                s1 = next;
1258            }
1259            ms.reset_level();
1260            if let Some(res) = match_pat(&mut ms, s1, 0)? {
1261                matched = Some(res);
1262                break;
1263            }
1264            if ms.aborted || s1 >= ms.src.len() || anchor {
1265                break;
1266            }
1267            s1 += 1;
1268        }
1269
1270        if let Some(err) = state.sandbox_charge(ms.steps) {
1271            return Err(err);
1272        }
1273
1274        if let Some(res) = matched {
1275            if find {
1276                state.push(LuaValue::Int((s1 + 1) as i64));
1277                state.push(LuaValue::Int(res as i64));
1278                let nc = push_captures(state, &ms, None)?;
1279                return Ok(nc + 2);
1280            } else {
1281                return push_captures(state, &ms, Some((s1, res)));
1282            }
1283        }
1284    }
1285
1286    state.push(LuaValue::Nil);
1287    Ok(1)
1288}
1289
1290/// `string.find(s, pattern [, init [, plain]])` — find pattern in `s`.
1291///
1292pub fn str_find(state: &mut LuaState) -> Result<usize, LuaError> {
1293    str_find_aux(state, true)
1294}
1295
1296/// `string.match(s, pattern [, init])` — match pattern against `s`.
1297///
1298pub fn str_match(state: &mut LuaState) -> Result<usize, LuaError> {
1299    str_find_aux(state, false)
1300}
1301
1302/// Continuation function for `string.gmatch` iterator closure.
1303///
1304///
1305/// The 5.3+ `gmatch` iterator step (the default registered by [`gmatch`]).
1306///
1307/// Reads the iterator's three closure upvalues: 1 and 2 are the traced source
1308/// and pattern strings; 3 is a userdata whose host payload ([`GMatchIterState`])
1309/// holds the mutable byte positions advanced across calls.
1310///
1311/// `DEDUP` is monomorphized — `true` for this 5.3+ entry, `false` for
1312/// [`gmatch_aux_legacy`] (5.1/5.2). Specializing on it (rather than reading a
1313/// runtime flag in this per-match-hot function) keeps the 5.3+ path's codegen
1314/// byte-identical to the pre-P2c single-version matcher; the empty-match seam
1315/// costs nothing on the common path.
1316pub fn gmatch_aux(state: &mut LuaState) -> Result<usize, LuaError> {
1317    gmatch_step::<true>(state)
1318}
1319
1320/// The 5.1/5.2 `gmatch` iterator step (no `lastmatch` empty-match de-dup; the
1321/// pre-5.3.3 advance rule). Registered by [`gmatch`] only on those versions.
1322pub fn gmatch_aux_legacy(state: &mut LuaState) -> Result<usize, LuaError> {
1323    gmatch_step::<false>(state)
1324}
1325
1326#[inline(always)]
1327fn gmatch_step<const DEDUP: bool>(state: &mut LuaState) -> Result<usize, LuaError> {
1328    let s_val = state.value_at(upvalue_index(1));
1329    let p_val = state.value_at(upvalue_index(2));
1330    let (LuaValue::Str(s_str), LuaValue::Str(p_str)) = (&s_val, &p_val) else {
1331        return Ok(0);
1332    };
1333    let iter_val = state.value_at(upvalue_index(3));
1334    let LuaValue::UserData(iter_ud) = iter_val else {
1335        return Ok(0);
1336    };
1337    let Some(host) = iter_ud.host_value() else {
1338        return Ok(0);
1339    };
1340    let Ok(iter_state) = host.downcast::<RefCell<GMatchIterState>>() else {
1341        return Ok(0);
1342    };
1343
1344    let s: &[u8] = s_str.as_bytes();
1345    let p: &[u8] = p_str.as_bytes();
1346    let (start_pos, last_match, stored_bound_depth) = {
1347        let iter = iter_state.borrow();
1348        (iter.pos, iter.last_match, iter.bound_depth)
1349    };
1350    // DEDUP=true ⟹ 5.3+ ⟹ the depth bound is always on; fold it to a constant
1351    // so the 5.3+ step's `MatchState::new` is the baseline `MAX_CC_CALLS`.
1352    let bound_depth = if DEDUP { true } else { stored_bound_depth };
1353
1354    let ls = s.len();
1355
1356    let step_limit = state.sandbox_match_step_limit();
1357    let mut ms = MatchState::new(s, p, step_limit, bound_depth);
1358    let start_byte = required_start_byte(p);
1359
1360    let mut src = start_pos;
1361    let mut hit: Option<(usize, usize)> = None;
1362    while src <= ls {
1363        if let Some(byte) = start_byte {
1364            let Some(next) = next_start_with_byte(s, src, byte) else {
1365                break;
1366            };
1367            src = next;
1368        }
1369        ms.reset_level();
1370        if let Some(e) = match_pat(&mut ms, src, 0)? {
1371            if !DEDUP || Some(e) != last_match {
1372                hit = Some((src, e));
1373                break;
1374            }
1375        }
1376        if ms.aborted {
1377            break;
1378        }
1379        src += 1;
1380    }
1381
1382    if let Some(err) = state.sandbox_charge(ms.steps) {
1383        return Err(err);
1384    }
1385
1386    if let Some((src, e)) = hit {
1387        {
1388            let mut iter = iter_state.borrow_mut();
1389            // 5.3+ stores the raw match end and de-dups via `last_match` on the
1390            // next call. Pre-5.3 has no `last_match`; it advances past an empty
1391            // match by one position (`if (e == src) newstart++`).
1392            iter.pos = if !DEDUP && e == src { e + 1 } else { e };
1393            iter.last_match = Some(e);
1394        }
1395        return push_captures(state, &ms, Some((src, e)));
1396    }
1397
1398    Ok(0)
1399}
1400
1401/// `string.gmatch(s, pattern [, init])` — return an iterator for all matches.
1402///
1403/// Builds the iterator closure consumed by [`gmatch_aux`] (5.3+) or
1404/// [`gmatch_aux_legacy`] (5.1/5.2): the source and pattern become traced
1405/// upvalues 1 and 2, and a fresh userdata holding a [`GMatchIterState`] becomes
1406/// upvalue 3 (the mutable byte positions). The empty-match-dedup seam is bound
1407/// at creation by picking the closure, so the per-match step never branches on
1408/// it (see [`gmatch_step`]).
1409pub fn gmatch(state: &mut LuaState) -> Result<usize, LuaError> {
1410    let s_ref = match state.to_lua_string(1) {
1411        Some(r) => r,
1412        None => {
1413            state.check_arg_string(1)?;
1414            unreachable!("check_arg_string raises when arg #1 is not a string");
1415        }
1416    };
1417    let ls = s_ref.len();
1418    match state.to_lua_string(2) {
1419        Some(_) => {}
1420        None => {
1421            state.check_arg_string(2)?;
1422            unreachable!("check_arg_string raises when arg #2 is not a string");
1423        }
1424    };
1425    let init_raw = state.opt_arg_integer(3, 1)?;
1426    let mut init = pos_relat_i(init_raw, ls).saturating_sub(1);
1427    if init > ls {
1428        init = ls + 1;
1429    }
1430
1431    let version = state.global().lua_version;
1432    let dedup = matcher_dedups_empty_match(version);
1433
1434    lua_vm::api::set_top(state, 2)?;
1435
1436    state.push_value_at(1)?;
1437    state.push_value_at(2)?;
1438    let iter_ud = state.new_userdata_typed(b"string.gmatch.state", 0, 0)?;
1439    let iter_state: Rc<dyn Any> = Rc::new(RefCell::new(GMatchIterState {
1440        pos: init,
1441        last_match: None,
1442        bound_depth: matcher_bounds_depth(version),
1443    }));
1444    iter_ud.set_host_value(Some(iter_state));
1445
1446    let aux = if dedup { gmatch_aux } else { gmatch_aux_legacy };
1447    state.push_c_closure(aux, 3)?;
1448    Ok(1)
1449}
1450
1451/// Add a replacement string with `%n` capture references to `buf`.
1452///
1453fn add_s(
1454    state: &mut LuaState,
1455    ms: &MatchState,
1456    buf: &mut Vec<u8>,
1457    s: usize,
1458    e: usize,
1459) -> Result<(), LuaError> {
1460    let news_bytes = state.to_lua_string_bytes(3).unwrap_or_default();
1461    let mut i = 0usize;
1462    while i < news_bytes.len() {
1463        if news_bytes[i] != L_ESC {
1464            buf.push(news_bytes[i]);
1465            i += 1;
1466        } else {
1467            i += 1; // skip ESC
1468            if i >= news_bytes.len() {
1469                break;
1470            }
1471            let c = news_bytes[i];
1472            if c == L_ESC {
1473                buf.push(L_ESC);
1474            } else if c == b'0' {
1475                buf.extend_from_slice(&ms.src[s..e]);
1476            } else if c.is_ascii_digit() {
1477                match get_one_capture(ms, (c - b'1') as usize, s, e)? {
1478                    CaptureInfo::Position(n) => {
1479                        // push position then pop into buf
1480                        let formatted = format!("{}", n).into_bytes();
1481                        buf.extend_from_slice(&formatted);
1482                    }
1483                    CaptureInfo::Bytes(b) => {
1484                        buf.extend_from_slice(b);
1485                    }
1486                }
1487            } else {
1488                return Err(LuaError::runtime(format_args!(
1489                    "invalid use of '{}' in replacement string",
1490                    L_ESC as char
1491                )));
1492            }
1493            i += 1;
1494        }
1495    }
1496    Ok(())
1497}
1498
1499/// Add the replacement value (string, table lookup, or function call) to `buf`.
1500/// Returns `true` if the original text was changed.
1501///
1502/// C `lstrlib.c` accepts any string-coercible result: `!lua_isstring(L, -1)` is
1503/// the rejection test, and `lua_isstring` is true for numbers as well as strings.
1504/// A returned number (integer or float) is therefore converted to its textual
1505/// form; only `false`/`nil` keep the original match.
1506fn add_value(
1507    state: &mut LuaState,
1508    ms: &MatchState,
1509    buf: &mut Vec<u8>,
1510    s: usize,
1511    e: usize,
1512    tr: LuaType,
1513) -> Result<bool, LuaError> {
1514    match tr {
1515        LuaType::Function => {
1516            state.push_value_at(3)?;
1517            let n = push_captures(state, ms, Some((s, e)))?;
1518            state.call(n as i32, 1)?;
1519        }
1520        LuaType::Table => {
1521            match get_one_capture(ms, 0, s, e)? {
1522                CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1523                CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1524            }
1525            state.get_table(3)?;
1526        }
1527        _ => {
1528            // LUA_TNUMBER or LUA_TSTRING: add replacement string directly
1529            add_s(state, ms, buf, s, e)?;
1530            return Ok(true);
1531        }
1532    }
1533
1534    let top_bool = state.arg_to_bool(-1);
1535    if !top_bool {
1536        state.pop_n(1);
1537        buf.extend_from_slice(&ms.src[s..e]);
1538        return Ok(false);
1539    }
1540    let ty = state.type_at(-1);
1541    if ty != LuaType::String && ty != LuaType::Number {
1542        let tname = state.type_name_at(-1).to_owned();
1543        return Err(LuaError::runtime(format_args!(
1544            "invalid replacement value (a {})",
1545            tname.escape_ascii()
1546        )));
1547    }
1548    let v = match state.to_string_coerced(-1) {
1549        Some(b) => b,
1550        None => Vec::new(),
1551    };
1552    state.pop();
1553    buf.extend_from_slice(&v);
1554    Ok(true)
1555}
1556
1557/// `string.gsub(s, pattern, repl [, n])` — global substitution.
1558///
1559pub fn str_gsub(state: &mut LuaState) -> Result<usize, LuaError> {
1560    let src_ref = match state.to_lua_string(1) {
1561        Some(r) => r,
1562        None => {
1563            state.check_arg_string(1)?;
1564            unreachable!("check_arg_string raises when arg #1 is not a string");
1565        }
1566    };
1567    let pat_ref = match state.to_lua_string(2) {
1568        Some(r) => r,
1569        None => {
1570            state.check_arg_string(2)?;
1571            unreachable!("check_arg_string raises when arg #2 is not a string");
1572        }
1573    };
1574    let src: &[u8] = src_ref.as_bytes();
1575    let pat: &[u8] = pat_ref.as_bytes();
1576    let src_len = src.len();
1577    let max_s = state.opt_arg_integer(4, (src_len + 1) as i64)?;
1578    let tr = state.type_at(3);
1579
1580    if !matches!(
1581        tr,
1582        LuaType::Number | LuaType::String | LuaType::Function | LuaType::Table
1583    ) {
1584        let v = state.arg(3);
1585        return Err(LuaError::type_arg_error(3, "string/function/table", &v));
1586    }
1587
1588    let anchor = pat.first() == Some(&b'^');
1589    let pat_slice = if anchor { &pat[1..] } else { pat };
1590
1591    let version = state.global().lua_version;
1592    let dedup = matcher_dedups_empty_match(version);
1593
1594    let step_limit = state.sandbox_match_step_limit();
1595    let mut ms = MatchState::new(src, pat_slice, step_limit, matcher_bounds_depth(version));
1596    let start_byte = if anchor {
1597        None
1598    } else {
1599        required_start_byte(ms.pat)
1600    };
1601    let mut buf: Vec<u8> = Vec::with_capacity(src_len);
1602    let mut src_pos = 0usize;
1603    let mut last_match: Option<usize> = None;
1604    let mut n: i64 = 0;
1605    let mut changed = false;
1606
1607    while n < max_s {
1608        if let Some(byte) = start_byte {
1609            let Some(next) = next_start_with_byte(ms.src, src_pos, byte) else {
1610                buf.extend_from_slice(&ms.src[src_pos..]);
1611                src_pos = ms.src.len();
1612                break;
1613            };
1614            if next > src_pos {
1615                buf.extend_from_slice(&ms.src[src_pos..next]);
1616                src_pos = next;
1617            }
1618        }
1619        ms.reset_level();
1620        let maybe_e = match_pat(&mut ms, src_pos, 0)?;
1621        if dedup {
1622            // 5.3+: `e != lastmatch` suppresses the redundant empty match left
1623            // over from the previous non-empty one; on accept, `src = e`
1624            // unconditionally and the empty re-match is deduped next iteration.
1625            if let Some(e) = maybe_e {
1626                if last_match != Some(e) {
1627                    n += 1;
1628                    let delta = add_value(state, &ms, &mut buf, src_pos, e, tr)?;
1629                    changed |= delta;
1630                    src_pos = e;
1631                    last_match = Some(e);
1632                } else if src_pos < ms.src.len() {
1633                    buf.push(ms.src[src_pos]);
1634                    src_pos += 1;
1635                } else {
1636                    break;
1637                }
1638            } else if src_pos < ms.src.len() {
1639                buf.push(ms.src[src_pos]);
1640                src_pos += 1;
1641            } else {
1642                break;
1643            }
1644        } else {
1645            // 5.1/5.2: no `lastmatch`. Every match counts; a non-empty match
1646            // skips to `e`, an empty match (or no match) copies one char. This
1647            // is what doubles `gsub(" *", "-")` to `-a--b--c-d-`. Mirrors the
1648            // 5.2.4 `lstrlib.c` `if (e) { n++; add_value } if (e && e>src) src=e;
1649            // else if (src<end) addchar(*src++); else break;` shape.
1650            if let Some(e) = maybe_e {
1651                n += 1;
1652                let delta = add_value(state, &ms, &mut buf, src_pos, e, tr)?;
1653                changed |= delta;
1654                if e > src_pos {
1655                    src_pos = e;
1656                } else if src_pos < ms.src.len() {
1657                    buf.push(ms.src[src_pos]);
1658                    src_pos += 1;
1659                } else {
1660                    break;
1661                }
1662            } else if src_pos < ms.src.len() {
1663                buf.push(ms.src[src_pos]);
1664                src_pos += 1;
1665            } else {
1666                break;
1667            }
1668        }
1669        if ms.aborted || anchor {
1670            break;
1671        }
1672    }
1673
1674    if let Some(err) = state.sandbox_charge(ms.steps) {
1675        return Err(err);
1676    }
1677
1678    if !changed {
1679        state.push_value_at(1)?;
1680    } else {
1681        buf.extend_from_slice(&ms.src[src_pos..]);
1682        state.push_bytes(&buf)?;
1683    }
1684    state.push(LuaValue::Int(n));
1685    Ok(2)
1686}
1687
1688// ────────────────────────────────────────────────────────────────────────────
1689// §7  String format (`string.format`)
1690// ────────────────────────────────────────────────────────────────────────────
1691
1692/// Add a hex-float digit to buffer and return the fractional remainder.
1693///
1694fn adddigit(buf: &mut Vec<u8>, x: f64) -> f64 {
1695    let dd = x.floor();
1696    let d = dd as i32;
1697    let c = if d < 10 {
1698        b'0' + d as u8
1699    } else {
1700        b'a' + (d - 10) as u8
1701    };
1702    buf.push(c);
1703    x - dd
1704}
1705
1706/// Convert a float to a hex-float string body (digits only, no sign, no `0x` prefix).
1707///
1708/// Returns `(frac_digits, exponent_string)` for use by `format_hex_float`.
1709///
1710fn num2straux(x: f64) -> Vec<u8> {
1711    format_hex_float(x, None)
1712}
1713
1714/// Produce a hex-float string for `x` with optional precision (digits after the point).
1715///
1716/// When `precision` is `None` the minimum number of digits needed for a round-trip
1717/// is emitted (C's default `%a` behaviour). When `precision` is `Some(p)` exactly `p`
1718/// digits follow the radix point; trailing zeros are added as needed, and excess
1719/// digits are discarded (C truncates rather than rounds, matching the C `printf`
1720/// behaviour on the tested platforms).
1721fn format_hex_float(x: f64, precision: Option<usize>) -> Vec<u8> {
1722    if x.is_nan() {
1723        return b"nan".to_vec();
1724    }
1725    if x.is_infinite() {
1726        return if x < 0.0 {
1727            b"-inf".to_vec()
1728        } else {
1729            b"inf".to_vec()
1730        };
1731    }
1732    if x == 0.0 {
1733        let sign: &[u8] = if x.is_sign_negative() { b"-" } else { b"" };
1734        return match precision {
1735            None => [sign, b"0x0p+0"].concat(),
1736            Some(0) => [sign, b"0x0p+0"].concat(),
1737            Some(p) => {
1738                let zeros = "0".repeat(p);
1739                [sign, b"0x0.", zeros.as_bytes(), b"p+0"].concat()
1740            }
1741        };
1742    }
1743
1744    let (m_raw, exp) = frexp(x);
1745    let mut buf: Vec<u8> = Vec::new();
1746    let mut m = m_raw;
1747    if m < 0.0 {
1748        buf.push(b'-');
1749        m = -m;
1750    }
1751    buf.extend_from_slice(b"0x");
1752
1753    let nbfd = 1;
1754    m = adddigit(&mut buf, m * (1 << nbfd) as f64);
1755    let e = exp - nbfd;
1756
1757    match precision {
1758        None => {
1759            if m > 0.0 {
1760                buf.push(b'.');
1761                while m > 0.0 {
1762                    m = adddigit(&mut buf, m * 16.0);
1763                }
1764            }
1765        }
1766        Some(0) => {}
1767        Some(p) => {
1768            buf.push(b'.');
1769            for _ in 0..p {
1770                if m > 0.0 {
1771                    m = adddigit(&mut buf, m * 16.0);
1772                } else {
1773                    buf.push(b'0');
1774                }
1775            }
1776        }
1777    }
1778
1779    let exp_str = format!("p{:+}", e);
1780    buf.extend_from_slice(exp_str.as_bytes());
1781    buf
1782}
1783
1784/// Decompose `x` into mantissa in `[-1.0, -0.5] ∪ [0.5, 1.0)` and exponent.
1785///
1786/// Equivalent to C's `frexp`. The sign of `x` is preserved in the returned mantissa
1787/// so that `num2straux` can emit the leading `-` correctly for negative inputs.
1788fn frexp(x: f64) -> (f64, i32) {
1789    if x == 0.0 || x.is_nan() || x.is_infinite() {
1790        return (x, 0);
1791    }
1792    let bits = x.to_bits();
1793    let sign_bit = bits & 0x8000_0000_0000_0000u64;
1794    let exp_bits = ((bits >> 52) & 0x7FF) as i32;
1795    if exp_bits == 0 {
1796        let (m, e) = frexp(x * (1u64 << 52) as f64);
1797        return (m, e - 52);
1798    }
1799    let exp = exp_bits - 1022;
1800    let mantissa_bits = sign_bit | (bits & 0x000F_FFFF_FFFF_FFFF) | 0x3FE0_0000_0000_0000;
1801    (f64::from_bits(mantissa_bits), exp)
1802}
1803
1804/// Convert float `n` to a Lua-readable literal (hex or special representation).
1805///
1806/// Lua 5.4/5.5 emit round-trippable literals for the non-finite values
1807/// (`1e9999`/`-1e9999`/`(0/0)`); Lua 5.3's `%q` predates that and falls through
1808/// to the platform `%g` text (`inf`/`-inf`/`nan`).
1809fn quotefloat(n: f64, version: lua_types::LuaVersion) -> Vec<u8> {
1810    if n == f64::INFINITY {
1811        return if version == lua_types::LuaVersion::V53 {
1812            b"inf".to_vec()
1813        } else {
1814            b"1e9999".to_vec()
1815        };
1816    } else if n == f64::NEG_INFINITY {
1817        return if version == lua_types::LuaVersion::V53 {
1818            b"-inf".to_vec()
1819        } else {
1820            b"-1e9999".to_vec()
1821        };
1822    } else if n.is_nan() {
1823        return if version == lua_types::LuaVersion::V53 {
1824            b"nan".to_vec()
1825        } else {
1826            b"(0/0)".to_vec()
1827        };
1828    }
1829    // Rust formats with a `.` decimal point regardless of locale, so unlike C's
1830    // `lua_number2strx` there is no locale separator to rewrite to `.`.
1831    num2straux(n)
1832}
1833
1834/// Add a quoted Lua string literal to `buf` using the Lua 5.2+ escaping rules:
1835/// `"`/`\`/newline are backslash-escaped, every other control byte becomes a
1836/// decimal escape (`\d`, or `\ddd` when followed by a digit), and other bytes
1837/// pass through.
1838///
1839fn addquoted(buf: &mut Vec<u8>, s: &[u8]) {
1840    buf.push(b'"');
1841    for (idx, &c) in s.iter().enumerate() {
1842        if c == b'"' || c == b'\\' || c == b'\n' {
1843            buf.push(b'\\');
1844            buf.push(c);
1845        } else if c.is_ascii_control() {
1846            let next_is_digit = s.get(idx + 1).map_or(false, |n| n.is_ascii_digit());
1847            let formatted = if next_is_digit {
1848                format!("\\{:03}", c)
1849            } else {
1850                format!("\\{}", c)
1851            };
1852            buf.extend_from_slice(formatted.as_bytes());
1853        } else {
1854            buf.push(c);
1855        }
1856    }
1857    buf.push(b'"');
1858}
1859
1860/// Add a quoted Lua string literal to `buf` using the Lua 5.1 escaping rules.
1861///
1862/// 5.1's `addquoted` differs from 5.2+: only `"`/`\`/newline are
1863/// backslash-escaped, NUL becomes the 3-digit decimal escape `\000`, carriage
1864/// return becomes the named escape `\r`, and every other byte (including other
1865/// control characters) is emitted literally.
1866fn addquoted_51(buf: &mut Vec<u8>, s: &[u8]) {
1867    buf.push(b'"');
1868    for &c in s.iter() {
1869        match c {
1870            b'"' | b'\\' | b'\n' => {
1871                buf.push(b'\\');
1872                buf.push(c);
1873            }
1874            b'\r' => buf.extend_from_slice(b"\\r"),
1875            0 => buf.extend_from_slice(b"\\000"),
1876            _ => buf.push(c),
1877        }
1878    }
1879    buf.push(b'"');
1880}
1881
1882/// Add a Lua literal representation of arg `n` to `buf`.
1883///
1884fn addliteral(state: &mut LuaState, buf: &mut Vec<u8>, arg: i32) -> Result<(), LuaError> {
1885    match state.type_at(arg) {
1886        LuaType::String => {
1887            let s = state.check_arg_string(arg)?.to_vec();
1888            addquoted(buf, &s);
1889        }
1890        LuaType::Number => {
1891            if state.is_integer(arg) {
1892                let n = state.to_integer(arg).unwrap_or(0);
1893                let formatted = if n == i64::MIN {
1894                    format!("0x{:016x}", n as u64)
1895                } else {
1896                    format!("{}", n)
1897                };
1898                buf.extend_from_slice(formatted.as_bytes());
1899            } else {
1900                let version = state.global().lua_version;
1901                let n = state.to_number(arg).unwrap_or(0.0);
1902                let hex = quotefloat(n, version);
1903                buf.extend_from_slice(&hex);
1904            }
1905        }
1906        LuaType::Nil => {
1907            buf.extend_from_slice(b"nil");
1908        }
1909        LuaType::Boolean => {
1910            buf.extend_from_slice(if state.to_boolean(arg) {
1911                b"true"
1912            } else {
1913                b"false"
1914            });
1915        }
1916        _ => {
1917            return Err(LuaError::arg_error(arg, "value has no literal form"));
1918        }
1919    }
1920    Ok(())
1921}
1922
1923/// The flag characters each `string.format` conversion class accepts, used by
1924/// [`check_conv_spec`] to reject an out-of-class flag (e.g. `+` on `%x`).
1925const FMT_FLAGS_F: &[u8] = b"-+#0 ";
1926const FMT_FLAGS_X: &[u8] = b"-#0";
1927const FMT_FLAGS_I: &[u8] = b"-+0 ";
1928const FMT_FLAGS_U: &[u8] = b"-0";
1929const FMT_FLAGS_C: &[u8] = b"-";
1930
1931/// Validate a format specifier against allowed flags and width/precision digit counts.
1932///
1933/// `form` is the full specifier slice including the leading `%` and the trailing
1934/// conversion character (e.g. `b"%100.3d"`). `flags` is the allowed-flags byte set for
1935/// this conversion type. `allow_precision` is false for conversions that forbid `.`.
1936///
1937/// Consumes flags, then up to 2 width digits, then (if allowed) `.` + up to 2
1938/// precision digits, then asserts we are at the conversion character. Returns
1939/// `Err("invalid conversion specification")` on failure.
1940fn check_conv_spec(
1941    state: &mut LuaState,
1942    form: &[u8],
1943    flags: &[u8],
1944    allow_precision: bool,
1945) -> Result<(), LuaError> {
1946    let mut i = 1usize; // skip '%'
1947    while i < form.len() && flags.contains(&form[i]) {
1948        i += 1;
1949    }
1950    if i < form.len() && form[i] == b'0' {
1951        return Err(invalid_conv_spec(state, form));
1952    }
1953    if i < form.len() && form[i].is_ascii_digit() {
1954        i += 1;
1955        if i < form.len() && form[i].is_ascii_digit() {
1956            i += 1;
1957        }
1958    }
1959    if allow_precision && i < form.len() && form[i] == b'.' {
1960        i += 1;
1961        if i < form.len() && form[i].is_ascii_digit() {
1962            i += 1;
1963            if i < form.len() && form[i].is_ascii_digit() {
1964                i += 1;
1965            }
1966        }
1967    }
1968    if i != form.len() - 1 {
1969        return Err(invalid_conv_spec(state, form));
1970    }
1971    Ok(())
1972}
1973
1974/// Build the version-appropriate "invalid conversion specification" error,
1975/// prefixed with the calling location like reference `luaL_error`.
1976///
1977/// Lua 5.3 `scanformat` raises `invalid format (width or precision too long)`
1978/// with no offending spec; Lua 5.4/5.5 `checkformat` raises
1979/// `invalid conversion specification: '<form>'`.
1980fn invalid_conv_spec(state: &mut LuaState, form: &[u8]) -> LuaError {
1981    let msg: Vec<u8> = if state.global().lua_version == lua_types::LuaVersion::V53 {
1982        b"invalid format (width or precision too long)".to_vec()
1983    } else {
1984        let mut m = b"invalid conversion specification: '".to_vec();
1985        m.extend_from_slice(form);
1986        m.push(b'\'');
1987        m
1988    };
1989    lua_vm::debug::c_api_runtime(state, msg)
1990}
1991
1992/// Parsed printf-style format specifier (flags, width, precision).
1993#[derive(Default)]
1994struct FmtSpec {
1995    left_align: bool,
1996    plus_sign: bool,
1997    space_sign: bool,
1998    alt_form: bool,
1999    zero_pad: bool,
2000    width: usize,
2001    precision: Option<usize>,
2002}
2003
2004fn parse_fmt_spec(spec: &[u8]) -> FmtSpec {
2005    let mut s = FmtSpec::default();
2006    let mut i = 0;
2007    while i < spec.len() {
2008        match spec[i] {
2009            b'-' => s.left_align = true,
2010            b'+' => s.plus_sign = true,
2011            b' ' => s.space_sign = true,
2012            b'#' => s.alt_form = true,
2013            b'0' => s.zero_pad = true,
2014            _ => break,
2015        }
2016        i += 1;
2017    }
2018    while i < spec.len() && spec[i].is_ascii_digit() {
2019        s.width = s.width * 10 + (spec[i] - b'0') as usize;
2020        i += 1;
2021    }
2022    if i < spec.len() && spec[i] == b'.' {
2023        i += 1;
2024        let mut p = 0usize;
2025        while i < spec.len() && spec[i].is_ascii_digit() {
2026            p = p * 10 + (spec[i] - b'0') as usize;
2027            i += 1;
2028        }
2029        s.precision = Some(p);
2030    }
2031    s
2032}
2033
2034fn pad_str(buf: &mut Vec<u8>, body: &[u8], spec: &FmtSpec) {
2035    let body = match spec.precision {
2036        Some(p) if body.len() > p => &body[..p],
2037        _ => body,
2038    };
2039    if body.len() >= spec.width {
2040        buf.extend_from_slice(body);
2041        return;
2042    }
2043    let pad = spec.width - body.len();
2044    if spec.left_align {
2045        buf.extend_from_slice(body);
2046        for _ in 0..pad {
2047            buf.push(b' ');
2048        }
2049    } else {
2050        for _ in 0..pad {
2051            buf.push(b' ');
2052        }
2053        buf.extend_from_slice(body);
2054    }
2055}
2056
2057fn pad_int(buf: &mut Vec<u8>, sign_prefix: &[u8], digits: &[u8], spec: &FmtSpec) {
2058    let min_digits = spec.precision.unwrap_or(0);
2059    let zeroes_for_prec = if digits.len() < min_digits {
2060        min_digits - digits.len()
2061    } else {
2062        0
2063    };
2064    let core_len = sign_prefix.len() + zeroes_for_prec + digits.len();
2065    if core_len >= spec.width {
2066        buf.extend_from_slice(sign_prefix);
2067        for _ in 0..zeroes_for_prec {
2068            buf.push(b'0');
2069        }
2070        buf.extend_from_slice(digits);
2071        return;
2072    }
2073    let pad = spec.width - core_len;
2074    let use_zero_pad = spec.zero_pad && !spec.left_align && spec.precision.is_none();
2075    if spec.left_align {
2076        buf.extend_from_slice(sign_prefix);
2077        for _ in 0..zeroes_for_prec {
2078            buf.push(b'0');
2079        }
2080        buf.extend_from_slice(digits);
2081        for _ in 0..pad {
2082            buf.push(b' ');
2083        }
2084    } else if use_zero_pad {
2085        buf.extend_from_slice(sign_prefix);
2086        for _ in 0..pad {
2087            buf.push(b'0');
2088        }
2089        for _ in 0..zeroes_for_prec {
2090            buf.push(b'0');
2091        }
2092        buf.extend_from_slice(digits);
2093    } else {
2094        for _ in 0..pad {
2095            buf.push(b' ');
2096        }
2097        buf.extend_from_slice(sign_prefix);
2098        for _ in 0..zeroes_for_prec {
2099            buf.push(b'0');
2100        }
2101        buf.extend_from_slice(digits);
2102    }
2103}
2104
2105fn signed_int_parts(n: i64, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
2106    if n == 0 && spec.precision == Some(0) {
2107        return (Vec::new(), Vec::new());
2108    }
2109    let (sign, abs_digits) = if n < 0 {
2110        (b"-".to_vec(), {
2111            let u = (n as i128).unsigned_abs();
2112            format!("{}", u).into_bytes()
2113        })
2114    } else {
2115        let s: Vec<u8> = if spec.plus_sign {
2116            b"+".to_vec()
2117        } else if spec.space_sign {
2118            b" ".to_vec()
2119        } else {
2120            Vec::new()
2121        };
2122        (s, format!("{}", n).into_bytes())
2123    };
2124    (sign, abs_digits)
2125}
2126
2127fn unsigned_int_parts(n: u64, base: u32, upper: bool, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
2128    let digits = if n == 0 && spec.precision == Some(0) {
2129        Vec::new()
2130    } else {
2131        match base {
2132            8 => format!("{:o}", n).into_bytes(),
2133            16 if upper => format!("{:X}", n).into_bytes(),
2134            16 => format!("{:x}", n).into_bytes(),
2135            _ => format!("{}", n).into_bytes(),
2136        }
2137    };
2138    let prefix: Vec<u8> = if spec.alt_form && n != 0 {
2139        match base {
2140            8 => b"0".to_vec(),
2141            16 if upper => b"0X".to_vec(),
2142            16 => b"0x".to_vec(),
2143            _ => Vec::new(),
2144        }
2145    } else {
2146        Vec::new()
2147    };
2148    (prefix, digits)
2149}
2150
2151fn format_float(n: f64, conv: u8, spec: &FmtSpec) -> Vec<u8> {
2152    let prec = spec.precision.unwrap_or(6);
2153    if n.is_nan() {
2154        return if conv.is_ascii_uppercase() {
2155            b"NAN".to_vec()
2156        } else {
2157            b"nan".to_vec()
2158        };
2159    }
2160    if n.is_infinite() {
2161        let s: &[u8] = if conv.is_ascii_uppercase() {
2162            if n < 0.0 {
2163                b"-INF"
2164            } else {
2165                b"INF"
2166            }
2167        } else if n < 0.0 {
2168            b"-inf"
2169        } else {
2170            b"inf"
2171        };
2172        return s.to_vec();
2173    }
2174    match conv {
2175        b'f' | b'F' => {
2176            let mut result = format!("{:.*}", prec, n).into_bytes();
2177            if spec.alt_form && !result.contains(&b'.') {
2178                result.push(b'.');
2179            }
2180            result
2181        }
2182        b'e' => format_exp(n, prec, false, spec.alt_form),
2183        b'E' => {
2184            let mut v = format_exp(n, prec, false, spec.alt_form);
2185            for b in v.iter_mut() {
2186                if *b == b'e' {
2187                    *b = b'E';
2188                }
2189            }
2190            v
2191        }
2192        b'g' | b'G' => {
2193            let p = if prec == 0 { 1 } else { prec };
2194            let v = format_g(n, p, spec.alt_form);
2195            if conv == b'G' {
2196                v.into_iter()
2197                    .map(|b| if b == b'e' { b'E' } else { b })
2198                    .collect()
2199            } else {
2200                v
2201            }
2202        }
2203        _ => format!("{}", n).into_bytes(),
2204    }
2205}
2206
2207/// Format `n` in `%e` style with `prec` fractional digits.
2208///
2209/// The zero branch preserves the sign of negative zero (C `printf` emits
2210/// `-0.0` as `-0e+00`); `n == 0.0` is also true for `-0.0`, so the sign bit is
2211/// the only way to distinguish them.
2212fn format_exp(n: f64, prec: usize, _upper: bool, alt: bool) -> Vec<u8> {
2213    if n == 0.0 {
2214        let neg = if n.is_sign_negative() { "-" } else { "" };
2215        let mantissa: String = if prec == 0 {
2216            if alt {
2217                "0.".to_string()
2218            } else {
2219                "0".to_string()
2220            }
2221        } else {
2222            format!("0.{}", "0".repeat(prec))
2223        };
2224        return format!("{}{}e+00", neg, mantissa).into_bytes();
2225    }
2226    let abs = n.abs();
2227    let exp = abs.log10().floor() as i32;
2228    let mantissa = n / 10f64.powi(exp);
2229    let mantissa_str = format!("{:.*}", prec, mantissa);
2230    let (mant_final, exp_final) = if let Some(dot_pos) = mantissa_str.find('.') {
2231        let int_part = &mantissa_str[..dot_pos];
2232        let abs_int = int_part.trim_start_matches('-');
2233        if abs_int.len() > 1 {
2234            let new_mant = if prec == 0 {
2235                mantissa_str[..mantissa_str.len() - 1].to_string()
2236            } else {
2237                let neg = if int_part.starts_with('-') { "-" } else { "" };
2238                let frac = &mantissa_str[dot_pos + 1..];
2239                format!("{}{}.{}{}", neg, &abs_int[..1], &abs_int[1..], frac)
2240            };
2241            (new_mant, exp + (abs_int.len() as i32 - 1))
2242        } else {
2243            (mantissa_str, exp)
2244        }
2245    } else if mantissa_str.trim_start_matches('-').len() > 1 {
2246        let neg = if mantissa_str.starts_with('-') {
2247            "-"
2248        } else {
2249            ""
2250        };
2251        let body = mantissa_str.trim_start_matches('-');
2252        let bumped = format!("{}{}.{}", neg, &body[..1], &body[1..]);
2253        (bumped, exp + (body.len() as i32 - 1))
2254    } else {
2255        (mantissa_str, exp)
2256    };
2257    let sign = if exp_final < 0 { '-' } else { '+' };
2258    let mant_out = if alt && !mant_final.contains('.') {
2259        format!("{}.", mant_final)
2260    } else {
2261        mant_final
2262    };
2263    format!("{}e{}{:02}", mant_out, sign, exp_final.abs()).into_bytes()
2264}
2265
2266/// Format `n` in `%g` style with `prec` significant digits.
2267///
2268/// The zero branch preserves the sign of negative zero (C `printf` emits `-0.0`
2269/// as `-0`); `n == 0.0` is also true for `-0.0`, so the sign bit distinguishes
2270/// them.
2271fn format_g(n: f64, prec: usize, alt: bool) -> Vec<u8> {
2272    if n == 0.0 {
2273        let neg = if n.is_sign_negative() { "-" } else { "" };
2274        return if alt {
2275            format!("{}0.{}", neg, "0".repeat(prec.saturating_sub(1))).into_bytes()
2276        } else {
2277            format!("{}0", neg).into_bytes()
2278        };
2279    }
2280    let abs = n.abs();
2281    let exp = abs.log10().floor() as i32;
2282    if exp < -4 || exp >= prec as i32 {
2283        let ep = if prec == 0 { 0 } else { prec - 1 };
2284        let mut v = format_exp(n, ep, false, alt);
2285        if !alt {
2286            v = strip_trailing_zeros_exp(&v);
2287        }
2288        v
2289    } else {
2290        let dec_places = (prec as i32 - 1 - exp).max(0) as usize;
2291        let mut v = format!("{:.*}", dec_places, n).into_bytes();
2292        if !alt {
2293            v = strip_trailing_zeros_fixed(&v);
2294        }
2295        v
2296    }
2297}
2298
2299fn strip_trailing_zeros_fixed(s: &[u8]) -> Vec<u8> {
2300    if !s.contains(&b'.') {
2301        return s.to_vec();
2302    }
2303    let mut end = s.len();
2304    while end > 0 && s[end - 1] == b'0' {
2305        end -= 1;
2306    }
2307    if end > 0 && s[end - 1] == b'.' {
2308        end -= 1;
2309    }
2310    s[..end].to_vec()
2311}
2312
2313fn strip_trailing_zeros_exp(s: &[u8]) -> Vec<u8> {
2314    let e_pos = match s.iter().position(|&b| b == b'e' || b == b'E') {
2315        Some(p) => p,
2316        None => return s.to_vec(),
2317    };
2318    let mantissa = &s[..e_pos];
2319    let exp_part = &s[e_pos..];
2320    if !mantissa.contains(&b'.') {
2321        let mut out = mantissa.to_vec();
2322        out.extend_from_slice(exp_part);
2323        return out;
2324    }
2325    let mut end = mantissa.len();
2326    while end > 0 && mantissa[end - 1] == b'0' {
2327        end -= 1;
2328    }
2329    if end > 0 && mantissa[end - 1] == b'.' {
2330        end -= 1;
2331    }
2332    let mut out = mantissa[..end].to_vec();
2333    out.extend_from_slice(exp_part);
2334    out
2335}
2336
2337/// `string.format(fmt, ...)` — C-style string formatting.
2338///
2339/// Fetch the integer argument for a `%d`/`%i`/`%u`/`%o`/`%x`/`%X` conversion.
2340///
2341/// On the dual-number versions (5.3+) an integer is required and a non-integral
2342/// number raises "number has no integer representation". On the float-only
2343/// versions (5.1/5.2) there is no integer subtype, so `string.format` truncates
2344/// the number toward zero — `("%d"):format(3.5)` is `3`, `(-3.5)` is `-3` —
2345/// matching lua5.2.4. A value outside the `lua_Integer` range (including inf/nan)
2346/// raises "number has no integer representation", which lua5.2.4 phrases as
2347/// "not a number in proper range"; the harness battery checks the truncation
2348/// cases (the out-of-range message text is a separate 5.2 error-format gap).
2349fn format_int_arg(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
2350    if state.global().lua_version.number_model() != lua_types::NumberModel::FloatOnly {
2351        return state.check_arg_integer(arg);
2352    }
2353    let n = state.check_arg_number(arg)?;
2354    let t = n.trunc();
2355    if t.is_finite() && (-9223372036854775808.0..=9223372036854775808.0).contains(&t) {
2356        Ok(t as i64)
2357    } else {
2358        Err(LuaError::arg_error(
2359            arg,
2360            "number has no integer representation",
2361        ))
2362    }
2363}
2364
2365/// Fetch the unsigned argument for a `%u`/`%o`/`%x`/`%X` conversion.
2366///
2367/// On the dual-number versions (5.3+) this is the bit pattern of the checked
2368/// integer, identical to `format_int_arg(...) as u64`.
2369///
2370/// On the float-only versions there is no integer subtype, so the C reference
2371/// casts the `double` to an unsigned word:
2372/// - Lua 5.1 casts unconditionally; the platform `fptoui` saturates, so a
2373///   negative value yields `0`, `inf`/values above `2^64` yield `u64::MAX`, and
2374///   positive fractions truncate toward zero. Rust's `as u64` saturating cast
2375///   reproduces this exactly.
2376/// - Lua 5.2 first range-checks `0 <= n <= 2^64`, raising `not a non-negative
2377///   number in proper range` otherwise, then casts the same way.
2378fn format_uint_arg(state: &mut LuaState, arg: i32) -> Result<u64, LuaError> {
2379    if state.global().lua_version.number_model() != lua_types::NumberModel::FloatOnly {
2380        return Ok(format_int_arg(state, arg)? as u64);
2381    }
2382    let n = state.check_arg_number(arg)?;
2383    if state.global().lua_version == lua_types::LuaVersion::V52
2384        && !(n >= 0.0 && n <= 18446744073709551616.0)
2385    {
2386        return Err(lua_vm::debug::arg_error_impl(
2387            state,
2388            arg,
2389            b"not a non-negative number in proper range",
2390        ));
2391    }
2392    Ok(n as u64)
2393}
2394
2395pub fn str_format(state: &mut LuaState) -> Result<usize, LuaError> {
2396    let top = state.get_top();
2397    let mut arg = 1i32;
2398    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2399    let mut buf: Vec<u8> = Vec::new();
2400    let mut i = 0usize;
2401
2402    while i < fmt_bytes.len() {
2403        let c = fmt_bytes[i];
2404        if c != L_ESC {
2405            buf.push(c);
2406            i += 1;
2407            continue;
2408        }
2409        i += 1;
2410        if i >= fmt_bytes.len() {
2411            break;
2412        }
2413        if fmt_bytes[i] == L_ESC {
2414            buf.push(L_ESC);
2415            i += 1;
2416            continue;
2417        }
2418
2419        // Parse a format specifier
2420        arg += 1;
2421        if arg > top {
2422            return Err(lua_vm::debug::arg_error_impl(state, arg, b"no value"));
2423        }
2424
2425        // Collect flags, width, precision
2426        let spec_start = i - 1; // includes the initial '%'
2427                                // Skip flags: -, +, #, 0, space
2428        while i < fmt_bytes.len() && b"-+#0 ".contains(&fmt_bytes[i]) {
2429            i += 1;
2430        }
2431        // Lua 5.3 `scanformat`: the flags buffer is `FLAGS = "-+ #0"`, so a flags
2432        // run of `sizeof(FLAGS) == 6` or more characters is "repeated flags".
2433        // 5.4/5.5 fold this into the single "(too long)" check below.
2434        if state.global().lua_version == lua_types::LuaVersion::V53 && i - (spec_start + 1) >= 6 {
2435            return Err(lua_vm::debug::c_api_runtime(
2436                state,
2437                b"invalid format (repeated flags)".to_vec(),
2438            ));
2439        }
2440        // Skip width digits
2441        if i < fmt_bytes.len() && fmt_bytes[i] != b'0' {
2442            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2443                i += 1;
2444            }
2445        }
2446        // Skip precision
2447        if i < fmt_bytes.len() && fmt_bytes[i] == b'.' {
2448            i += 1;
2449            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2450                i += 1;
2451            }
2452        }
2453
2454        if i >= fmt_bytes.len() {
2455            let form: Vec<u8> = fmt_bytes[spec_start..].to_vec();
2456            return Err(invalid_conv_spec(state, &form));
2457        }
2458
2459        let conv = fmt_bytes[i];
2460        i += 1;
2461
2462        let spec_slice = &fmt_bytes[spec_start + 1..i - 1];
2463        let form = &fmt_bytes[spec_start..i];
2464
2465        // Must check before parse_fmt_spec to avoid overflow on huge widths.
2466        if spec_slice.len() + 1 >= 22 {
2467            return Err(lua_vm::debug::c_api_runtime(
2468                state,
2469                b"invalid format (too long)".to_vec(),
2470            ));
2471        }
2472
2473        let spec = parse_fmt_spec(spec_slice);
2474
2475        match conv {
2476            b'c' => {
2477                check_conv_spec(state, form, FMT_FLAGS_C, false)?;
2478                let n = state.check_arg_integer(arg)?;
2479                let body = vec![n as u8];
2480                pad_str(&mut buf, &body, &spec);
2481            }
2482            b'd' | b'i' => {
2483                check_conv_spec(state, form, FMT_FLAGS_I, true)?;
2484                let n = format_int_arg(state, arg)?;
2485                let (sign, digits) = signed_int_parts(n, &spec);
2486                pad_int(&mut buf, &sign, &digits, &spec);
2487            }
2488            b'u' => {
2489                check_conv_spec(state, form, FMT_FLAGS_U, true)?;
2490                let n = format_uint_arg(state, arg)?;
2491                let (prefix, digits) = unsigned_int_parts(n, 10, false, &spec);
2492                pad_int(&mut buf, &prefix, &digits, &spec);
2493            }
2494            b'o' => {
2495                check_conv_spec(state, form, FMT_FLAGS_X, true)?;
2496                let n = format_uint_arg(state, arg)?;
2497                let (prefix, digits) = unsigned_int_parts(n, 8, false, &spec);
2498                pad_int(&mut buf, &prefix, &digits, &spec);
2499            }
2500            b'x' => {
2501                check_conv_spec(state, form, FMT_FLAGS_X, true)?;
2502                let n = format_uint_arg(state, arg)?;
2503                let (prefix, digits) = unsigned_int_parts(n, 16, false, &spec);
2504                pad_int(&mut buf, &prefix, &digits, &spec);
2505            }
2506            b'X' => {
2507                check_conv_spec(state, form, FMT_FLAGS_X, true)?;
2508                let n = format_uint_arg(state, arg)?;
2509                let (prefix, digits) = unsigned_int_parts(n, 16, true, &spec);
2510                pad_int(&mut buf, &prefix, &digits, &spec);
2511            }
2512            b'a' | b'A' => {
2513                check_conv_spec(state, form, FMT_FLAGS_F, true)?;
2514                let n = state.check_arg_number(arg)?;
2515                let body = format_hex_float(n, spec.precision);
2516                let body: Vec<u8> = if conv == b'A' {
2517                    body.into_iter().map(|b| b.to_ascii_uppercase()).collect()
2518                } else {
2519                    body
2520                };
2521                let (sign, digits): (Vec<u8>, Vec<u8>) =
2522                    if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2523                        (vec![body[0]], body[1..].to_vec())
2524                    } else if spec.plus_sign {
2525                        (b"+".to_vec(), body)
2526                    } else if spec.space_sign {
2527                        (b" ".to_vec(), body)
2528                    } else {
2529                        (Vec::new(), body)
2530                    };
2531                let no_prec_spec = FmtSpec {
2532                    left_align: spec.left_align,
2533                    plus_sign: spec.plus_sign,
2534                    space_sign: spec.space_sign,
2535                    alt_form: spec.alt_form,
2536                    zero_pad: spec.zero_pad,
2537                    width: spec.width,
2538                    precision: None,
2539                };
2540                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2541            }
2542            b'f' | b'e' | b'E' | b'g' | b'G' => {
2543                check_conv_spec(state, form, FMT_FLAGS_F, true)?;
2544                let n = state.check_arg_number(arg)?;
2545                let body = format_float(n, conv, &spec);
2546                let (sign, digits): (Vec<u8>, Vec<u8>) =
2547                    if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2548                        (vec![body[0]], body[1..].to_vec())
2549                    } else if n >= 0.0 && spec.plus_sign {
2550                        (b"+".to_vec(), body)
2551                    } else if n >= 0.0 && spec.space_sign {
2552                        (b" ".to_vec(), body)
2553                    } else {
2554                        (Vec::new(), body)
2555                    };
2556                let no_prec_spec = FmtSpec {
2557                    left_align: spec.left_align,
2558                    plus_sign: spec.plus_sign,
2559                    space_sign: spec.space_sign,
2560                    alt_form: spec.alt_form,
2561                    zero_pad: spec.zero_pad,
2562                    width: spec.width,
2563                    precision: None,
2564                };
2565                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2566            }
2567            b'p' => {
2568                check_conv_spec(state, form, FMT_FLAGS_C, false)?;
2569                let s: Vec<u8> = match lua_vm::api::to_pointer(state, arg) {
2570                    Some(p) => format!("0x{:x}", p).into_bytes(),
2571                    None => b"(null)".to_vec(),
2572                };
2573                pad_str(
2574                    &mut buf,
2575                    &s,
2576                    &FmtSpec {
2577                        precision: None,
2578                        ..spec
2579                    },
2580                );
2581            }
2582            b'q' => {
2583                if matches!(
2584                    state.global().lua_version,
2585                    lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
2586                ) {
2587                    let s = state.check_arg_string(arg)?;
2588                    if state.global().lua_version == lua_types::LuaVersion::V51 {
2589                        addquoted_51(&mut buf, &s);
2590                    } else {
2591                        addquoted(&mut buf, &s);
2592                    }
2593                } else {
2594                    if form.len() > 2 {
2595                        return Err(LuaError::runtime(format_args!(
2596                            "specifier '%q' cannot have modifiers"
2597                        )));
2598                    }
2599                    addliteral(state, &mut buf, arg)?;
2600                }
2601            }
2602            b's' => {
2603                check_conv_spec(state, form, FMT_FLAGS_C, true)?;
2604                let pushed = matches!(state.global().lua_version, lua_types::LuaVersion::V51);
2605                let s = if pushed {
2606                    state.check_arg_string(arg)?
2607                } else {
2608                    state.to_display_string(arg)?
2609                };
2610                let has_modifiers = spec.width != 0 || spec.precision.is_some();
2611                if has_modifiers && s.contains(&0u8) {
2612                    return Err(lua_vm::debug::arg_error_impl(
2613                        state,
2614                        arg,
2615                        b"string contains zeros",
2616                    ));
2617                }
2618                pad_str(&mut buf, &s, &spec);
2619                if !pushed {
2620                    state.pop_n(1);
2621                }
2622            }
2623            _ => {
2624                let verb: &[u8] = if state.global().lua_version == lua_types::LuaVersion::V53 {
2625                    b"option"
2626                } else {
2627                    b"conversion"
2628                };
2629                let mut msg = b"invalid ".to_vec();
2630                msg.extend_from_slice(verb);
2631                msg.extend_from_slice(b" '");
2632                msg.extend_from_slice(form);
2633                msg.extend_from_slice(b"' to 'format'");
2634                return Err(lua_vm::debug::c_api_runtime(state, msg));
2635            }
2636        }
2637    }
2638
2639    state.push_bytes(&buf)?;
2640    Ok(1)
2641}
2642
2643// ────────────────────────────────────────────────────────────────────────────
2644// §8  Pack / unpack
2645// ────────────────────────────────────────────────────────────────────────────
2646
2647/// Return `true` if `c` is an ASCII digit.
2648fn is_digit(c: u8) -> bool {
2649    c.is_ascii_digit()
2650}
2651
2652/// Read an optional integer from the format string, returning `df` if absent.
2653///
2654/// `wide` selects the accumulator width: 5.3/5.4 used `int` (cap `i32::MAX`);
2655/// 5.5 uses `size_t` (cap the host pointer width). The reference stops consuming
2656/// digits once another `*10 + 9` would overflow, leaving the rest to be read as
2657/// the next option — which is why `c<int-overflow>` yields "invalid format
2658/// option '<digit>'" on 5.3/5.4 but parses cleanly on 5.5.
2659fn getnum(fmt: &[u8], pos: &mut usize, df: i64, wide: bool) -> i64 {
2660    if *pos >= fmt.len() || !is_digit(fmt[*pos]) {
2661        return df;
2662    }
2663    let cap: i64 = if wide { i64::MAX } else { i32::MAX as i64 };
2664    let mut a = 0i64;
2665    while *pos < fmt.len() && is_digit(fmt[*pos]) {
2666        a = a * 10 + (fmt[*pos] - b'0') as i64;
2667        *pos += 1;
2668        if a > (cap - 9) / 10 {
2669            break;
2670        }
2671    }
2672    a
2673}
2674
2675/// Read an integer from the format string, error if out of `[1, MAXINTSIZE]`.
2676///
2677fn getnumlimit(fmt: &[u8], pos: &mut usize, df: i64) -> Result<usize, LuaError> {
2678    let sz = getnum(fmt, pos, df, false);
2679    if sz > MAX_INT_SIZE as i64 || sz <= 0 {
2680        return Err(LuaError::runtime(format_args!(
2681            "integral size ({}) out of limits [1,{}]",
2682            sz, MAX_INT_SIZE
2683        )));
2684    }
2685    Ok(sz as usize)
2686}
2687
2688/// Read and classify the next pack format option, filling `size`.
2689///
2690fn getoption(
2691    h: &mut Header,
2692    fmt: &[u8],
2693    pos: &mut usize,
2694    size: &mut usize,
2695) -> Result<KOption, LuaError> {
2696    // In Rust, the native max-align of a union of f64/void*/size_t is 8 on 64-bit.
2697    const NATIVE_MAX_ALIGN: usize = std::mem::align_of::<f64>();
2698
2699    if *pos >= fmt.len() {
2700        return Ok(KOption::Nop);
2701    }
2702    let opt = fmt[*pos];
2703    *pos += 1;
2704    *size = 0;
2705
2706    match opt {
2707        b'b' => {
2708            *size = 1;
2709            Ok(KOption::Int)
2710        }
2711        b'B' => {
2712            *size = 1;
2713            Ok(KOption::Uint)
2714        }
2715        b'h' => {
2716            *size = 2;
2717            Ok(KOption::Int)
2718        }
2719        b'H' => {
2720            *size = 2;
2721            Ok(KOption::Uint)
2722        }
2723        b'l' => {
2724            *size = 8;
2725            Ok(KOption::Int)
2726        } // sizeof(long) on 64-bit
2727        b'L' => {
2728            *size = 8;
2729            Ok(KOption::Uint)
2730        }
2731        b'j' => {
2732            *size = SZINT;
2733            Ok(KOption::Int)
2734        }
2735        b'J' => {
2736            *size = SZINT;
2737            Ok(KOption::Uint)
2738        }
2739        b'T' => {
2740            *size = std::mem::size_of::<usize>();
2741            Ok(KOption::Uint)
2742        }
2743        b'f' => {
2744            *size = 4;
2745            Ok(KOption::Float)
2746        }
2747        b'n' => {
2748            *size = 8;
2749            Ok(KOption::Number)
2750        } // sizeof(lua_Number) = sizeof(f64) = 8
2751        b'd' => {
2752            *size = 8;
2753            Ok(KOption::Double)
2754        } // sizeof(double) = 8
2755        b'i' => {
2756            *size = getnumlimit(fmt, pos, 4)?;
2757            Ok(KOption::Int)
2758        }
2759        b'I' => {
2760            *size = getnumlimit(fmt, pos, 4)?;
2761            Ok(KOption::Uint)
2762        }
2763        b's' => {
2764            *size = getnumlimit(fmt, pos, std::mem::size_of::<usize>() as i64)?;
2765            Ok(KOption::Kstring)
2766        }
2767        b'c' => {
2768            let n = getnum(fmt, pos, -1, h.wide_size);
2769            if n == -1 {
2770                return Err(LuaError::runtime(format_args!(
2771                    "missing size for format option 'c'"
2772                )));
2773            }
2774            *size = n as usize;
2775            Ok(KOption::Char)
2776        }
2777        b'z' => Ok(KOption::Zstr),
2778        b'x' => {
2779            *size = 1;
2780            Ok(KOption::Padding)
2781        }
2782        b'X' => Ok(KOption::Paddalign),
2783        b' ' => Ok(KOption::Nop),
2784        b'<' => {
2785            h.is_little = true;
2786            Ok(KOption::Nop)
2787        }
2788        b'>' => {
2789            h.is_little = false;
2790            Ok(KOption::Nop)
2791        }
2792        b'=' => {
2793            h.is_little = cfg!(target_endian = "little");
2794            Ok(KOption::Nop)
2795        }
2796        b'!' => {
2797            let n = getnum(fmt, pos, NATIVE_MAX_ALIGN as i64, false);
2798            h.max_align = getnumlimit(fmt, pos, n)?;
2799            Ok(KOption::Nop)
2800        }
2801        _ => Err(LuaError::runtime(format_args!(
2802            "invalid format option '{}'",
2803            opt as char
2804        ))),
2805    }
2806}
2807
2808/// Get full details about the next format option, including alignment padding.
2809///
2810fn getdetails(
2811    state: &mut LuaState,
2812    h: &mut Header,
2813    total_size: usize,
2814    fmt: &[u8],
2815    pos: &mut usize,
2816    psize: &mut usize,
2817    ntoalign: &mut usize,
2818) -> Result<KOption, LuaError> {
2819    let opt = getoption(h, fmt, pos, psize)?;
2820    let mut align = *psize;
2821
2822    if opt == KOption::Paddalign {
2823        if *pos >= fmt.len() {
2824            return Err(lua_vm::debug::arg_error_impl(
2825                state,
2826                1,
2827                b"invalid next option for option 'X'",
2828            ));
2829        }
2830        let mut dummy_size = 0usize;
2831        let next_opt = getoption(h, fmt, pos, &mut dummy_size)?;
2832        align = dummy_size;
2833        if next_opt == KOption::Char || align == 0 {
2834            return Err(lua_vm::debug::arg_error_impl(
2835                state,
2836                1,
2837                b"invalid next option for option 'X'",
2838            ));
2839        }
2840    }
2841
2842    if align <= 1 || opt == KOption::Char {
2843        *ntoalign = 0;
2844    } else {
2845        if align > h.max_align {
2846            align = h.max_align;
2847        }
2848        if (align & (align - 1)) != 0 {
2849            return Err(lua_vm::debug::arg_error_impl(
2850                state,
2851                1,
2852                b"format asks for alignment not power of 2",
2853            ));
2854        }
2855        *ntoalign = (align - (total_size & (align - 1))) & (align - 1);
2856    }
2857    Ok(opt)
2858}
2859
2860/// Pack integer `n` with `size` bytes into `buf` with given endianness.
2861///
2862fn packint(buf: &mut Vec<u8>, mut n: u64, is_little: bool, size: usize, neg: bool) {
2863    let start = buf.len();
2864    buf.resize(start + size, 0);
2865    let slice = &mut buf[start..start + size];
2866    // Write LSB first (little-endian), then swap if big-endian
2867    for i in 0..size {
2868        slice[if is_little { i } else { size - 1 - i }] = (n & MC as u64) as u8;
2869        n >>= NB;
2870    }
2871    // Sign extension for negative numbers larger than lua_Integer
2872    if neg && size > SZINT {
2873        for i in SZINT..size {
2874            slice[if is_little { i } else { size - 1 - i }] = MC;
2875        }
2876    }
2877}
2878
2879/// Copy bytes with endianness correction.
2880///
2881fn copywithendian(dest: &mut [u8], src: &[u8], is_little: bool) {
2882    debug_assert_eq!(dest.len(), src.len());
2883    if is_little == cfg!(target_endian = "little") {
2884        dest.copy_from_slice(src);
2885    } else {
2886        for (d, s) in dest.iter_mut().zip(src.iter().rev()) {
2887            *d = *s;
2888        }
2889    }
2890}
2891
2892/// Unpack a (possibly signed) integer from `data[0..size]`.
2893///
2894fn unpackint(
2895    _state: &LuaState,
2896    data: &[u8],
2897    is_little: bool,
2898    size: usize,
2899    is_signed: bool,
2900) -> Result<i64, LuaError> {
2901    let limit = size.min(SZINT);
2902    let mut res: u64 = 0;
2903    for i in (0..limit).rev() {
2904        res <<= NB;
2905        let byte_idx = if is_little { i } else { size - 1 - i };
2906        res |= data[byte_idx] as u64;
2907    }
2908
2909    if size < SZINT {
2910        if is_signed {
2911            let mask: u64 = 1u64 << (size * NB as usize - 1);
2912            res = (res ^ mask).wrapping_sub(mask);
2913        }
2914    } else if size > SZINT {
2915        let mask = if !is_signed || (res as i64) >= 0 {
2916            0u8
2917        } else {
2918            MC
2919        };
2920        for i in limit..size {
2921            let byte_idx = if is_little { i } else { size - 1 - i };
2922            if data[byte_idx] != mask {
2923                return Err(LuaError::runtime(format_args!(
2924                    "{}-byte integer does not fit into Lua Integer",
2925                    size
2926                )));
2927            }
2928        }
2929    }
2930    Ok(res as i64)
2931}
2932
2933/// `string.pack(fmt, ...)` — pack values into a binary string.
2934///
2935pub fn str_pack(state: &mut LuaState) -> Result<usize, LuaError> {
2936    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2937    let fmt = &fmt_bytes[..];
2938    let mut h = Header::new(state.global().lua_version == lua_types::LuaVersion::V55);
2939    let mut arg = 1i32;
2940    let mut total_size = 0usize;
2941    let mut buf: Vec<u8> = Vec::new();
2942    let mut pos = 0usize;
2943
2944    while pos < fmt.len() {
2945        let mut size = 0usize;
2946        let mut ntoalign = 0usize;
2947        let opt = getdetails(
2948            state,
2949            &mut h,
2950            total_size,
2951            fmt,
2952            &mut pos,
2953            &mut size,
2954            &mut ntoalign,
2955        )?;
2956        // 5.5 `str_pack` rejects an oversized running total ("result too long")
2957        // BEFORE consuming the value argument; 5.3/5.4 have no such check (their
2958        // `int` sizes cannot reach the limit). MAX_SIZE is the host pointer width.
2959        if h.wide_size {
2960            let space = ntoalign + size;
2961            if space > (i64::MAX as usize) || total_size > (i64::MAX as usize) - space {
2962                return Err(lua_vm::debug::arg_error_impl(
2963                    state,
2964                    arg,
2965                    b"result too long",
2966                ));
2967            }
2968        }
2969        total_size += ntoalign + size;
2970        for _ in 0..ntoalign {
2971            buf.push(PACK_PAD_BYTE);
2972        }
2973        arg += 1;
2974
2975        match opt {
2976            KOption::Int => {
2977                let n = state.check_arg_integer(arg)?;
2978                if size < SZINT {
2979                    let lim: i64 = 1i64 << (size * NB as usize - 1);
2980                    if !(-lim <= n && n < lim) {
2981                        return Err(lua_vm::debug::arg_error_impl(
2982                            state,
2983                            arg,
2984                            b"integer overflow",
2985                        ));
2986                    }
2987                }
2988                packint(&mut buf, n as u64, h.is_little, size, n < 0);
2989            }
2990            KOption::Uint => {
2991                let n = state.check_arg_integer(arg)?;
2992                if size < SZINT {
2993                    let lim: u64 = 1u64 << (size * NB as usize);
2994                    if (n as u64) >= lim {
2995                        return Err(lua_vm::debug::arg_error_impl(
2996                            state,
2997                            arg,
2998                            b"unsigned overflow",
2999                        ));
3000                    }
3001                }
3002                packint(&mut buf, n as u64, h.is_little, size, false);
3003            }
3004            KOption::Float => {
3005                let f = state.check_arg_number(arg)? as f32;
3006                let start = buf.len();
3007                buf.resize(start + 4, 0);
3008                copywithendian(
3009                    &mut buf[start..start + 4],
3010                    &f.to_bits().to_ne_bytes(),
3011                    h.is_little,
3012                );
3013            }
3014            KOption::Number => {
3015                let f = state.check_arg_number(arg)?;
3016                let start = buf.len();
3017                buf.resize(start + 8, 0);
3018                copywithendian(
3019                    &mut buf[start..start + 8],
3020                    &f.to_bits().to_ne_bytes(),
3021                    h.is_little,
3022                );
3023            }
3024            KOption::Double => {
3025                let f = state.check_arg_number(arg)? as f64;
3026                let start = buf.len();
3027                buf.resize(start + 8, 0);
3028                copywithendian(
3029                    &mut buf[start..start + 8],
3030                    &f.to_bits().to_ne_bytes(),
3031                    h.is_little,
3032                );
3033            }
3034            KOption::Char => {
3035                let s = state.check_arg_string(arg)?.to_vec();
3036                if s.len() > size {
3037                    return Err(lua_vm::debug::arg_error_impl(
3038                        state,
3039                        arg,
3040                        b"string longer than given size",
3041                    ));
3042                }
3043                buf.extend_from_slice(&s);
3044                let pad = size - s.len();
3045                for _ in 0..pad {
3046                    buf.push(PACK_PAD_BYTE);
3047                }
3048            }
3049            KOption::Kstring => {
3050                let s = state.check_arg_string(arg)?.to_vec();
3051                let len = s.len();
3052                if size < SZINT && len >= (1usize << (size * 8)) {
3053                    return Err(lua_vm::debug::arg_error_impl(
3054                        state,
3055                        arg,
3056                        b"string length does not fit in given size",
3057                    ));
3058                }
3059                packint(&mut buf, len as u64, h.is_little, size, false);
3060                buf.extend_from_slice(&s);
3061                total_size += len;
3062            }
3063            KOption::Zstr => {
3064                let s = state.check_arg_string(arg)?.to_vec();
3065                if s.contains(&0) {
3066                    return Err(lua_vm::debug::arg_error_impl(
3067                        state,
3068                        arg,
3069                        b"string contains zeros",
3070                    ));
3071                }
3072                buf.extend_from_slice(&s);
3073                buf.push(0);
3074                total_size += s.len() + 1;
3075            }
3076            KOption::Padding => {
3077                buf.push(PACK_PAD_BYTE);
3078                arg -= 1; // undo increment
3079            }
3080            KOption::Paddalign | KOption::Nop => {
3081                arg -= 1; // undo increment
3082            }
3083        }
3084    }
3085
3086    state.push_bytes(&buf)?;
3087    Ok(1)
3088}
3089
3090/// `string.packsize(fmt)` — return the byte-size the format would produce.
3091///
3092pub fn str_packsize(state: &mut LuaState) -> Result<usize, LuaError> {
3093    let fmt_bytes = state.check_arg_string(1)?.to_vec();
3094    let fmt = &fmt_bytes[..];
3095    let mut h = Header::new(state.global().lua_version == lua_types::LuaVersion::V55);
3096    let mut total_size = 0usize;
3097    let mut pos = 0usize;
3098
3099    while pos < fmt.len() {
3100        let mut size = 0usize;
3101        let mut ntoalign = 0usize;
3102        let opt = getdetails(
3103            state,
3104            &mut h,
3105            total_size,
3106            fmt,
3107            &mut pos,
3108            &mut size,
3109            &mut ntoalign,
3110        )?;
3111        if opt == KOption::Kstring || opt == KOption::Zstr {
3112            return Err(lua_vm::debug::arg_error_impl(
3113                state,
3114                1,
3115                b"variable-length format",
3116            ));
3117        }
3118        let space = ntoalign + size;
3119        let max_total: usize = if h.wide_size {
3120            i64::MAX as usize
3121        } else {
3122            PACK_MAXSIZE
3123        };
3124        if space > max_total || total_size > max_total - space {
3125            return Err(lua_vm::debug::arg_error_impl(
3126                state,
3127                1,
3128                b"format result too large",
3129            ));
3130        }
3131        total_size += space;
3132    }
3133    state.push(LuaValue::Int(total_size as i64));
3134    Ok(1)
3135}
3136
3137/// `string.unpack(fmt, s [, pos])` — unpack binary data from string.
3138///
3139pub fn str_unpack(state: &mut LuaState) -> Result<usize, LuaError> {
3140    let fmt_bytes = state.check_arg_string(1)?.to_vec();
3141    let data_bytes = state.check_arg_string(2)?.to_vec();
3142    let ld = data_bytes.len();
3143    let pos_raw = state.opt_arg_integer(3, 1)?;
3144    let mut pos = if matches!(state.global().lua_version, lua_types::LuaVersion::V53) {
3145        posrelat_53(pos_raw, ld).wrapping_sub(1)
3146    } else {
3147        pos_relat_i(pos_raw, ld).saturating_sub(1)
3148    };
3149
3150    if pos > ld {
3151        return Err(lua_vm::debug::arg_error_impl(
3152            state,
3153            3,
3154            b"initial position out of string",
3155        ));
3156    }
3157
3158    let fmt = &fmt_bytes[..];
3159    let data = &data_bytes[..];
3160    let mut h = Header::new(state.global().lua_version == lua_types::LuaVersion::V55);
3161    let mut fmt_pos = 0usize;
3162    let mut n = 0usize;
3163
3164    while fmt_pos < fmt.len() {
3165        let mut size = 0usize;
3166        let mut ntoalign = 0usize;
3167        let opt = getdetails(
3168            state,
3169            &mut h,
3170            pos,
3171            fmt,
3172            &mut fmt_pos,
3173            &mut size,
3174            &mut ntoalign,
3175        )?;
3176
3177        if ntoalign + size > ld - pos {
3178            return Err(lua_vm::debug::arg_error_impl(
3179                state,
3180                2,
3181                b"data string too short",
3182            ));
3183        }
3184        pos += ntoalign;
3185        state.ensure_stack(2, "too many results")?;
3186        n += 1;
3187
3188        match opt {
3189            KOption::Int => {
3190                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, true)?;
3191                state.push(LuaValue::Int(v));
3192            }
3193            KOption::Uint => {
3194                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, false)?;
3195                state.push(LuaValue::Int(v));
3196            }
3197            KOption::Float => {
3198                let mut bytes = [0u8; 4];
3199                copywithendian(&mut bytes, &data[pos..pos + 4], h.is_little);
3200                let f = f32::from_bits(u32::from_ne_bytes(bytes));
3201                state.push(LuaValue::Float(f as f64));
3202            }
3203            KOption::Number => {
3204                let mut bytes = [0u8; 8];
3205                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
3206                let f = f64::from_bits(u64::from_ne_bytes(bytes));
3207                state.push(LuaValue::Float(f));
3208            }
3209            KOption::Double => {
3210                let mut bytes = [0u8; 8];
3211                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
3212                let f = f64::from_bits(u64::from_ne_bytes(bytes));
3213                state.push(LuaValue::Float(f));
3214            }
3215            KOption::Char => {
3216                state.push_bytes(&data[pos..pos + size])?;
3217            }
3218            KOption::Kstring => {
3219                let len =
3220                    unpackint(state, &data[pos..pos + size], h.is_little, size, false)? as usize;
3221                if len > ld - pos - size {
3222                    return Err(lua_vm::debug::arg_error_impl(
3223                        state,
3224                        2,
3225                        b"data string too short",
3226                    ));
3227                }
3228                state.push_bytes(&data[pos + size..pos + size + len])?;
3229                pos += len;
3230            }
3231            KOption::Zstr => {
3232                let found = data[pos..].iter().position(|&b| b == 0);
3233                let end = match found {
3234                    Some(e) => e,
3235                    None => {
3236                        return Err(lua_vm::debug::arg_error_impl(
3237                            state,
3238                            2,
3239                            b"unfinished string for format 'z'",
3240                        ))
3241                    }
3242                };
3243                if pos + end >= ld {
3244                    return Err(lua_vm::debug::arg_error_impl(
3245                        state,
3246                        2,
3247                        b"unfinished string for format 'z'",
3248                    ));
3249                }
3250                state.push_bytes(&data[pos..pos + end])?;
3251                pos += end + 1;
3252            }
3253            KOption::Paddalign | KOption::Padding | KOption::Nop => {
3254                n -= 1; // undo increment
3255            }
3256        }
3257        pos += size;
3258    }
3259
3260    state.push(LuaValue::Int((pos + 1) as i64));
3261    Ok(n + 1)
3262}
3263
3264// ────────────────────────────────────────────────────────────────────────────
3265// §9  Module registration
3266// ────────────────────────────────────────────────────────────────────────────
3267
3268/// Function table for `string` library.
3269///
3270pub const STRING_LIB: &[(&[u8], lua_CFunction)] = &[
3271    (b"byte", str_byte),
3272    (b"char", str_char),
3273    (b"dump", str_dump),
3274    (b"find", str_find),
3275    (b"format", str_format),
3276    (b"gmatch", gmatch),
3277    (b"gsub", str_gsub),
3278    (b"len", str_len),
3279    (b"lower", str_lower),
3280    (b"match", str_match),
3281    (b"rep", str_rep),
3282    (b"reverse", str_reverse),
3283    (b"sub", str_sub),
3284    (b"upper", str_upper),
3285];
3286
3287/// Pack/unpack entries (`string.pack`, `string.packsize`, `string.unpack`).
3288///
3289/// These were introduced in Lua 5.3; they are absent in 5.1 and 5.2, so they
3290/// are registered conditionally in `luaopen_string` rather than living in the
3291/// unconditional `STRING_LIB` array.
3292const STRING_PACK_LIB: &[(&[u8], lua_CFunction)] = &[
3293    (b"pack", str_pack),
3294    (b"packsize", str_packsize),
3295    (b"unpack", str_unpack),
3296];
3297
3298/// Metamethods to install on the string metatable.
3299///
3300pub const STRING_META_METHODS: &[(&[u8], lua_CFunction)] = &[
3301    (b"__add", arith_add),
3302    (b"__sub", arith_sub),
3303    (b"__mul", arith_mul),
3304    (b"__mod", arith_mod),
3305    (b"__pow", arith_pow),
3306    (b"__div", arith_div),
3307    (b"__idiv", arith_idiv),
3308    (b"__unm", arith_unm),
3309];
3310
3311/// Create the string metatable and set it as the metatable for all strings.
3312///
3313pub fn createmetatable(state: &mut LuaState) -> Result<(), LuaError> {
3314    state.new_lib_table(STRING_META_METHODS)?;
3315    state.set_funcs(STRING_META_METHODS, 0)?;
3316    state.push_string(b"")?;
3317    let mt_idx = state.top_idx() - 2;
3318    let mt = state.get_at(mt_idx);
3319    state.push(mt);
3320    state.set_metatable(-2)?;
3321    state.pop_n(1);
3322    let strlib_idx = state.top_idx() - 2;
3323    let strlib = state.get_at(strlib_idx);
3324    state.push(strlib);
3325    state.set_field(-2, b"__index")?;
3326    state.pop_n(1);
3327    Ok(())
3328}
3329
3330/// `luaopen_string` — open the string library.
3331///
3332pub fn luaopen_string(state: &mut LuaState) -> Result<usize, LuaError> {
3333    state.new_lib(STRING_LIB)?;
3334    // Lua 5.1 carries `string.gfind`, the pre-5.0 name for `gmatch` (an exact
3335    // alias). It was removed in 5.2. Verified against lua5.1.5:
3336    // `type(string.gfind)` == "function" and it iterates identically to
3337    // `gmatch`. See specs/followup/5.1-roster-syntax.md §1.
3338    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
3339        state.push_c_function(gmatch)?;
3340        state.set_field(-2, b"gfind")?;
3341    }
3342    if state.global().lua_version != lua_types::LuaVersion::V51
3343        && state.global().lua_version != lua_types::LuaVersion::V52
3344    {
3345        for (name, f) in STRING_PACK_LIB {
3346            state.push_c_function(*f)?;
3347            state.set_field(-2, name)?;
3348        }
3349    }
3350    createmetatable(state)?;
3351    Ok(1)
3352}
3353
3354// ────────────────────────────────────────────────────────────────────────────
3355// PORT STATUS
3356//   target_crate:  lua-stdlib
3357//   unsafe_blocks: 0
3358//   load-bearing:  the recursive pattern matcher (match_pat + its helpers
3359//                  singlematch/match_class/matchbracketclass/classend/
3360//                  max_expand/min_expand/start_capture/end_capture/
3361//                  match_capture/matchbalance) is HOT and CPI-critical. The
3362//                  goto->`'outer: loop` tail-call translation, the per-char
3363//                  match dispatch, and the recursion structure are NOT to be
3364//                  refactored — extract/rename and doc-comments only, proven
3365//                  Ir/branch-sim neutral. See GRADUATED.md "string".
3366//   net:           behavior is pinned by the behavioral suite — multiversion
3367//                  oracle (incl. the P2c pattern-too-complex gate, the 5.3.3
3368//                  empty-match advance rule, and the capture-overflow tripwire),
3369//                  strings.lua + pm.lua, check.sh 5.1-5.5. Version seams are
3370//                  single-sourced in matcher_bounds_depth (5.1 has no MAXCCALLS
3371//                  recursion guard) and matcher_dedups_empty_match (the 5.3.3
3372//                  `e != lastmatch` rule, absent on 5.1/5.2). Further per-version
3373//                  seams: pack/packsize/unpack are registered only for 5.3+
3374//                  (STRING_PACK_LIB in luaopen_string); string.rep ignores the
3375//                  separator on 5.1; `%q` strict-string-coerces on 5.1/5.2
3376//                  (addquoted_51 for 5.1's NUL/`\r`/literal-control rules) and
3377//                  emits inf/nan literally on 5.3 (quotefloat); `%s` is strict on
3378//                  5.1; `%u`/`%o`/`%x`/`%X` cast the float-only number per
3379//                  version (format_uint_arg: 5.1 saturating fptoui, 5.2 range
3380//                  check); `%g`/`%e` preserve negative-zero on every version.
3381//   perf:          the cold API fns borrow source bytes through to_lua_string
3382//                  (GcRef) rather than copying via check_arg_string; num_to_str
3383//                  stringifies small integers into a stack buffer. string_ops
3384//                  ~2.00x, string_ops_long ~1.48x vs reference (best-of-5).
3385// ────────────────────────────────────────────────────────────────────────────