Files
plainleaf/client/space_lua/stdlib/math.ts
T

425 lines
14 KiB
TypeScript

import {
LuaBuiltinFunction,
LuaMultiRes,
LuaRuntimeError,
LuaTable,
} from "../runtime.ts";
import { isNegativeZero, isTaggedFloat, makeLuaFloat } 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 {
if (typeof x === "number") return x;
if (isTaggedFloat(x)) return x.value;
return Number(x);
}
export const mathApi = new LuaTable({
// math constants
huge: 1 / 0,
pi: Math.PI,
// math.type(x) => "integer" | "float" | nil
type: new LuaBuiltinFunction({
callback: (_sf, x?: any) => {
if (x === undefined) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.type' (value expected)",
_sf,
);
}
if (isTaggedFloat(x)) {
return "float";
}
if (typeof x === "number") {
if (!Number.isFinite(x) || isNegativeZero(x)) {
return "float";
}
return Number.isInteger(x) ? "integer" : "float";
}
if (typeof x === "bigint") {
return "integer";
}
return null;
},
description:
"Returns `integer` or `float` for a number, or `nil` for other values.",
parameters: [{ name: "x", description: "Value to inspect." }],
returns: [{ type: "string|nil", description: "Numeric subtype or `nil`." }],
}),
/**
* If the value x is representable as a Lua integer, returns an integer
* with that value. Otherwise returns nil.
* Strings are NOT accepted — only Lua number values.
*/
tointeger: new LuaBuiltinFunction({
callback: (_sf, x?: any) => {
if (typeof x === "number") {
return Number.isInteger(x) && Number.isFinite(x) ? x : null;
}
if (isTaggedFloat(x)) {
const n = x.value;
return Number.isInteger(n) && Number.isFinite(n) ? n : null;
}
if (typeof x === "string") {
const n = untagNumber(x); // Number(x) coerces the string
if (Number.isNaN(n) || !Number.isFinite(n) || !Number.isInteger(n))
return null;
return n;
}
return null;
},
description:
"Converts a value to an integer when it has an exact finite integral representation.",
parameters: [{ name: "x", description: "Value to convert." }],
returns: [
{ type: "integer|nil", description: "Converted integer or `nil`." },
],
}),
/**
* When called without arguments, returns a pseudo-random float with
* uniform distribution in the range [0,1). When called with two
* integers m and n, math.random returns a pseudo-random integer
* with uniform distribution in the range [m, n]. The call
* math.random(n), for a positive n, is equivalent to
* math.random(1,n). The call math.random(0) produces an integer
* with all bits (pseudo)random.
*/
random: new LuaBuiltinFunction({
callback: (_sf, m?: number, n?: number) => {
if (m !== undefined) m = untagNumber(m);
if (n !== undefined) n = untagNumber(n);
try {
return prng.random(m, n);
} catch (e: any) {
throw new LuaRuntimeError(e.message, _sf);
}
},
description:
"Returns a pseudo-random float or an integer in a requested inclusive range.",
signatures: [
"math.random(): number",
"math.random(n): integer",
"math.random(m, n): integer",
],
parameters: [
{ name: "m", type: "integer", optional: true },
{ name: "n", type: "integer", optional: true },
],
returns: [{ type: "number", description: "Pseudo-random result." }],
examples: [
{
code: "print(math.random())\nprint(math.random(10))\nprint(math.random(5, 10))",
},
],
}),
/**
* 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({
callback: (_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]);
},
description:
"Seeds the pseudo-random generator and returns the two seeds used.",
signatures: [
"math.randomseed(): integer, integer",
"math.randomseed(x, y): integer, integer",
],
parameters: [
{ name: "x", type: "integer", optional: true },
{ name: "y", type: "integer", optional: true },
],
returns: [
{ type: "integer", description: "First seed." },
{ type: "integer", description: "Second seed." },
],
}),
// Basic functions
abs: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.abs(untagNumber(x)),
description: "Returns the absolute value of `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
ceil: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.ceil(untagNumber(x)),
description: "Returns the smallest integer greater than or equal to `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "integer" }],
}),
floor: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.floor(untagNumber(x)),
description: "Returns the largest integer less than or equal to `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "integer" }],
}),
max: new LuaBuiltinFunction({
callback: (_sf, ...args: number[]) => Math.max(...args.map(untagNumber)),
description: "Returns the greatest of its arguments.",
signatures: ["math.max(x, ...): number"],
returns: [{ type: "number" }],
}),
min: new LuaBuiltinFunction({
callback: (_sf, ...args: number[]) => Math.min(...args.map(untagNumber)),
description: "Returns the least of its arguments.",
signatures: ["math.min(x, ...): number"],
returns: [{ type: "number" }],
}),
// Rounding and remainder
fmod: new LuaBuiltinFunction({
callback: (_sf, x: number, y: number) => untagNumber(x) % untagNumber(y),
description:
"Returns the remainder of `x / y` with the quotient rounded toward zero.",
parameters: [
{ name: "x", type: "number" },
{ name: "y", type: "number" },
],
returns: [{ type: "number" }],
}),
modf: new LuaBuiltinFunction({
callback: (_sf, x: number) => {
const xn = untagNumber(x);
const int = Math.trunc(xn);
// Guarantee that the `frac` part is always Lua float
const frac = makeLuaFloat(xn - int);
return new LuaMultiRes([int, frac]);
},
description: "Splits `x` into its integral and fractional parts.",
parameters: [{ name: "x", type: "number" }],
returns: [
{ type: "integer", description: "Integral part." },
{ type: "float", description: "Fractional part." },
],
examples: [{ code: "local integer, fraction = math.modf(3.14)" }],
}),
// Returns m and e such that x = m * 2^e, 0.5 <= |m| < 1 (or m=0 when x=0).
// e is an integer. Mirrors C99/Lua.
// Special cases: frexp(0) = (0, 0); frexp(+-inf/nan) = (x, 0).
frexp: new LuaBuiltinFunction({
callback: (_sf, x: number) => {
const xn = untagNumber(x);
if (xn === 0 || !Number.isFinite(xn) || Number.isNaN(xn)) {
return new LuaMultiRes([xn, 0]);
}
const abs = Math.abs(xn);
let e = Math.floor(Math.log2(abs)) + 1;
let m = xn / 2 ** e;
if (Math.abs(m) >= 1.0) {
e += 1;
m /= 2;
}
if (Math.abs(m) < 0.5) {
e -= 1;
m *= 2;
}
return new LuaMultiRes([m, e]);
},
description:
"Decomposes `x` into a normalized fraction and a power-of-two exponent.",
parameters: [{ name: "x", type: "number" }],
returns: [
{ type: "number", description: "Fraction." },
{ type: "integer", description: "Exponent." },
],
}),
// Returns m * 2^e (the inverse of frexp). Mirrors C99/Lua.
ldexp: new LuaBuiltinFunction({
callback: (_sf, m: number, e: number) =>
untagNumber(m) * 2 ** untagNumber(e),
description: "Returns `m * 2^e`, the inverse of `math.frexp`.",
parameters: [
{ name: "m", type: "number" },
{ name: "e", type: "integer" },
],
returns: [{ type: "number" }],
}),
// Power and logarithms
exp: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.exp(untagNumber(x)),
description: "Returns `e` raised to `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
log: new LuaBuiltinFunction({
callback: (_sf, x: number, base?: number) => {
if (base === undefined) {
return Math.log(untagNumber(x));
}
return Math.log(untagNumber(x)) / Math.log(untagNumber(base));
},
description:
"Returns the logarithm of `x`, using the natural base unless another base is supplied.",
parameters: [
{ name: "x", type: "number" },
{ name: "base", type: "number", optional: true },
],
returns: [{ type: "number" }],
examples: [{ code: "print(math.log(100, 10)) -- 2" }],
}),
// Power function (deprecated in Lua 5.4 but retained for compatibility)
pow: new LuaBuiltinFunction({
callback: (_sf, x: number, y: number) => untagNumber(x) ** untagNumber(y),
description: "Returns `x` raised to the power `y`.",
parameters: [
{ name: "x", type: "number" },
{ name: "y", type: "number" },
],
returns: [{ type: "number" }],
deprecated: "Use the `^` operator instead.",
}),
sqrt: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.sqrt(untagNumber(x)),
description: "Returns the square root of `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
// Trigonometric functions
cos: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.cos(untagNumber(x)),
description: "Returns the cosine of `x` radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
sin: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.sin(untagNumber(x)),
description: "Returns the sine of `x` radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
tan: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.tan(untagNumber(x)),
description: "Returns the tangent of `x` radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
acos: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.acos(untagNumber(x)),
description: "Returns the arc cosine of `x` in radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
asin: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.asin(untagNumber(x)),
description: "Returns the arc sine of `x` in radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
atan: new LuaBuiltinFunction({
callback: (_sf, y: number, x?: number) => {
if (x === undefined) {
return Math.atan(untagNumber(y));
}
return Math.atan2(untagNumber(y), untagNumber(x));
},
description:
"Returns the arc tangent of `y/x` in radians, using `1` for omitted `x`.",
parameters: [
{ name: "y", type: "number" },
{ name: "x", type: "number", optional: true },
],
returns: [{ type: "number" }],
}),
// Hyperbolic functions (deprecated in Lua 5.4 but retained for compatibility)
cosh: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.cosh(untagNumber(x)),
description: "Returns the hyperbolic cosine of `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
deprecated: "Retained for compatibility with older Lua versions.",
}),
sinh: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.sinh(untagNumber(x)),
description: "Returns the hyperbolic sine of `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
deprecated: "Retained for compatibility with older Lua versions.",
}),
tanh: new LuaBuiltinFunction({
callback: (_sf, x: number) => Math.tanh(untagNumber(x)),
description: "Returns the hyperbolic tangent of `x`.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
deprecated: "Retained for compatibility with older Lua versions.",
}),
// Additional utility
deg: new LuaBuiltinFunction({
callback: (_sf, x: number) => (untagNumber(x) * 180) / Math.PI,
description: "Converts an angle from radians to degrees.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
examples: [{ code: "print(math.deg(math.pi)) -- 180" }],
}),
rad: new LuaBuiltinFunction({
callback: (_sf, x: number) => (untagNumber(x) * Math.PI) / 180,
description: "Converts an angle from degrees to radians.",
parameters: [{ name: "x", type: "number" }],
returns: [{ type: "number" }],
}),
ult: new LuaBuiltinFunction({
callback: (_sf, m: number, n: number) => {
return untagNumber(m) >>> 0 < untagNumber(n) >>> 0;
},
description: "Compares two integers as unsigned 32-bit values.",
parameters: [
{ name: "m", type: "integer" },
{ name: "n", type: "integer" },
],
returns: [{ type: "boolean" }],
examples: [{ code: "print(math.ult(2, 3)) -- true" }],
}),
// Keep the cosineSimilarity utility function
cosineSimilarity: new LuaBuiltinFunction({
callback: (sf, vecA: LuaTable | number[], vecB: LuaTable | number[]) => {
if (vecA instanceof LuaTable) {
vecA = vecA.toJSArray();
}
if (vecB instanceof LuaTable) {
vecB = vecB.toJSArray();
}
if (vecA.length !== vecB.length) {
throw new LuaRuntimeError("Vectors must be of the same length", sf);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] ** 2;
normB += vecB[i] ** 2;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
},
description:
"Returns the cosine similarity between two equal-length numeric vectors.",
parameters: [
{ name: "vecA", type: "table" },
{ name: "vecB", type: "table" },
],
returns: [{ type: "number", description: "Cosine similarity." }],
examples: [{ code: "print(math.cosineSimilarity({1, 2, 3}, {4, 5, 6}))" }],
}),
});