* Space Lua: Improve numeric types semantics Add numeric result normalization for arithmetic operations: * Integer operations preserve int type, collapse -0 to +0 * Float operations preserve float type, maintain -0.0 * Division always produces float results Fix for-loop variable type tracking: * Loop variable type determined by start/step parameters * Integer loops produce integer variables * Float/mixed loops produce float-tagged variables Implement table key normalization: * Integer-valued floats (1.0, 2.0) normalize to integer keys * All zero variants (0, 0.0, -0.0) map to same key * Non-integer floats (1.5) remain distinct keys Add tests for: * Arithmetic type preservation across operators * For-loop type modes (int/float/mixed) * Table key equivalence and normalization * Edge cases with zero, -0.0, and special values * Standard library `type` and `math.type` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Code refactor and deduplication Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
213 lines
4.4 KiB
TypeScript
213 lines
4.4 KiB
TypeScript
import type { NumericType } from "./ast.ts";
|
|
import { luaToNumberDetailed } from "./tonumber.ts";
|
|
import { luaTypeName } from "./runtime.ts";
|
|
|
|
export const ZeroBoxKind = Symbol("ZeroBox");
|
|
|
|
export type LuaFloatTag = {
|
|
readonly type: "float";
|
|
readonly value: number;
|
|
};
|
|
|
|
export function isBoxedZero(x: any): boolean {
|
|
return x instanceof Number && Number(x) === 0 && !isNegativeZero(Number(x));
|
|
}
|
|
|
|
export function getZeroBoxKind(x: any): NumericType | undefined {
|
|
return isBoxedZero(x)
|
|
? (x as any)[ZeroBoxKind] as NumericType | undefined
|
|
: undefined;
|
|
}
|
|
|
|
export function boxZero(kind: NumericType): number {
|
|
const z = new Number(0);
|
|
(z as any)[ZeroBoxKind] = kind;
|
|
return z as unknown as number;
|
|
}
|
|
|
|
export function isFloatTag(v: any): v is LuaFloatTag {
|
|
return v && typeof v === "object" && v.type === "float";
|
|
}
|
|
|
|
// Tag an integer-valued number as a float:
|
|
// * zero: returns boxed float zero,
|
|
// * integer: returns tagged float,
|
|
// * non-integer: returns plain number.
|
|
export function floatLiteral(n: number): number | LuaFloatTag {
|
|
if (n === 0 && !isNegativeZero(n)) {
|
|
return boxZero("float");
|
|
}
|
|
if (Number.isInteger(n)) {
|
|
return { type: "float" as const, value: n };
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export function untagNumber(n: any): number {
|
|
if (isFloatTag(n)) {
|
|
return n.value;
|
|
}
|
|
if (n instanceof Number) {
|
|
return Number(n);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export function getNumericKind(
|
|
n: number | LuaFloatTag | any,
|
|
): NumericType | undefined {
|
|
if (isFloatTag(n)) {
|
|
return "float";
|
|
}
|
|
if (typeof n === "number") {
|
|
return Number.isInteger(n) ? "int" : "float";
|
|
}
|
|
if (n instanceof Number) {
|
|
const kind = getZeroBoxKind(n);
|
|
if (kind) {
|
|
return kind;
|
|
}
|
|
return Number.isInteger(Number(n)) ? "int" : "float";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export type OpHints = {
|
|
leftKind?: NumericType;
|
|
rightKind?: NumericType;
|
|
};
|
|
|
|
export const luaStringCoercionError: Error = new Error(
|
|
"LuaStringCoercionError",
|
|
);
|
|
|
|
export function isNegativeZero(x: number): boolean {
|
|
// faster than `Object.is(<object>, -0)`
|
|
return x === 0 && 1 / x === -Infinity;
|
|
}
|
|
|
|
function produceNegativeZero() {
|
|
return { n: -0, isInt: false, zeroKind: "float" as const };
|
|
}
|
|
|
|
export function coerceNumeric(
|
|
val: unknown,
|
|
): {
|
|
n: number;
|
|
isInt: boolean;
|
|
zeroKind?: NumericType;
|
|
} {
|
|
if (isFloatTag(val)) {
|
|
const n = val.value;
|
|
if (n === 0) {
|
|
if (isNegativeZero(n)) {
|
|
return produceNegativeZero();
|
|
}
|
|
return { n: 0, isInt: false, zeroKind: "float" };
|
|
}
|
|
// Non-zero float tag: always mark as float
|
|
return { n, isInt: false, zeroKind: "float" };
|
|
}
|
|
|
|
if (isBoxedZero(val)) {
|
|
return { n: 0, isInt: true, zeroKind: getZeroBoxKind(val)! };
|
|
}
|
|
|
|
if (typeof val === "number") {
|
|
const n = val;
|
|
if (n === 0) {
|
|
if (isNegativeZero(n)) {
|
|
return produceNegativeZero();
|
|
}
|
|
return {
|
|
n,
|
|
isInt: true,
|
|
};
|
|
}
|
|
return {
|
|
n,
|
|
isInt: Number.isInteger(n),
|
|
};
|
|
}
|
|
|
|
if (val instanceof Number) {
|
|
const n = Number(val);
|
|
if (n === 0 && !isNegativeZero(n)) {
|
|
return {
|
|
n,
|
|
isInt: true,
|
|
zeroKind: getZeroBoxKind(val),
|
|
};
|
|
}
|
|
return {
|
|
n,
|
|
isInt: Number.isInteger(n),
|
|
};
|
|
}
|
|
|
|
if (typeof val === "string") {
|
|
const det = luaToNumberDetailed(val);
|
|
if (!det) {
|
|
throw luaStringCoercionError;
|
|
}
|
|
|
|
const n = det.value;
|
|
const isInt = det.numericType === "int";
|
|
|
|
if (n === 0) {
|
|
if (isNegativeZero(n)) {
|
|
return produceNegativeZero();
|
|
}
|
|
return {
|
|
n,
|
|
isInt,
|
|
zeroKind: det.numericType,
|
|
};
|
|
}
|
|
|
|
return {
|
|
n,
|
|
isInt,
|
|
};
|
|
}
|
|
|
|
throw new Error(
|
|
`attempt to perform arithmetic on a ${luaTypeName(val)} value`,
|
|
);
|
|
}
|
|
|
|
export function coerceNumericPair(
|
|
a: unknown,
|
|
b: unknown,
|
|
hints?: OpHints,
|
|
): {
|
|
ax: number;
|
|
bx: number;
|
|
bothInt: boolean;
|
|
aZeroKind?: NumericType;
|
|
bZeroKind?: NumericType;
|
|
} {
|
|
const A = coerceNumeric(a);
|
|
const B = coerceNumeric(b);
|
|
|
|
const aIsInt = hints?.leftKind !== undefined
|
|
? hints.leftKind === "int"
|
|
: (A.zeroKind === "float"
|
|
? false
|
|
: (A.zeroKind === "int" ? true : A.isInt));
|
|
|
|
const bIsInt = hints?.rightKind !== undefined
|
|
? hints.rightKind === "int"
|
|
: (B.zeroKind === "float"
|
|
? false
|
|
: (B.zeroKind === "int" ? true : B.isInt));
|
|
|
|
return {
|
|
ax: A.n,
|
|
bx: B.n,
|
|
bothInt: aIsInt && bIsInt,
|
|
aZeroKind: A.zeroKind,
|
|
bZeroKind: B.zeroKind,
|
|
};
|
|
}
|