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