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// ──────────────────────────────────────────────────────────────────────────