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