Files
plainleaf/client/space_lua/numeric.ts
T
Matouš Jan FialkaandGitHub 5a20a5f8f4 Space Lua: Align arithmetical error messages with Lua 5.4 (#1793)
* 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>
2026-01-29 08:04:27 +01:00

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,
};
}