Skip to main content

lua_stdlib/
math_lib.rs

1//! Standard mathematical library — `math.*`.
2//!
3//! The PRNG is xoshiro256** operating on four 64-bit words (the single 64-bit
4//! code path; there is no 32-bit fallback to keep).
5//!
6//! The deprecated `LUA_COMPAT_MATHLIB` roster (`cosh`, `sinh`, `tanh`, `pow`,
7//! `log10`, `atan2`) ships in the default 5.1/5.2/5.3/5.4 builds and is dropped
8//! in 5.5; `frexp`/`ldexp` survive into 5.5. `atan2` is an alias of `math_atan`.
9//! Which functions exist per version is the registration logic in
10//! [`luaopen_math`].
11
12use crate::state_stub::{LuaState, LuaStateStubExt as _};
13use lua_types::{LuaError, LuaType, LuaValue};
14
15// ── Constants ──────────────────────────────────────────────────────────────
16
17/// `math.pi`. Higher precision than `std::f64::consts::PI` (which is rounded to
18/// the nearest `f64`); both round-trip to the same `f64` bit pattern.
19const PI: f64 = 3.141592653589793238462643383279502884_f64;
20
21// ── Type aliases for library registration ─────────────────────────────────
22
23/// A Lua C-style function: takes the Lua state, returns count of pushed values.
24type LuaCFunction = fn(&mut LuaState) -> Result<usize, LuaError>;
25
26// ── PRNG state ────────────────────────────────────────────────────────────
27
28/// State for the xoshiro256** PRNG.
29///
30/// In C this is stored as raw `lua_newuserdatauv` memory and accessed by
31/// casting the userdata pointer. Until typed-userdata closure upvalues are
32/// available, the PRNG state instead lives in a thread-local cell so that
33/// `math.random` and `math.randomseed` are callable from Lua. This collapses
34/// per-`LuaState` PRNG isolation to per-thread, which is sufficient for the
35/// 5.4 test corpus.
36struct RanState {
37    s: [u64; 4],
38}
39
40thread_local! {
41    static RAN_STATE: std::cell::RefCell<RanState> =
42        std::cell::RefCell::new(RanState { s: [0xff, 0xff, 0xff, 0xff] });
43}
44
45/// The xoshiro256** generator: the bit-exact, load-bearing PRNG core.
46///
47/// Every function here is pinned byte-for-byte by the behavioral net (the
48/// `multiversion_oracle` PRNG-sequence tests on 5.4/5.5). The internals must NOT
49/// be reordered or "simplified" — any change to the arithmetic diverges from the
50/// reference stream. Grouping them in one private module makes that contract
51/// explicit; the callers reach them as `xoshiro::*`.
52mod xoshiro {
53    /// Number of binary digits in the `f64` mantissa (`DBL_MANT_DIG`).
54    const FIGS: u32 = 53;
55
56    /// Bits to discard from a 64-bit random word before float conversion (`= 11`):
57    /// the word is reduced to its top [`FIGS`] significant bits.
58    const SHIFT64_FIG: u32 = 64 - FIGS;
59
60    /// Advance the xoshiro256** state by one step and return the next raw 64-bit
61    /// pseudo-random value.
62    pub(super) fn next_rand(s: &mut [u64; 4]) -> u64 {
63        let s0 = s[0];
64        let s1 = s[1];
65        let s2 = s[2] ^ s0;
66        let s3 = s[3] ^ s1;
67        let res = s1.wrapping_mul(5).rotate_left(7).wrapping_mul(9);
68        s[0] = s0 ^ s3;
69        s[1] = s1 ^ s2;
70        s[2] = s2 ^ (s1 << 17);
71        s[3] = s3.rotate_left(45);
72        res
73    }
74
75    /// Convert a raw 64-bit PRNG output to a float in [0.0, 1.0).
76    ///
77    /// Takes the top FIGS=53 bits, interprets them as a signed integer, scales
78    /// by `scaleFIG = 0.5 / 2^52`, then corrects the two's-complement sign.
79    pub(super) fn rand_to_float(x: u64) -> f64 {
80        let sx = (x >> SHIFT64_FIG) as i64;
81        let scale_fig: f64 = 0.5 / ((1u64 << (FIGS - 1)) as f64);
82        let mut res = (sx as f64) * scale_fig;
83        if sx < 0 {
84            res += 1.0;
85        }
86        debug_assert!(0.0 <= res && res < 1.0);
87        res
88    }
89
90    /// Initialise the four PRNG words from two seed values.
91    ///
92    /// `s[1]` is forced to `0xff` so the state is never all-zero (xoshiro's
93    /// fixed point), and sixteen draws are discarded to spread the seed bits before
94    /// the first observable output. Takes the word array (not the [`LuaState`]) so
95    /// the caller can push the seed values without a borrow conflict against the
96    /// `RanState` upvalue.
97    pub(super) fn set_seed_words(s: &mut [u64; 4], n1: u64, n2: u64) {
98        s[0] = n1;
99        s[1] = 0xff;
100        s[2] = n2;
101        s[3] = 0;
102        for _ in 0..16 {
103            next_rand(s);
104        }
105    }
106
107    /// Project `ran` uniformly into `[0, n]` by rejection sampling.
108    ///
109    /// When `n + 1` is a power of two the low bits are already uniform, so a single
110    /// mask suffices. Otherwise `lim` is built as the smallest `2^b - 1` not smaller
111    /// than `n` (the bit-smear), and draws outside `[0, n]` are rejected and
112    /// redrawn. The `>> 32` smear step is unconditional here because `u64` always
113    /// has 64 bits; the C source guards it behind an `#if` on the integer width.
114    /// Takes the word array (not `&mut RanState`) to avoid nested borrows at the
115    /// call sites.
116    pub(super) fn project(mut ran: u64, n: u64, s: &mut [u64; 4]) -> u64 {
117        if (n & n.wrapping_add(1)) == 0 {
118            return ran & n;
119        }
120        let mut lim = n;
121        lim |= lim >> 1;
122        lim |= lim >> 2;
123        lim |= lim >> 4;
124        lim |= lim >> 8;
125        lim |= lim >> 16;
126        lim |= lim >> 32;
127        debug_assert!((lim & lim.wrapping_add(1)) == 0);
128        debug_assert!(lim >= n);
129        debug_assert!((lim >> 1) < n);
130        loop {
131            ran &= lim;
132            if ran <= n {
133                break;
134            }
135            ran = next_rand(s);
136        }
137        ran
138    }
139}
140
141// ── Helpers ───────────────────────────────────────────────────────────────
142
143/// Whether the argument at stack index `n` carries the integer subtype.
144///
145/// Several `math.*` functions (`abs`, `floor`, `ceil`, `fmod`, `modf`, `type`)
146/// branch on whether an argument is an `Int` versus a `Float`, preserving the
147/// integer subtype on the integer path. This names that recurring predicate so
148/// the branch reads as intent rather than as a raw `matches!` on the stack slot.
149fn arg_is_int(state: &mut LuaState, n: i32) -> bool {
150    matches!(state.value_at(n), LuaValue::Int(_))
151}
152
153/// The smallest `f64` whose value equals an `i64` exactly: `i64::MIN` as a
154/// float (`-2^63`, exactly representable).
155const I64_MIN_AS_F64: f64 = i64::MIN as f64;
156
157/// One past the largest representable `i64`, as a float: `2^63`. `i64::MAX`
158/// (`2^63 - 1`) is not exactly representable in `f64`, so the in-range test
159/// uses a strict `<` against this boundary rather than `<= i64::MAX as f64`.
160const I64_MAX_PLUS_1_AS_F64: f64 = -(i64::MIN as f64);
161
162/// Push `d` as an `Int` when it has an integer value that fits exactly in an
163/// `i64`, otherwise push it as a `Float`.
164///
165/// `floor`/`ceil`/`modf` use this to keep their result an integer subtype when
166/// it round-trips through `i64` without loss, falling back to a float for
167/// magnitudes outside the `i64` range. The bound check is half-open
168/// (`I64_MIN_AS_F64 <= d < I64_MAX_PLUS_1_AS_F64`) because `i64::MAX` itself is
169/// not exactly representable as an `f64`.
170fn push_int_or_float(state: &mut LuaState, d: f64) {
171    if d >= I64_MIN_AS_F64 && d < I64_MAX_PLUS_1_AS_F64 {
172        state.push(LuaValue::Int(d as i64));
173    } else {
174        state.push(LuaValue::Float(d));
175    }
176}
177
178/// Leave the first argument as the sole result (an integer floor/ceil/modf is
179/// already its own integer part — return it unchanged, preserving subtype).
180///
181/// This goes through the public, frame-relative [`lua_vm::api::set_top`] so the
182/// truncation is relative to the call frame; the inherent `LuaState::set_top`
183/// would treat its argument as an absolute `StackIdx` and discard the argument.
184fn keep_first_arg(state: &mut LuaState) -> Result<(), LuaError> {
185    lua_vm::api::set_top(state, 1)
186}
187
188// ── Basic math functions ──────────────────────────────────────────────────
189
190/// `math.abs(x)` — absolute value, preserving integer type when possible.
191///
192fn math_abs(state: &mut LuaState) -> Result<usize, LuaError> {
193    if arg_is_int(state, 1) {
194        let n = state.to_integer(1).unwrap_or(0);
195        let n = if n < 0 {
196            (0u64.wrapping_sub(n as u64)) as i64
197        } else {
198            n
199        };
200        state.push(LuaValue::Int(n));
201    } else {
202        let x = state.check_number(1)?;
203        state.push(LuaValue::Float(x.abs()));
204    }
205    Ok(1)
206}
207
208/// `math.sin(x)` — sine (radians).
209///
210fn math_sin(state: &mut LuaState) -> Result<usize, LuaError> {
211    let x = state.check_number(1)?;
212    state.push(LuaValue::Float(x.sin()));
213    Ok(1)
214}
215
216/// `math.cos(x)` — cosine (radians).
217///
218fn math_cos(state: &mut LuaState) -> Result<usize, LuaError> {
219    let x = state.check_number(1)?;
220    state.push(LuaValue::Float(x.cos()));
221    Ok(1)
222}
223
224/// `math.tan(x)` — tangent (radians).
225///
226fn math_tan(state: &mut LuaState) -> Result<usize, LuaError> {
227    let x = state.check_number(1)?;
228    state.push(LuaValue::Float(x.tan()));
229    Ok(1)
230}
231
232/// `math.asin(x)` — arc-sine, result in radians.
233///
234fn math_asin(state: &mut LuaState) -> Result<usize, LuaError> {
235    let x = state.check_number(1)?;
236    state.push(LuaValue::Float(x.asin()));
237    Ok(1)
238}
239
240/// `math.acos(x)` — arc-cosine, result in radians.
241///
242fn math_acos(state: &mut LuaState) -> Result<usize, LuaError> {
243    let x = state.check_number(1)?;
244    state.push(LuaValue::Float(x.acos()));
245    Ok(1)
246}
247
248/// `math.atan(y [, x])` — arc-tangent of y/x (defaults x=1), result in
249/// radians. Subsumes C's `atan2` when x is provided.
250///
251fn math_atan(state: &mut LuaState) -> Result<usize, LuaError> {
252    let y = state.check_number(1)?;
253    let x = state.opt_number(2, 1.0)?;
254    state.push(LuaValue::Float(y.atan2(x)));
255    Ok(1)
256}
257
258/// `math.cosh(x)` — hyperbolic cosine. Deprecated `LUA_COMPAT_MATHLIB`
259/// function, registered only under the 5.3 backend.
260///
261fn math_cosh(state: &mut LuaState) -> Result<usize, LuaError> {
262    let x = state.check_number(1)?;
263    state.push(LuaValue::Float(x.cosh()));
264    Ok(1)
265}
266
267/// `math.sinh(x)` — hyperbolic sine. Deprecated `LUA_COMPAT_MATHLIB`
268/// function, registered only under the 5.3 backend.
269///
270fn math_sinh(state: &mut LuaState) -> Result<usize, LuaError> {
271    let x = state.check_number(1)?;
272    state.push(LuaValue::Float(x.sinh()));
273    Ok(1)
274}
275
276/// `math.tanh(x)` — hyperbolic tangent. Deprecated `LUA_COMPAT_MATHLIB`
277/// function, registered only under the 5.3 backend.
278///
279fn math_tanh(state: &mut LuaState) -> Result<usize, LuaError> {
280    let x = state.check_number(1)?;
281    state.push(LuaValue::Float(x.tanh()));
282    Ok(1)
283}
284
285/// `math.pow(x, y)` — x raised to the power y, always returning a float.
286/// Deprecated `LUA_COMPAT_MATHLIB` function, registered only under the 5.3
287/// backend. Mirrors C `pow(luaL_checknumber, luaL_checknumber)`.
288///
289fn math_pow(state: &mut LuaState) -> Result<usize, LuaError> {
290    let x = state.check_number(1)?;
291    let y = state.check_number(2)?;
292    state.push(LuaValue::Float(x.powf(y)));
293    Ok(1)
294}
295
296/// `math.log10(x)` — base-10 logarithm. Deprecated `LUA_COMPAT_MATHLIB`
297/// function, registered only under the 5.3 backend.
298///
299fn math_log10(state: &mut LuaState) -> Result<usize, LuaError> {
300    let x = state.check_number(1)?;
301    state.push(LuaValue::Float(x.log10()));
302    Ok(1)
303}
304
305/// `math.ldexp(x, e)` — `x * 2^e`. Deprecated `LUA_COMPAT_MATHLIB` function,
306/// registered only under the 5.3 backend. The exponent is an integer argument
307/// truncated to C `int` range, matching `ldexp(x, (int)luaL_checkinteger)`.
308///
309fn math_ldexp(state: &mut LuaState) -> Result<usize, LuaError> {
310    let x = state.check_number(1)?;
311    let e = state.check_integer(2)? as i32;
312    state.push(LuaValue::Float(ldexp(x, e)));
313    Ok(1)
314}
315
316/// Pure `ldexp`: returns `x * 2^exp` with C `ldexp` semantics.
317///
318/// A naive `x * 2f64.powi(exp)` underflows (or overflows) the intermediate
319/// `2^exp` for large-magnitude exponents, losing subnormal results such as
320/// `ldexp(1.0, -1074) == 5e-324`. The scaling is therefore applied in bounded
321/// steps so no intermediate factor under/overflows: each step multiplies by a
322/// power of two whose magnitude stays inside the normal `f64` range.
323fn ldexp(x: f64, exp: i32) -> f64 {
324    if x == 0.0 || !x.is_finite() {
325        return x;
326    }
327    let mut result = x;
328    let mut e = exp;
329    // 2^1023 is the largest power of two representable as a normal f64; chunk
330    // the exponent so each `from_bits` factor is always finite and nonzero.
331    while e > 1023 {
332        result *= f64::from_bits(0x7feu64 << 52); // 2^1023
333        e -= 1023;
334    }
335    while e < -1022 {
336        result *= f64::from_bits(0x001u64 << 52); // 2^-1022 (smallest normal)
337        e += 1022;
338    }
339    result * f64::from_bits(((e + 1023) as u64) << 52)
340}
341
342/// `math.frexp(x)` — split x into a normalized mantissa and an exponent such
343/// that `x == mantissa * 2^exponent` with `0.5 <= |mantissa| < 1`. Returns the
344/// float mantissa followed by the **integer** exponent, matching C
345/// `frexp` + `lua_pushinteger`. Deprecated `LUA_COMPAT_MATHLIB` function,
346/// registered only under the 5.3 backend.
347///
348/// Rust std has no `frexp`; this replicates C `frexp` via `f64` bit
349/// manipulation, including the `frexp(0.0) == (0.0, 0)` special case (and the
350/// matching `-0.0`, infinity, and NaN cases, which C leaves unchanged with a
351/// zero exponent).
352fn math_frexp(state: &mut LuaState) -> Result<usize, LuaError> {
353    let x = state.check_number(1)?;
354    let (mantissa, exponent) = frexp(x);
355    state.push(LuaValue::Float(mantissa));
356    state.push(LuaValue::Int(exponent as i64));
357    Ok(2)
358}
359
360/// Pure `frexp`: returns `(mantissa, exponent)` with `x == mantissa * 2^exp`.
361///
362/// Replicates C `frexp` semantics for f64. Zero, infinity, and NaN are
363/// returned unchanged with a zero exponent.
364fn frexp(x: f64) -> (f64, i32) {
365    if x == 0.0 || !x.is_finite() {
366        return (x, 0);
367    }
368    let bits = x.to_bits();
369    let raw_exp = ((bits >> 52) & 0x7ff) as i32;
370    if raw_exp == 0 {
371        // Subnormal: scale up by 2^54 to normalize, then correct the exponent.
372        let (m, e) = frexp(x * (1u64 << 54) as f64);
373        return (m, e - 54);
374    }
375    // Bias the exponent so the mantissa lands in [0.5, 1): set the stored
376    // exponent field to 0x3fe (unbiased -1).
377    let exponent = raw_exp - 1022;
378    let mantissa_bits = (bits & !(0x7ffu64 << 52)) | (0x3feu64 << 52);
379    (f64::from_bits(mantissa_bits), exponent)
380}
381
382/// `math.tointeger(x)` — return `x` as an integer, or the fail value when it
383/// has no exact integer representation.
384///
385/// The fail value is `nil`, not `false`: the default 5.3/5.4/5.5 builds expand
386/// `luaL_pushfail` to a nil push (only a `LUA_FAILISFALSE` build would push
387/// `false`), and the oracle contract pins the nil.
388fn math_toint(state: &mut LuaState) -> Result<usize, LuaError> {
389    if let Some(n) = state.to_integer_opt(1) {
390        state.push(LuaValue::Int(n));
391    } else {
392        state.check_any(1)?;
393        state.push(LuaValue::Nil);
394    }
395    Ok(1)
396}
397
398/// `math.floor(x)` — largest integer ≤ x.
399///
400fn math_floor(state: &mut LuaState) -> Result<usize, LuaError> {
401    if arg_is_int(state, 1) {
402        keep_first_arg(state)?;
403    } else {
404        let d = state.check_number(1)?.floor();
405        push_int_or_float(state, d);
406    }
407    Ok(1)
408}
409
410/// `math.ceil(x)` — smallest integer ≥ x.
411///
412fn math_ceil(state: &mut LuaState) -> Result<usize, LuaError> {
413    if arg_is_int(state, 1) {
414        keep_first_arg(state)?;
415    } else {
416        let d = state.check_number(1)?.ceil();
417        push_int_or_float(state, d);
418    }
419    Ok(1)
420}
421
422/// `math.fmod(x, y)` — floating-point remainder (same sign as x).
423///
424fn math_fmod(state: &mut LuaState) -> Result<usize, LuaError> {
425    if arg_is_int(state, 1) && arg_is_int(state, 2) {
426        let a = state.to_integer(1).unwrap_or(0);
427        let d = state.to_integer(2).unwrap_or(0);
428        if (d as u64).wrapping_add(1) <= 1 {
429            if d == 0 {
430                return Err(lua_vm::debug::arg_error_impl(state, 2, b"zero"));
431            }
432            state.push(LuaValue::Int(0));
433        } else {
434            state.push(LuaValue::Int(a % d));
435        }
436    } else {
437        let x = state.check_number(1)?;
438        let y = state.check_number(2)?;
439        state.push(LuaValue::Float(x % y));
440    }
441    Ok(1)
442}
443
444/// `math.modf(x)` — split into integer and fractional parts; returns both.
445///
446/// The integer part is computed with `ceil`/`floor` + subtraction rather than
447/// libc `modf`, avoiding a `double *` out-parameter ABI. An integer argument is
448/// its own integer part with a `0.0` fractional part.
449fn math_modf(state: &mut LuaState) -> Result<usize, LuaError> {
450    if arg_is_int(state, 1) {
451        keep_first_arg(state)?;
452        state.push(LuaValue::Float(0.0));
453    } else {
454        let n = state.check_number(1)?;
455        let ip = if n < 0.0 { n.ceil() } else { n.floor() };
456        push_int_or_float(state, ip);
457        let frac = if n == ip { 0.0 } else { n - ip };
458        state.push(LuaValue::Float(frac));
459    }
460    Ok(2)
461}
462
463/// `math.sqrt(x)` — square root.
464///
465fn math_sqrt(state: &mut LuaState) -> Result<usize, LuaError> {
466    let x = state.check_number(1)?;
467    state.push(LuaValue::Float(x.sqrt()));
468    Ok(1)
469}
470
471/// `math.ult(m, n)` — unsigned less-than on integers.
472///
473fn math_ult(state: &mut LuaState) -> Result<usize, LuaError> {
474    let a = state.check_integer(1)?;
475    let b = state.check_integer(2)?;
476    state.push(LuaValue::Bool((a as u64) < (b as u64)));
477    Ok(1)
478}
479
480/// `math.log(x [, base])` — logarithm; natural if base omitted.
481///
482fn math_log(state: &mut LuaState) -> Result<usize, LuaError> {
483    let x = state.check_number(1)?;
484    // Lua 5.1's `math.log` takes a single argument and silently ignores any
485    // second; the two-argument base form is a 5.2 addition. Verified against
486    // lua5.1.5: `math.log(8,2) == math.log(8) == ln(8)`, and a second arg never
487    // errors. See specs/followup/5.1-roster-syntax.md §1.
488    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
489        state.push(LuaValue::Float(x.ln()));
490        return Ok(1);
491    }
492    let res = if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
493        x.ln()
494    } else {
495        let base = state.check_number(2)?;
496        if base == 2.0 {
497            x.log2()
498        } else if base == 10.0 {
499            x.log10()
500        } else {
501            x.ln() / base.ln()
502        }
503    };
504    state.push(LuaValue::Float(res));
505    Ok(1)
506}
507
508/// `math.exp(x)` — e raised to the power x.
509///
510fn math_exp(state: &mut LuaState) -> Result<usize, LuaError> {
511    let x = state.check_number(1)?;
512    state.push(LuaValue::Float(x.exp()));
513    Ok(1)
514}
515
516/// `math.deg(x)` — convert radians to degrees.
517///
518fn math_deg(state: &mut LuaState) -> Result<usize, LuaError> {
519    let x = state.check_number(1)?;
520    state.push(LuaValue::Float(x * (180.0 / PI)));
521    Ok(1)
522}
523
524/// `math.rad(x)` — convert degrees to radians.
525///
526fn math_rad(state: &mut LuaState) -> Result<usize, LuaError> {
527    let x = state.check_number(1)?;
528    state.push(LuaValue::Float(x * (PI / 180.0)));
529    Ok(1)
530}
531
532/// Whether `math.max`/`math.min` use the float-only `luaL_checknumber` path.
533///
534/// 5.1 and 5.2 coerce every argument with `luaL_checknumber`, comparing
535/// `lua_Number` doubles and returning the coerced number — so a non-number
536/// argument raises `number expected, got <type>` and a number-shaped string
537/// argument is accepted and returned as a number. 5.3+ rewrote both to use
538/// `lua_compare(..., LUA_OPLT)`, which compares the values directly (so strings
539/// compare lexicographically and are returned unchanged). 5.4 is the
540/// unchangeable baseline, so the float-only path is gated to V51/V52 only.
541fn max_min_is_float_only(state: &LuaState) -> bool {
542    matches!(
543        state.global().lua_version,
544        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
545    )
546}
547
548/// `math.min(x, ...)` — minimum of all arguments.
549///
550/// On 5.1/5.2 every argument is coerced via `luaL_checknumber` and the smallest
551/// number is returned (see [`max_min_is_float_only`]); on 5.3+ the smallest
552/// argument by Lua `<` comparison is returned unchanged.
553fn math_min(state: &mut LuaState) -> Result<usize, LuaError> {
554    let n = state.get_top();
555    if n < 1 {
556        return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
557    }
558    if max_min_is_float_only(state) {
559        let mut dmin = state.check_number(1)?;
560        for i in 2..=n {
561            let d = state.check_number(i)?;
562            if d < dmin {
563                dmin = d;
564            }
565        }
566        state.push(LuaValue::Float(dmin));
567        return Ok(1);
568    }
569    let mut imin: i32 = 1;
570    for i in 2..=n {
571        if state.compare_lt(i, imin)? {
572            imin = i;
573        }
574    }
575    state.push_value(imin)?;
576    Ok(1)
577}
578
579/// `math.max(x, ...)` — maximum of all arguments.
580///
581/// On 5.1/5.2 every argument is coerced via `luaL_checknumber` and the largest
582/// number is returned (see [`max_min_is_float_only`]); on 5.3+ the largest
583/// argument by Lua `<` comparison is returned unchanged.
584fn math_max(state: &mut LuaState) -> Result<usize, LuaError> {
585    let n = state.get_top();
586    if n < 1 {
587        return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
588    }
589    if max_min_is_float_only(state) {
590        let mut dmax = state.check_number(1)?;
591        for i in 2..=n {
592            let d = state.check_number(i)?;
593            if d > dmax {
594                dmax = d;
595            }
596        }
597        state.push(LuaValue::Float(dmax));
598        return Ok(1);
599    }
600    let mut imax: i32 = 1;
601    for i in 2..=n {
602        if state.compare_lt(imax, i)? {
603            imax = i;
604        }
605    }
606    state.push_value(imax)?;
607    Ok(1)
608}
609
610/// `math.type(x)` — return `"integer"`, `"float"`, or the fail value for
611/// non-numbers.
612///
613/// The non-number fail value is `nil`, not `false`, for the same reason as
614/// [`math_toint`]: `luaL_pushfail` pushes nil in the default builds and the
615/// oracle contract pins the nil.
616fn math_type(state: &mut LuaState) -> Result<usize, LuaError> {
617    if matches!(state.type_at(1), LuaType::Number) {
618        if arg_is_int(state, 1) {
619            state.push_string(b"integer")?;
620        } else {
621            state.push_string(b"float")?;
622        }
623    } else {
624        state.check_any(1)?;
625        state.push(LuaValue::Nil);
626    }
627    Ok(1)
628}
629
630// ── PRNG-backed Lua functions ─────────────────────────────────────────────
631
632/// `math.random([m [, n]])` — pseudo-random number generation.
633///
634/// With no arguments: float in `[0, 1)`. With one argument `n`: integer in
635/// `[1, n]` (or a full-range `u64` when `n == 0`, on 5.4/5.5). With two
636/// arguments `m, n`: integer in `[m, n]`. The PRNG word is advanced first, then
637/// the arguments are read via a separate borrow.
638///
639/// TODO: the PRNG state is a thread-local rather than per-`lua_State` typed
640/// userdata in closure upvalue 1; migrating it to `GcRef<RefCell<RanState>>`
641/// (so each state has its own generator) is deferred to the upvalue-userdata
642/// work, not changed here.
643fn math_random(state: &mut LuaState) -> Result<usize, LuaError> {
644    let rv = advance_prng(state)?;
645    let n_args = state.get_top();
646
647    if n_args == 0 {
648        state.push(LuaValue::Float(xoshiro::rand_to_float(rv)));
649        return Ok(1);
650    }
651
652    let version = state.global().lua_version;
653    let is_v53 = version == lua_types::LuaVersion::V53;
654    // 5.1/5.2 are float-only and use the C `rand()` contract: there is no
655    // `random(0)` full-range special case (that is a 5.4/5.5 addition), the
656    // empty-interval error for `random(m, n)` reports argument index 2 (the
657    // upper bound), and integer-valued results are pushed as `Float` to honour
658    // the never-construct-`Int` invariant under `FloatOnly`. See
659    // specs/followup/5.1-numbers-prng.md §"Impl seams".
660    let float_only = version.number_model() == lua_types::NumberModel::FloatOnly;
661
662    let (low, up, empty_arg) = match n_args {
663        1 => {
664            let up = state.check_integer(1)?;
665            // 5.4/5.5 `random(0)` returns a full-range integer; 5.1/5.2/5.3 have
666            // no such special case — it is `[1, 0]`, an empty interval.
667            if up == 0 && !is_v53 && !float_only {
668                state.push(LuaValue::Int(rv as i64));
669                return Ok(1);
670            }
671            (1i64, up, 1)
672        }
673        2 => {
674            let low = state.check_integer(1)?;
675            let up = state.check_integer(2)?;
676            // 5.1's `luaL_checkint(L, 2)` for the upper bound means its
677            // empty-interval `luaL_argerror` reports argument #2; the modern
678            // bodies report #1.
679            let empty_arg = if float_only { 2 } else { 1 };
680            (low, up, empty_arg)
681        }
682        _ => {
683            return Err(LuaError::runtime(format_args!("wrong number of arguments")));
684        }
685    };
686
687    if low > up {
688        return Err(lua_vm::debug::arg_error_impl(
689            state,
690            empty_arg,
691            b"interval is empty",
692        ));
693    }
694
695    // 5.3 `math_random` rejects intervals whose width overflows a signed integer
696    // (`low >= 0 || up <= LUA_MAXINTEGER + low`). 5.4/5.5 use the `project`
697    // bit-mask algorithm, which handles the full range without erroring.
698    if is_v53 && !(low >= 0 || up <= i64::MAX.wrapping_add(low)) {
699        return Err(lua_vm::debug::arg_error_impl(
700            state,
701            1,
702            b"interval too large",
703        ));
704    }
705
706    let range = (up as u64).wrapping_sub(low as u64);
707    let p = project_from_upvalue(state, rv, range)?;
708    let result = (p as u64).wrapping_add(low as u64) as i64;
709    if float_only {
710        state.push(LuaValue::Float(result as f64));
711    } else {
712        state.push(LuaValue::Int(result));
713    }
714    Ok(1)
715}
716
717/// `math.randomseed([x [, y]])` — seed the PRNG.
718///
719/// The return shape and auto-seed behavior are version-gated and load-bearing:
720/// - 5.1/5.2 (float-only) REQUIRE the seed argument (a missing one raises
721///   "number expected, got no value"), take a single seed word, and return NO
722///   values.
723/// - 5.3+ auto-seed from host entropy when the argument is absent and return the
724///   two seed words.
725///
726/// See `specs/followup/5.1-numbers-prng.md`.
727fn math_randomseed(state: &mut LuaState) -> Result<usize, LuaError> {
728    let float_only = state.global().lua_version.number_model() == lua_types::NumberModel::FloatOnly;
729
730    if matches!(state.type_at(1), LuaType::None) {
731        if float_only {
732            let n1 = state.check_integer(1)? as u64;
733            apply_set_seed_quiet(state, n1, 0);
734            return Ok(0);
735        }
736        apply_random_seed(state)?;
737    } else {
738        let n1 = state.check_integer(1)? as u64;
739        if float_only {
740            apply_set_seed_quiet(state, n1, 0);
741            return Ok(0);
742        }
743        let n2 = state.opt_integer(2, 0)? as u64;
744        apply_set_seed(state, n1, n2)?;
745    }
746    Ok(2)
747}
748
749/// Advance the PRNG in the thread-local [`RAN_STATE`] and return the raw 64-bit
750/// output.
751///
752/// The thread-local is the only difference from the C source, which draws from
753/// userdata in closure upvalue 1; the [`next_rand`] algorithm is identical (see
754/// [`math_random`] for the deferred per-state migration).
755fn advance_prng(_state: &mut LuaState) -> Result<u64, LuaError> {
756    Ok(RAN_STATE.with(|r| xoshiro::next_rand(&mut r.borrow_mut().s)))
757}
758
759/// Project a raw draw into `[0, n]` using the thread-local PRNG for any
760/// rejection redraws (see [`advance_prng`] for the thread-local rationale).
761fn project_from_upvalue(_state: &mut LuaState, ran: u64, n: u64) -> Result<u64, LuaError> {
762    Ok(RAN_STATE.with(|r| xoshiro::project(ran, n, &mut r.borrow_mut().s)))
763}
764
765/// Seed the PRNG from the host entropy hook (the 5.3+ auto-seed path).
766///
767/// The second seed word is derived deterministically from the entropy value;
768/// the C source additionally mixes address entropy, which is deferred until a
769/// richer host entropy API exists.
770fn apply_random_seed(state: &mut LuaState) -> Result<(), LuaError> {
771    let entropy = state.global().entropy_hook.map(|hook| hook()).unwrap_or(0);
772    let seed1 = entropy;
773    let seed2: u64 = entropy.rotate_left(17) ^ 0x9e37_79b9_7f4a_7c15;
774    apply_set_seed(state, seed1, seed2)
775}
776
777/// Apply explicit seeds to the thread-local PRNG and push them onto the stack
778/// (the 5.3+ `randomseed` return shape — the two seed words).
779fn apply_set_seed(state: &mut LuaState, n1: u64, n2: u64) -> Result<(), LuaError> {
780    RAN_STATE.with(|r| xoshiro::set_seed_words(&mut r.borrow_mut().s, n1, n2));
781    state.push(LuaValue::Int(n1 as i64));
782    state.push(LuaValue::Int(n2 as i64));
783    Ok(())
784}
785
786/// Seed the PRNG without pushing the seed words onto the stack.
787///
788/// 5.1/5.2 `math.randomseed` returns no values, so its seeding path must not
789/// push (unlike the modern [`apply_set_seed`], which returns the two words).
790fn apply_set_seed_quiet(_state: &mut LuaState, n1: u64, n2: u64) {
791    RAN_STATE.with(|r| xoshiro::set_seed_words(&mut r.borrow_mut().s, n1, n2));
792}
793
794/// Register `math.random` and `math.randomseed` on the math library table at
795/// stack top, after seeding the PRNG.
796///
797/// C binds the PRNG state as upvalue 1 of both closures; with the PRNG in a
798/// thread-local ([`RAN_STATE`]) these are registered as plain (non-closure)
799/// entries instead.
800fn set_rand_func(state: &mut LuaState) -> Result<(), LuaError> {
801    apply_random_seed(state)?;
802    state.pop_n(2);
803
804    state.push_c_function(math_random)?;
805    state.set_field(-2, b"random")?;
806    state.push_c_function(math_randomseed)?;
807    state.set_field(-2, b"randomseed")?;
808    Ok(())
809}
810
811// ── Library registration table ────────────────────────────────────────────
812
813static MATHLIB_FUNCS: &[(&[u8], LuaCFunction)] = &[
814    (b"abs", math_abs),
815    (b"acos", math_acos),
816    (b"asin", math_asin),
817    (b"atan", math_atan),
818    (b"ceil", math_ceil),
819    (b"cos", math_cos),
820    (b"deg", math_deg),
821    (b"exp", math_exp),
822    (b"tointeger", math_toint),
823    (b"floor", math_floor),
824    (b"fmod", math_fmod),
825    (b"ult", math_ult),
826    (b"log", math_log),
827    (b"max", math_max),
828    (b"min", math_min),
829    (b"modf", math_modf),
830    (b"rad", math_rad),
831    (b"sin", math_sin),
832    (b"sqrt", math_sqrt),
833    (b"tan", math_tan),
834    (b"type", math_type),
835    // `frexp`/`ldexp` survive into 5.5, unlike the rest of the compat roster:
836    // they are registered unconditionally on 5.3/5.4/5.5. Verified against all
837    // three reference binaries (`type(math.frexp)`/`type(math.ldexp)` ==
838    // "function" on 5.3.6, 5.4.7, 5.5.0).
839    (b"frexp", math_frexp),
840    (b"ldexp", math_ldexp),
841];
842
843// ── Module entry point ────────────────────────────────────────────────────
844
845/// Open the `math` library: create the table, register the version-agnostic
846/// functions and the per-version roster delta, populate the constants, and wire
847/// up the PRNG functions.
848pub fn luaopen_math(state: &mut LuaState) -> Result<usize, LuaError> {
849    state.new_lib(MATHLIB_FUNCS)?;
850
851    // Per-version roster delta: the `LUA_COMPAT_MATHLIB`-gated functions
852    // (`atan2` as an alias of `math_atan`, plus cosh/sinh/tanh/pow/log10) ship
853    // in the default lua5.3.6 build (`LUA_COMPAT_MATHLIB` on) AND the default
854    // lua5.4.7 build (its `LUA_COMPAT_5_3` umbrella turns `LUA_COMPAT_MATHLIB`
855    // on), but were dropped in lua5.5.0 (macro commented out). Verified by
856    // probing all three reference binaries directly. `frexp`/`ldexp` are NOT in
857    // this set — they survive into 5.5 and live in the agnostic roster above.
858    // `new_lib` leaves the new table on the stack top, so we register into it
859    // directly. See `specs/followup/5.3-math.md` (whose 5.4/5.5-absence claim
860    // is corrected here against the binaries, the binding oracle).
861    // The `LUA_COMPAT_MATHLIB` deprecated roster also ships in the default
862    // lua5.2.4 build (verified against the reference binary: `type(math.atan2)`
863    // etc. == "function" on 5.2.4). 5.5 drops them.
864    if matches!(
865        state.global().lua_version,
866        lua_types::LuaVersion::V51
867            | lua_types::LuaVersion::V52
868            | lua_types::LuaVersion::V53
869            | lua_types::LuaVersion::V54
870    ) {
871        const COMPAT_MATH_FUNCS: &[(&[u8], LuaCFunction)] = &[
872            (b"atan2", math_atan),
873            (b"cosh", math_cosh),
874            (b"sinh", math_sinh),
875            (b"tanh", math_tanh),
876            (b"pow", math_pow),
877            (b"log10", math_log10),
878        ];
879        state.set_funcs_with_upvalues(COMPAT_MATH_FUNCS, 0)?;
880    }
881
882    // Lua 5.1 carries `math.mod`, a compat alias of `fmod` predating the rename
883    // (`math.mod(7,3) == 1`). It was removed in 5.2. Verified against
884    // lua5.1.5: `type(math.mod)` == "function". See
885    // specs/followup/5.1-roster-syntax.md §1.
886    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
887        state.push_c_function(math_fmod)?;
888        state.set_field(-2, b"mod")?;
889    }
890
891    state.push(LuaValue::Float(PI));
892    state.set_field(-2, b"pi")?;
893
894    state.push(LuaValue::Float(f64::INFINITY));
895    state.set_field(-2, b"huge")?;
896
897    // LUA_MAXINTEGER = i64::MAX (lua_Integer is int64_t in default config).
898    state.push(LuaValue::Int(i64::MAX));
899    state.set_field(-2, b"maxinteger")?;
900
901    state.push(LuaValue::Int(i64::MIN));
902    state.set_field(-2, b"mininteger")?;
903
904    // Lua 5.1/5.2 are float-only: the integer-subtype helpers (`math.type`,
905    // `math.tointeger`, `math.ult`) and the integer bounds
906    // (`math.maxinteger`/`mininteger`) are 5.3 additions and are absent there.
907    // Verified against lua5.2.4: each is `nil`.
908    if matches!(
909        state.global().lua_version,
910        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
911    ) {
912        for field in [
913            &b"type"[..],
914            &b"tointeger"[..],
915            &b"ult"[..],
916            &b"maxinteger"[..],
917            &b"mininteger"[..],
918        ] {
919            state.push(LuaValue::Nil);
920            state.set_field(-2, field)?;
921        }
922    }
923
924    set_rand_func(state)?;
925
926    Ok(1)
927}