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