Skip to main content

lua_stdlib/
math_lib.rs

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