[Space Lua]: Implement math.random and math.randomseed (#1845)

This commit is contained in:
Matouš Jan Fialka
2026-02-21 17:56:49 +01:00
committed by GitHub
parent 472ff93416
commit 3bb80ece3f
3 changed files with 278 additions and 44 deletions
+18 -44
View File
@@ -5,6 +5,10 @@ import {
LuaTable,
} from "../runtime.ts";
import { isNegativeZero, isTaggedFloat } from "../numeric.ts";
import { LuaPRNG } from "./prng.ts";
// One PRNG per module load, auto-seeded at startup
const prng = new LuaPRNG();
// Fast unwrap: avoids function call overhead for the common plain-number case
function untagNumber(x: any): number {
@@ -52,51 +56,21 @@ export const mathApi = new LuaTable({
random: new LuaBuiltinFunction((_sf, m?: number, n?: number) => {
if (m !== undefined) m = untagNumber(m);
if (n !== undefined) n = untagNumber(n);
if (m === undefined && n === undefined) {
return Math.random();
try {
return prng.random(m, n);
} catch (e: any) {
throw new LuaRuntimeError(e.message, _sf);
}
if (!Number.isInteger(m)) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (integer expected)",
_sf,
);
}
if (n === undefined) {
if (m! == 0) {
const high = Math.floor(Math.random() * 0x100000000);
const low = Math.floor(Math.random() * 0x100000000);
let result = (BigInt(high) << 32n) | BigInt(low);
if (result & (1n << 63n)) {
result -= 1n << 64n;
}
return result;
}
if (m! < 1) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (interval is empty)",
_sf,
);
}
return Math.floor(Math.random() * m!) + 1;
}
if (!Number.isInteger(n!)) {
throw new LuaRuntimeError(
"bad argument #2 to 'math.random' (integer expected)",
_sf,
);
}
if (n! < m!) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (interval is empty)",
_sf,
);
}
return Math.floor(Math.random() * (n! - m! + 1)) + m!;
}),
/**
* Seeds the pseudo-random generator. With no arguments, uses a
* time-based seed. Returns the two seed integers used (Lua 5.4 contract).
*/
randomseed: new LuaBuiltinFunction((_sf, x?: number, y?: number) => {
if (x !== undefined) x = untagNumber(x);
if (y !== undefined) y = untagNumber(y);
const [s1, s2] = prng.randomseed(x, y);
return new LuaMultiRes([s1, s2]);
}),
// Basic functions
+115
View File
@@ -25,6 +25,13 @@ local function assertClose(a, b, eps, msg)
end
end
local function assertError(fn, msg)
local ok, err = pcall(fn)
if ok then
error((msg or "assertError failed") .. ": expected an error but none was raised")
end
end
-- math.type (basic)
do
assertEquals(math.type(0), "integer", "math.type(0)")
@@ -161,3 +168,111 @@ do
assertEquals(f2, 3, "floor(8*0.49)")
assertEquals(f3, 4, "floor(9*0.5)")
end
-- random and randomseed
do
-- random(): float in [0, 1)
for _ = 1, 20 do
local v = math.random()
assertTrue(v >= 0 and v < 1, "random() in [0,1)")
assertEquals(math.type(v), "float", "random() returns float")
end
-- random(n): integer in [1, n]
for _ = 1, 20 do
local v = math.random(10)
assertTrue(v >= 1 and v <= 10, "random(10) in [1,10]")
assertEquals(math.type(v), "integer", "random(n) returns integer")
end
-- random(m, n): integer in [m, n]
for _ = 1, 20 do
local v = math.random(5, 10)
assertTrue(v >= 5 and v <= 10, "random(5,10) in [5,10]")
assertEquals(math.type(v), "integer", "random(m,n) returns integer")
end
-- random(m, m): always returns m
for _ = 1, 5 do
assertEquals(math.random(7, 7), 7, "random(m,m) == m")
end
-- random(0): raw 64-bit integer, type integer
local r0 = math.random(0)
assertEquals(math.type(r0), "integer", "random(0) returns integer")
-- random(n) with n=1: always 1
for _ = 1, 5 do
assertEquals(math.random(1), 1, "random(1) == 1")
end
-- error: random(0.5) — non-integer arg1
assertError(function() math.random(0.5) end)
-- error: random(1, 0) — empty interval
assertError(function() math.random(1, 0) end)
-- error: random(0) is valid (raw), but random(-1) is not
assertError(function() math.random(-1) end)
-- error: random(1, 1.5) — non-integer arg2
assertError(function() math.random(1, 1.5) end)
-- randomseed arg validation
assertError(function() math.randomseed(0.5) end)
assertError(function() math.randomseed(1, 0.5) end)
assertError(function() math.randomseed(1/0) end)
end
-- randomseed: determinism
do
-- same seed yields identical sequence
math.randomseed(42)
local a1, a2, a3 = math.random(), math.random(100), math.random(1, 50)
math.randomseed(42)
local b1, b2, b3 = math.random(), math.random(100), math.random(1, 50)
assertEquals(a1, b1, "randomseed: float sequence reproducible")
assertEquals(a2, b2, "randomseed: random(n) reproducible")
assertEquals(a3, b3, "randomseed: random(m,n) reproducible")
-- different seeds yields different first values
math.randomseed(1)
local c1 = math.random()
math.randomseed(2)
local d1 = math.random()
assertTrue(c1 ~= d1, "different seeds give different values")
-- two-argument seed: same pair yields same sequence
math.randomseed(123, 456)
local e1, e2 = math.random(), math.random()
math.randomseed(123, 456)
local f1, f2 = math.random(), math.random()
assertEquals(e1, f1, "randomseed(x,y): first value reproducible")
assertEquals(e2, f2, "randomseed(x,y): second value reproducible")
-- two-argument seed: different second arg yields different sequence
math.randomseed(123, 456)
local g1 = math.random()
math.randomseed(123, 789)
local h1 = math.random()
assertTrue(g1 ~= h1, "randomseed(x,y1) ~= randomseed(x,y2)")
-- no-args seed does not error and produces valid range
math.randomseed()
local i1 = math.random()
assertTrue(i1 >= 0 and i1 < 1, "randomseed(): range valid after auto-seed")
-- return values: two integers (Lua 5.4 contract)
local s1, s2 = math.randomseed(99)
assertEquals(math.type(s1), "integer", "randomseed returns integer s1")
assertEquals(math.type(s2), "integer", "randomseed returns integer s2")
-- re-seeding restores determinism after auto-seed
math.randomseed(7)
local j1 = math.random()
math.randomseed(7)
local j2 = math.random()
assertEquals(j1, j2, "determinism restored after re-seed")
end
+145
View File
@@ -0,0 +1,145 @@
// PRNG based on xoshiro256** for Space Lua
export class LuaPRNG {
private state: BigUint64Array;
constructor() {
this.state = new BigUint64Array(4);
this.autoSeed();
}
private rotl(x: bigint, k: number): bigint {
k = k & 63;
return ((x << BigInt(k)) | (x >> BigInt(64 - k))) & 0xFFFFFFFFFFFFFFFFn;
}
private nextrand(): bigint {
const s = this.state;
const s0 = s[0];
const s1 = s[1];
const s2 = s[2];
const s3 = s[3];
const res = this.rotl((s1 * 5n) & 0xFFFFFFFFFFFFFFFFn, 7) * 9n &
0xFFFFFFFFFFFFFFFFn;
const t = (s1 << 17n) & 0xFFFFFFFFFFFFFFFFn;
s[2] = s2 ^ s0;
s[3] = s3 ^ s1;
s[1] = s1 ^ s[2];
s[0] = s0 ^ s[3];
s[2] = s[2] ^ t;
s[3] = this.rotl(s[3], 45);
return res;
}
public setSeed(seed1: bigint, seed2: bigint = 0n): [bigint, bigint] {
const MASK = 0xFFFFFFFFFFFFFFFFn;
const s = this.state;
const sm64 = (x: bigint): bigint => {
x = (x ^ (x >> 30n)) * 0xBF58476D1CE4E5B9n & MASK;
x = (x ^ (x >> 27n)) * 0x94D049BB133111EBn & MASK;
return (x ^ (x >> 31n)) & MASK;
};
s[0] = sm64(seed1 & MASK);
s[1] = sm64((seed1 & MASK) | 0xFFn);
s[2] = sm64(seed2 & MASK);
s[3] = sm64(0n);
for (let i = 0; i < 16; i++) {
this.nextrand();
}
return [seed1, seed2];
}
private autoSeed(): [bigint, bigint] {
const t = BigInt(Date.now());
const entropy = BigInt(Math.floor(performance.now() * 1000));
return this.setSeed(t, entropy);
}
private project(ran: bigint, n: bigint): bigint {
if (n === 0n) return 0n;
let lim = n;
lim |= lim >> 1n;
lim |= lim >> 2n;
lim |= lim >> 4n;
lim |= lim >> 8n;
lim |= lim >> 16n;
lim |= lim >> 32n;
while (true) {
ran &= lim;
if (ran <= n) return ran;
ran = this.nextrand();
}
}
// `random()` yields float in [0, 1)
// `random(0)` yields raw 64-bit signed integer (all bits random)
// `random(n)` yields integer in [1, n]
// `random(m, n)` yields integer in [m, n]
public random(arg1?: number, arg2?: number): number | bigint {
const rv = this.nextrand();
if (arg1 === undefined) {
// Top 53 bits for full double precision
return Number(rv >> 11n) * (1.0 / 9007199254740992.0);
}
if (!isFinite(arg1) || !Number.isInteger(arg1)) {
throw new Error(
"bad argument #1 to 'random' (number has no integer representation)",
);
}
if (arg2 === undefined) {
if (arg1 === 0) {
// Raw 64-bit as signed bigint
const signed = rv > 0x7FFFFFFFFFFFFFFFn
? rv - 0x10000000000000000n
: rv;
return signed;
}
if (arg1 < 1) {
throw new Error("bad argument #1 to 'random' (interval is empty)");
}
return Number(this.project(rv, BigInt(arg1) - 1n) + 1n);
}
if (!isFinite(arg2) || !Number.isInteger(arg2)) {
throw new Error(
"bad argument #2 to 'random' (number has no integer representation)",
);
}
if (arg2 < arg1) {
throw new Error("bad argument #2 to 'random' (interval is empty)");
}
return Number(this.project(rv, BigInt(arg2) - BigInt(arg1)) + BigInt(arg1));
}
// Returns [seed1, seed2]
public randomseed(arg1?: number, arg2?: number): [bigint, bigint] {
if (arg1 === undefined) {
return this.autoSeed();
}
if (!isFinite(arg1) || !Number.isInteger(arg1)) {
throw new Error(
"bad argument #1 to 'randomseed' (number has no integer representation)",
);
}
if (arg2 !== undefined && (!isFinite(arg2) || !Number.isInteger(arg2))) {
throw new Error(
"bad argument #2 to 'randomseed' (number has no integer representation)",
);
}
const s1 = BigInt(Math.trunc(arg1));
const s2 = arg2 !== undefined ? BigInt(Math.trunc(arg2)) : 0n;
return this.setSeed(s1, s2);
}
}