* Space Lua: `<close>` attribute, `__close` metamethod and extensive tests Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Align arithmetical error messages with Lua 5.4 **Fixes:** - Tighten `tonumber` semantics to accept hex floats like `0x1.2` while keeping plain hex as integers. - Improve Lua compatibility by fixing relational `>`/`>=` operand ordering to preserve correct error type direction. **Changes:** - Update concatenation errors to include the operand type. - Add `LuaStringCoercionError` from numeric coercion and handle it in `luaOp()` to emit correct messages (or fallback). - Raise > `bad argument #2 to 'tonumber' (base out of range)` for invalid bases. - Update test suite **Notes:** - The `arithmetic_test.lua` test suite now passes if run with Lua 5.4. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Run `make fmt` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Rename `assert_throws` to `assertThrows` in Lua test suite Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Replace `LuaStringCoercionError` class with sentinel constant Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
146 lines
2.9 KiB
TypeScript
146 lines
2.9 KiB
TypeScript
import type { NumericType } from "./ast.ts";
|
|
import { luaToNumberDetailed } from "./tonumber.ts";
|
|
import { luaTypeName } from "./runtime.ts";
|
|
|
|
export const ZeroBoxKind = Symbol("ZeroBox");
|
|
|
|
export function isBoxedZero(x: any): boolean {
|
|
return x instanceof Number && Number(x) === 0 && !Object.is(Number(x), -0);
|
|
}
|
|
|
|
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 type OpHints = {
|
|
leftKind?: NumericType;
|
|
rightKind?: NumericType;
|
|
};
|
|
|
|
// Marker error used to let the evaluator decide the final message.
|
|
export const luaStringCoercionError: Error = new Error(
|
|
"LuaStringCoercionError",
|
|
);
|
|
|
|
export function coerceNumeric(
|
|
val: unknown,
|
|
): {
|
|
n: number;
|
|
isInt: boolean;
|
|
zeroKind?: NumericType;
|
|
} {
|
|
if (isBoxedZero(val)) {
|
|
return { n: 0, isInt: true, zeroKind: getZeroBoxKind(val)! };
|
|
}
|
|
|
|
if (typeof val === "number") {
|
|
const n = val;
|
|
if (n === 0) {
|
|
if (Object.is(n, -0)) {
|
|
return {
|
|
n,
|
|
isInt: true,
|
|
zeroKind: "float",
|
|
};
|
|
}
|
|
|
|
return {
|
|
n,
|
|
isInt: true,
|
|
};
|
|
}
|
|
return {
|
|
n,
|
|
isInt: Number.isInteger(n),
|
|
};
|
|
}
|
|
|
|
if (val instanceof Number) {
|
|
const n = Number(val);
|
|
if (n === 0 && !Object.is(n, -0)) {
|
|
return {
|
|
n,
|
|
isInt: true,
|
|
zeroKind: getZeroBoxKind(val),
|
|
};
|
|
}
|
|
return {
|
|
n,
|
|
isInt: Number.isInteger(n),
|
|
};
|
|
}
|
|
|
|
if (typeof val === "string") {
|
|
const det = luaToNumberDetailed(val);
|
|
if (!det) {
|
|
// Let evaluator produce the final error message
|
|
throw luaStringCoercionError;
|
|
}
|
|
|
|
const n = det.value;
|
|
const isInt = det.numericType === "int";
|
|
if (n === 0) {
|
|
if (Object.is(n, -0)) {
|
|
return {
|
|
n,
|
|
isInt,
|
|
zeroKind: "float",
|
|
};
|
|
}
|
|
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
|
|
? hints.leftKind === "int"
|
|
: (A.zeroKind ? A.zeroKind === "int" : A.isInt);
|
|
|
|
const bIsInt = hints?.rightKind
|
|
? hints.rightKind === "int"
|
|
: (B.zeroKind ? B.zeroKind === "int" : B.isInt);
|
|
|
|
return {
|
|
ax: A.n,
|
|
bx: B.n,
|
|
bothInt: aIsInt && bIsInt,
|
|
aZeroKind: A.zeroKind,
|
|
bZeroKind: B.zeroKind,
|
|
};
|
|
}
|