238 lines
5.1 KiB
TypeScript
238 lines
5.1 KiB
TypeScript
import type { NumericType } from "./ast.ts";
|
|
import { luaToNumberDetailed } from "./tonumber.ts";
|
|
import { luaTypeName } from "./runtime.ts";
|
|
|
|
export interface LuaTaggedFloat {
|
|
readonly value: number;
|
|
readonly isFloat: true;
|
|
}
|
|
|
|
// Pre-allocated singletons for float zeros
|
|
const FLOAT_POS_ZERO: LuaTaggedFloat = { value: 0, isFloat: true };
|
|
const FLOAT_NEG_ZERO: LuaTaggedFloat = { value: -0, isFloat: true };
|
|
|
|
export const luaStringCoercionError: Error = new Error(
|
|
"LuaStringCoercionError",
|
|
);
|
|
|
|
export function isNegativeZero(n: number): boolean {
|
|
return n === 0 && 1 / n === -Infinity;
|
|
}
|
|
|
|
export function isTaggedFloat(v: unknown): v is LuaTaggedFloat {
|
|
return v !== null && typeof v === "object" && (v as any).isFloat === true;
|
|
}
|
|
|
|
function makeFloat(n: number): LuaTaggedFloat {
|
|
if (n === 0) {
|
|
return isNegativeZero(n) ? FLOAT_NEG_ZERO : FLOAT_POS_ZERO;
|
|
}
|
|
return { value: n, isFloat: true };
|
|
}
|
|
|
|
// Box a zero with a given kind tag.
|
|
export function makeLuaZero(n: number, numericType: NumericType): any {
|
|
if (n !== 0) {
|
|
return n;
|
|
}
|
|
if (numericType !== "float") {
|
|
return 0;
|
|
}
|
|
return isNegativeZero(n) ? FLOAT_NEG_ZERO : FLOAT_POS_ZERO;
|
|
}
|
|
|
|
// Tag an integer-valued number as float.
|
|
// Only allocates for integer-valued results; non-integer floats
|
|
// (1.5, NaN, Inf) are already unambiguously float as plain `number`.
|
|
export function makeLuaFloat(n: number): any {
|
|
if (!Number.isInteger(n)) {
|
|
return n;
|
|
}
|
|
return makeFloat(n);
|
|
}
|
|
|
|
export function getZeroBoxKind(x: any): NumericType | undefined {
|
|
if (isTaggedFloat(x)) {
|
|
return "float";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// Unwrap a potentially tagged or boxed Number to a plain number.
|
|
export function untagNumber(n: any): number {
|
|
if (typeof n === "number") return n;
|
|
if (isTaggedFloat(n)) {
|
|
return n.value;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export function coerceToNumber(v: unknown): number | null {
|
|
if (typeof v === "number") {
|
|
return v;
|
|
}
|
|
if (isTaggedFloat(v)) {
|
|
return v.value;
|
|
}
|
|
if (typeof v === "string") {
|
|
const det = luaToNumberDetailed(v);
|
|
if (!det) {
|
|
return null;
|
|
}
|
|
return det.value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function inferNumericType(n: number): NumericType {
|
|
if (!Number.isFinite(n)) {
|
|
return "float";
|
|
}
|
|
if (isNegativeZero(n)) {
|
|
return "float";
|
|
}
|
|
return Number.isInteger(n) ? "int" : "float";
|
|
}
|
|
|
|
export function combineNumericTypes(
|
|
a: NumericType | undefined,
|
|
b: NumericType | undefined,
|
|
): NumericType {
|
|
if (a === "float" || b === "float") {
|
|
return "float";
|
|
}
|
|
return "int";
|
|
}
|
|
|
|
export function getNumericKind(n: unknown): NumericType | undefined {
|
|
if (typeof n === "number") {
|
|
return inferNumericType(n);
|
|
}
|
|
if (isTaggedFloat(n)) {
|
|
return "float";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export type CoerceNumericResult = {
|
|
n: number;
|
|
type: NumericType;
|
|
};
|
|
|
|
export function coerceNumeric(
|
|
val: unknown,
|
|
hint?: NumericType,
|
|
): CoerceNumericResult {
|
|
if (typeof val === "number") {
|
|
return { n: val, type: hint ?? inferNumericType(val) };
|
|
}
|
|
|
|
if (isTaggedFloat(val)) {
|
|
return { n: val.value, type: hint ?? "float" };
|
|
}
|
|
|
|
if (typeof val === "string") {
|
|
const det = luaToNumberDetailed(val);
|
|
if (!det) {
|
|
throw luaStringCoercionError;
|
|
}
|
|
return { n: det.value, type: hint ?? det.numericType };
|
|
}
|
|
|
|
throw new Error(
|
|
`attempt to perform arithmetic on a ${luaTypeName(val)} value`,
|
|
);
|
|
}
|
|
|
|
export type CoerceNumericPairResult = {
|
|
left: number;
|
|
right: number;
|
|
resultType: NumericType;
|
|
};
|
|
|
|
export function coerceNumericPair(
|
|
a: unknown,
|
|
b: unknown,
|
|
leftType?: NumericType,
|
|
rightType?: NumericType,
|
|
op?: string,
|
|
): CoerceNumericPairResult {
|
|
const forceFloat = op === "/" || op === "^";
|
|
|
|
// Both plain numbers
|
|
if (typeof a === "number" && typeof b === "number") {
|
|
const lt = leftType ?? inferNumericType(a);
|
|
const rt = rightType ?? inferNumericType(b);
|
|
|
|
return {
|
|
left: a,
|
|
right: b,
|
|
resultType: forceFloat
|
|
? "float"
|
|
: lt === "float" || rt === "float"
|
|
? "float"
|
|
: "int",
|
|
};
|
|
}
|
|
|
|
// One tagged float, one plain number
|
|
if (typeof a === "number" && isTaggedFloat(b)) {
|
|
return {
|
|
left: a,
|
|
right: b.value,
|
|
resultType: "float",
|
|
};
|
|
}
|
|
|
|
if (isTaggedFloat(a) && typeof b === "number") {
|
|
return {
|
|
left: a.value,
|
|
right: b,
|
|
resultType: "float",
|
|
};
|
|
}
|
|
|
|
// Both tagged floats
|
|
if (isTaggedFloat(a) && isTaggedFloat(b)) {
|
|
return {
|
|
left: a.value,
|
|
right: b.value,
|
|
resultType: "float",
|
|
};
|
|
}
|
|
|
|
// General fallback
|
|
const A = coerceNumeric(a, leftType);
|
|
const B = coerceNumeric(b, rightType);
|
|
|
|
return {
|
|
left: A.n,
|
|
right: B.n,
|
|
resultType: forceFloat ? "float" : combineNumericTypes(A.type, B.type),
|
|
};
|
|
}
|
|
|
|
export function normalizeArithmeticResult(
|
|
n: number,
|
|
resultType: NumericType,
|
|
): number {
|
|
if (n === 0) {
|
|
if (resultType === "int") {
|
|
return 0;
|
|
}
|
|
return isNegativeZero(n) ? -0 : 0;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export function toInteger(v: unknown): number | null {
|
|
if (typeof v === "number") {
|
|
return Number.isInteger(v) ? v : null;
|
|
}
|
|
if (isTaggedFloat(v)) {
|
|
const n = v.value;
|
|
return Number.isInteger(n) ? n : null;
|
|
}
|
|
return null;
|
|
}
|