[Space Lua]: Implement math.random and math.randomseed (#1845)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user