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(LuaError::arg_error(2, "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    let res = if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
473        x.ln()
474    } else {
475        let base = state.check_number(2)?;
476        if base == 2.0 {
477            x.log2()
478        } else if base == 10.0 {
479            x.log10()
480        } else {
481            x.ln() / base.ln()
482        }
483    };
484    state.push(LuaValue::Float(res));
485    Ok(1)
486}
487
488/// `math.exp(x)` — e raised to the power x.
489///
490fn math_exp(state: &mut LuaState) -> Result<usize, LuaError> {
491    let x = state.check_number(1)?;
492    state.push(LuaValue::Float(x.exp()));
493    Ok(1)
494}
495
496/// `math.deg(x)` — convert radians to degrees.
497///
498fn math_deg(state: &mut LuaState) -> Result<usize, LuaError> {
499    let x = state.check_number(1)?;
500    state.push(LuaValue::Float(x * (180.0 / PI)));
501    Ok(1)
502}
503
504/// `math.rad(x)` — convert degrees to radians.
505///
506fn math_rad(state: &mut LuaState) -> Result<usize, LuaError> {
507    let x = state.check_number(1)?;
508    state.push(LuaValue::Float(x * (PI / 180.0)));
509    Ok(1)
510}
511
512/// `math.min(x, ...)` — minimum of all arguments (uses Lua `<` comparison).
513///
514fn math_min(state: &mut LuaState) -> Result<usize, LuaError> {
515    let n = state.get_top();
516    let mut imin: i32 = 1;
517    if n < 1 {
518        return Err(LuaError::arg_error(1, "value expected"));
519    }
520    for i in 2..=n {
521        if state.compare_lt(i, imin)? {
522            imin = i;
523        }
524    }
525    state.push_value(imin)?;
526    Ok(1)
527}
528
529/// `math.max(x, ...)` — maximum of all arguments (uses Lua `<` comparison).
530///
531fn math_max(state: &mut LuaState) -> Result<usize, LuaError> {
532    let n = state.get_top();
533    let mut imax: i32 = 1;
534    if n < 1 {
535        return Err(LuaError::arg_error(1, "value expected"));
536    }
537    for i in 2..=n {
538        if state.compare_lt(imax, i)? {
539            imax = i;
540        }
541    }
542    state.push_value(imax)?;
543    Ok(1)
544}
545
546/// `math.type(x)` — return `"integer"`, `"float"`, or nil for non-numbers.
547///
548fn math_type(state: &mut LuaState) -> Result<usize, LuaError> {
549    if matches!(state.type_at(1), LuaType::Number) {
550        if matches!(state.value_at(1), LuaValue::Int(_)) {
551            state.push_string(b"integer")?;
552        } else {
553            state.push_string(b"float")?;
554        }
555    } else {
556        state.check_any(1)?;
557        // luaL_pushfail expands to lua_pushnil in the default 5.3/5.4/5.5
558        // builds; only a LUA_FAILISFALSE build pushes false, which the oracle
559        // contract pins off.
560        state.push(LuaValue::Nil);
561    }
562    Ok(1)
563}
564
565// ── PRNG-backed Lua functions ─────────────────────────────────────────────
566
567/// `math.random([m [, n]])` — pseudo-random number generation.
568///
569///
570/// With no arguments: float in [0, 1).
571/// With one argument n: integer in [1, n] (or full random u64 if n == 0).
572/// With two arguments m, n: integer in [m, n].
573fn math_random(state: &mut LuaState) -> Result<usize, LuaError> {
574    // TODO(port): RanState is stored as typed userdata in closure upvalue 1.
575    // Phase B must implement `state.upvalue_userdata_mut::<RanState>(1)` using
576    // interior mutability (e.g. GcRef<RefCell<RanState>>) to avoid the borrow
577    // conflict between &mut RanState and subsequent &mut LuaState push calls.
578    //
579    // For Phase A: advance PRNG and get args via separate borrows.
580    let rv = advance_prng(state)?;
581    let n_args = state.get_top();
582
583    if n_args == 0 {
584        state.push(LuaValue::Float(rand_to_float(rv)));
585        return Ok(1);
586    }
587
588    let (low, up) = match n_args {
589        1 => {
590            let up = state.check_integer(1)?;
591            if up == 0 {
592                // I2UInt(rv) = rv (trivial for u64)
593                state.push(LuaValue::Int(rv as i64));
594                return Ok(1);
595            }
596            (1i64, up)
597        }
598        2 => {
599            let low = state.check_integer(1)?;
600            let up = state.check_integer(2)?;
601            (low, up)
602        }
603        _ => {
604            return Err(LuaError::runtime(format_args!(
605                "wrong number of arguments"
606            )));
607        }
608    };
609
610    if low > up {
611        return Err(LuaError::arg_error(1, "interval is empty"));
612    }
613
614    let range = (up as u64).wrapping_sub(low as u64);
615    let p = project_from_upvalue(state, rv, range)?;
616    state.push(LuaValue::Int((p as u64).wrapping_add(low as u64) as i64));
617    Ok(1)
618}
619
620/// `math.randomseed([x [, y]])` — seed the PRNG; returns two seed values.
621///
622fn math_randomseed(state: &mut LuaState) -> Result<usize, LuaError> {
623    // TODO(port): same upvalue userdata access issue as math_random.
624    if matches!(state.type_at(1), LuaType::None) {
625        // randseed uses time(NULL) and address of L for entropy.
626        apply_random_seed(state)?;
627    } else {
628        //    lua_Integer n2 = luaL_optinteger(L, 2, 0);
629        let n1 = state.check_integer(1)? as u64;
630        let n2 = state.opt_integer(2, 0)? as u64;
631        apply_set_seed(state, n1, n2)?;
632    }
633    Ok(2)
634}
635
636/// Advance the PRNG stored in the thread-local `RAN_STATE` and return the
637/// raw 64-bit output.
638///
639/// PORT NOTE: In C this draws from the userdata in closure upvalue 1. The
640/// Rust port stores the PRNG state in a thread-local until typed-userdata
641/// closure upvalues are wired up. Storage location is the only difference;
642/// the algorithm is unchanged.
643fn advance_prng(_state: &mut LuaState) -> Result<u64, LuaError> {
644    Ok(RAN_STATE.with(|r| next_rand(&mut r.borrow_mut().s)))
645}
646
647/// Apply rejection sampling for `math.random` using the thread-local PRNG.
648///
649/// PORT NOTE: see `advance_prng` for the thread-local rationale.
650fn project_from_upvalue(
651    _state: &mut LuaState,
652    ran: u64,
653    n: u64,
654) -> Result<u64, LuaError> {
655    Ok(RAN_STATE.with(|r| project(ran, n, &mut r.borrow_mut().s)))
656}
657
658/// Seed the PRNG from wall-clock time (entropy source).
659///
660///
661/// TODO(port): must write n1 and n2 back to the upvalue RanState.
662fn apply_random_seed(state: &mut LuaState) -> Result<(), LuaError> {
663    let entropy = state.global().entropy_hook.map(|hook| hook()).unwrap_or(0);
664    let seed1 = entropy;
665    // TODO(port): C also mixes address entropy; keep the second seed derived
666    // deterministically unless a richer host entropy API is added.
667    let seed2: u64 = entropy.rotate_left(17) ^ 0x9e37_79b9_7f4a_7c15;
668    apply_set_seed(state, seed1, seed2)
669}
670
671/// Apply explicit seeds to the PRNG and push them onto the stack.
672///
673///
674/// PORT NOTE: writes seeds into the thread-local RanState (see `advance_prng`).
675fn apply_set_seed(state: &mut LuaState, n1: u64, n2: u64) -> Result<(), LuaError> {
676    RAN_STATE.with(|r| set_seed_words(&mut r.borrow_mut().s, n1, n2));
677    state.push(LuaValue::Int(n1 as i64));
678    state.push(LuaValue::Int(n2 as i64));
679    Ok(())
680}
681
682/// Register `math.random` and `math.randomseed` on the math library table at
683/// stack top, after seeding the thread-local PRNG.
684///
685///
686/// PORT NOTE: C stores the PRNG inside a userdata bound as upvalue 1 of both
687/// closures. Until typed userdata closure upvalues are available, the Rust
688/// port keeps the PRNG in a thread-local (see `RAN_STATE`) and registers the
689/// functions as plain non-closure entries on the library table.
690fn set_rand_func(state: &mut LuaState) -> Result<(), LuaError> {
691    apply_random_seed(state)?;
692    state.pop_n(2);
693
694    state.push_c_function(math_random)?;
695    state.set_field(-2, b"random")?;
696    state.push_c_function(math_randomseed)?;
697    state.set_field(-2, b"randomseed")?;
698    Ok(())
699}
700
701// ── Library registration table ────────────────────────────────────────────
702
703/// The `math` library function table.
704///
705///
706/// Placeholder entries (`None`) are filled in manually by `luaopen_math`
707/// (`pi`, `huge`, `maxinteger`, `mininteger`) or by `set_rand_func`
708/// (`random`, `randomseed`).
709#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
710static MATHLIB: &[LibReg] = &[
711    LibReg { name: b"abs",        func: Some(math_abs)    },
712    LibReg { name: b"acos",       func: Some(math_acos)   },
713    LibReg { name: b"asin",       func: Some(math_asin)   },
714    LibReg { name: b"atan",       func: Some(math_atan)   },
715    LibReg { name: b"ceil",       func: Some(math_ceil)   },
716    LibReg { name: b"cos",        func: Some(math_cos)    },
717    LibReg { name: b"deg",        func: Some(math_deg)    },
718    LibReg { name: b"exp",        func: Some(math_exp)    },
719    LibReg { name: b"tointeger",  func: Some(math_toint)  },
720    LibReg { name: b"floor",      func: Some(math_floor)  },
721    LibReg { name: b"fmod",       func: Some(math_fmod)   },
722    LibReg { name: b"ult",        func: Some(math_ult)    },
723    LibReg { name: b"log",        func: Some(math_log)    },
724    LibReg { name: b"max",        func: Some(math_max)    },
725    LibReg { name: b"min",        func: Some(math_min)    },
726    LibReg { name: b"modf",       func: Some(math_modf)   },
727    LibReg { name: b"rad",        func: Some(math_rad)    },
728    LibReg { name: b"sin",        func: Some(math_sin)    },
729    LibReg { name: b"sqrt",       func: Some(math_sqrt)   },
730    LibReg { name: b"tan",        func: Some(math_tan)    },
731    LibReg { name: b"type",       func: Some(math_type)   },
732    // Placeholders; values are set manually in luaopen_math / set_rand_func.
733    LibReg { name: b"random",     func: None },
734    LibReg { name: b"randomseed", func: None },
735    LibReg { name: b"pi",         func: None },
736    LibReg { name: b"huge",       func: None },
737    LibReg { name: b"maxinteger", func: None },
738    LibReg { name: b"mininteger", func: None },
739];
740
741static MATHLIB_FUNCS: &[(&[u8], LuaCFunction)] = &[
742    (b"abs",        math_abs),
743    (b"acos",       math_acos),
744    (b"asin",       math_asin),
745    (b"atan",       math_atan),
746    (b"ceil",       math_ceil),
747    (b"cos",        math_cos),
748    (b"deg",        math_deg),
749    (b"exp",        math_exp),
750    (b"tointeger",  math_toint),
751    (b"floor",      math_floor),
752    (b"fmod",       math_fmod),
753    (b"ult",        math_ult),
754    (b"log",        math_log),
755    (b"max",        math_max),
756    (b"min",        math_min),
757    (b"modf",       math_modf),
758    (b"rad",        math_rad),
759    (b"sin",        math_sin),
760    (b"sqrt",       math_sqrt),
761    (b"tan",        math_tan),
762    (b"type",       math_type),
763    // `frexp`/`ldexp` are registered unconditionally in lua5.4.7 and lua5.5.0
764    // (their `lmathlib.c` places these two outside the `LUA_COMPAT_MATHLIB`
765    // `#if`) and are part of the 5.3 compat roster too. Verified against all
766    // three reference binaries: `type(math.frexp)`/`type(math.ldexp)` ==
767    // "function" on 5.3.6, 5.4.7, and 5.5.0.
768    (b"frexp",      math_frexp),
769    (b"ldexp",      math_ldexp),
770];
771
772// ── Module entry point ────────────────────────────────────────────────────
773
774/// Open the `math` library: create the table, populate constants, register
775/// the PRNG functions with their shared `RanState` upvalue.
776///
777///
778/// `LUAMOD_API` → `pub` (see macros.tsv).
779pub fn luaopen_math(state: &mut LuaState) -> Result<usize, LuaError> {
780    // Creates a new table and registers all non-None entries from MATHLIB.
781    state.new_lib(MATHLIB_FUNCS)?;
782
783    // Per-version roster delta: the `LUA_COMPAT_MATHLIB`-gated functions
784    // (`atan2` as an alias of `math_atan`, plus cosh/sinh/tanh/pow/log10) ship
785    // in the default lua5.3.6 build (`LUA_COMPAT_MATHLIB` on) AND the default
786    // lua5.4.7 build (its `LUA_COMPAT_5_3` umbrella turns `LUA_COMPAT_MATHLIB`
787    // on), but were dropped in lua5.5.0 (macro commented out). Verified by
788    // probing all three reference binaries directly. `frexp`/`ldexp` are NOT in
789    // this set — they survive into 5.5 and live in the agnostic roster above.
790    // `new_lib` leaves the new table on the stack top, so we register into it
791    // directly. See `specs/followup/5.3-math.md` (whose 5.4/5.5-absence claim
792    // is corrected here against the binaries, the binding oracle).
793    if matches!(
794        state.global().lua_version,
795        lua_types::LuaVersion::V53 | lua_types::LuaVersion::V54
796    ) {
797        const COMPAT_MATH_FUNCS: &[(&[u8], LuaCFunction)] = &[
798            (b"atan2",  math_atan),
799            (b"cosh",   math_cosh),
800            (b"sinh",   math_sinh),
801            (b"tanh",   math_tanh),
802            (b"pow",    math_pow),
803            (b"log10",  math_log10),
804        ];
805        state.set_funcs_with_upvalues(COMPAT_MATH_FUNCS, 0)?;
806    }
807
808    state.push(LuaValue::Float(PI));
809    state.set_field(-2, b"pi")?;
810
811    state.push(LuaValue::Float(f64::INFINITY));
812    state.set_field(-2, b"huge")?;
813
814    // LUA_MAXINTEGER = i64::MAX (lua_Integer is int64_t in default config).
815    state.push(LuaValue::Int(i64::MAX));
816    state.set_field(-2, b"maxinteger")?;
817
818    state.push(LuaValue::Int(i64::MIN));
819    state.set_field(-2, b"mininteger")?;
820
821    // Registers math.random and math.randomseed as upvalue-bearing closures.
822    set_rand_func(state)?;
823
824    Ok(1)
825}
826
827// ──────────────────────────────────────────────────────────────────────────
828// PORT STATUS
829//   source:        src/lmathlib.c  (782 lines, 28 functions)
830//   target_crate:  lua-stdlib
831//   confidence:    medium
832//   todos:         16
833//   port_notes:    8
834//   unsafe_blocks: 0
835//   notes:         All basic math functions are mechanically faithful. The
836//                  PRNG xoshiro256** algorithm is correctly translated using
837//                  native u64 (only the 64-bit code path; the 32-bit fallback
838//                  is dropped). The main Phase-B work is wiring up the upvalue
839//                  RanState userdata: advance_prng, project_from_upvalue,
840//                  apply_random_seed, apply_set_seed, and set_rand_func all
841//                  carry TODO(port) stubs where typed userdata + interior
842//                  mutability (RefCell) is required to avoid borrow conflicts.
843//                  Deprecated LUA_COMPAT_MATHLIB functions (cosh, sinh, tanh,
844//                  pow, log10, ldexp, frexp, atan2) are registered only under
845//                  the 5.3 backend per specs/followup/5.3-math.md; absent in
846//                  5.4/5.5. atan2 reuses math_atan; frexp is implemented via
847//                  f64 bit manipulation (no Rust std frexp).
848//                  state.new_lib, state.set_field,
849//                  state.compare_lt, state.push_value, state.opt_number,
850//                  state.opt_integer, state.check_integer, state.check_number,
851//                  state.check_any, state.to_integer_opt, state.get_top,
852//                  state.set_top, state.pop_n API names assumed; Phase B
853//                  will reconcile with the actual LuaState impl.
854// ──────────────────────────────────────────────────────────────────────────