mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Convert global isFinite's argument with ToNumber (#1662)
This commit is contained in:
@@ -524,8 +524,15 @@ export function emitOperatorExpr(
|
||||
case "seqExpr": {
|
||||
// Statements mid-expression: each emits in place (its own frame,
|
||||
// exactly statement position); the result is an ordinary temp of
|
||||
// the current frame. No jump can leave the expression's region.
|
||||
for (const s of e.stmts) host.emitStmt(s);
|
||||
// the current frame. No jump can leave the expression's region. A
|
||||
// statement can still end the block (a call that always throws):
|
||||
// what follows then emits in a block with no predecessors, so every
|
||||
// value it reads is defined there, and LLVM deletes it.
|
||||
for (const s of e.stmts) {
|
||||
if (host.B.isTerminated()) host.B.startBlock(host.B.newLabel("seq.dead"));
|
||||
host.emitStmt(s);
|
||||
}
|
||||
if (host.B.isTerminated()) host.B.startBlock(host.B.newLabel("seq.dead"));
|
||||
return host.emitExpr(e.result);
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { lowerToNumberArguments } from "../lower-exprs.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
@@ -115,11 +115,19 @@ export function lowerStringStaticCall(
|
||||
const packed = lowerer.lowerExprExpecting(spread.expression, arrayOf(F64));
|
||||
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
|
||||
}
|
||||
// Each code converts with ToNumber: a missing value is NaN (code unit 0
|
||||
// for fromCharCode, a RangeError for fromCodePoint), exactly Node.
|
||||
const elems = call.arguments.nodes.map((a) => lowerToNumberArgument(lowerer, a));
|
||||
// Each code converts with ToNumber after every argument is evaluated: a
|
||||
// missing value is NaN (code unit 0 for fromCharCode, a RangeError for
|
||||
// fromCodePoint), exactly Node.
|
||||
const { stmts, values: elems } = lowerToNumberArguments(lowerer, call.arguments.nodes);
|
||||
const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
|
||||
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: `string.${member}`,
|
||||
args: [packed],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom
|
||||
|
||||
@@ -166,7 +166,6 @@ import { lowerReflectCall } from "./builtins/reflection.js";
|
||||
import {
|
||||
lowerAbsenceProbe,
|
||||
v8CalleeText,
|
||||
lowerOptionalNumber,
|
||||
lowerPromiseAllTupleCall,
|
||||
lowerPromiseRejectCall,
|
||||
lowerToNumberArgument,
|
||||
@@ -1008,10 +1007,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// excludes shadowed names. parseInt's omitted radix completes
|
||||
// to 0 — the spec's "undefined" (base 10 with the 0x hex escape);
|
||||
// parseFloat lowers the STRING form only (Node would ToString other
|
||||
// values — no static story); isNaN/isFinite's arguments are
|
||||
// checker-pinned (or checked) to number, where the global's ToNumber
|
||||
// coercion is the identity and the tests are Number.isNaN /
|
||||
// Number.isFinite exactly (ms's `isFinite(val)` guard).
|
||||
// values — no static story); isNaN/isFinite apply the global's
|
||||
// ToNumber to any argument, then test like Number.isNaN /
|
||||
// Number.isFinite.
|
||||
if (
|
||||
(expr.expression.text === "parseInt" ||
|
||||
expr.expression.text === "isNaN" ||
|
||||
@@ -1130,18 +1128,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
}
|
||||
if (islFn && expr.arguments.nodes.length === islFn.args.length) {
|
||||
// The static forms above already took the exactly-typed argument
|
||||
// (a string for parseFloat, a number for isFinite): name the
|
||||
// argument type that refused the call, so the message states the
|
||||
// condition rather than reading as if the global itself were
|
||||
// unsupported.
|
||||
// (a string for parseFloat): name the argument type that refused
|
||||
// the call, so the message states the condition rather than
|
||||
// reading as if the global itself were unsupported.
|
||||
const argText = loweredArgumentText(lowerer, expr.arguments.nodes[0]!);
|
||||
const exact = expr.expression.text === "isFinite" ? "number" : "string";
|
||||
lowerer.refuseUnsupportedApi(
|
||||
`'${expr.expression.text}' with a '${argText}' argument`,
|
||||
expr,
|
||||
exact === "string"
|
||||
? "only a string argument compiles statically: convert the argument explicitly first (for example String(x), or narrow the union)"
|
||||
: "only a number argument compiles statically: narrow the argument to a number first",
|
||||
"only a string argument compiles statically: convert the argument explicitly first (for example String(x), or narrow the union)",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10314,7 +10308,7 @@ export function lowerStaticNumberGlobal(
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (name === "parseInt" || name === "isNaN") {
|
||||
if (name === "parseInt" || name === "isNaN" || name === "isFinite") {
|
||||
const maxArgs = name === "parseInt" ? 2 : 1;
|
||||
if (args.length < 1 || args.length > maxArgs) {
|
||||
lowerer.noLowering(
|
||||
@@ -10322,10 +10316,12 @@ export function lowerStaticNumberGlobal(
|
||||
call,
|
||||
);
|
||||
}
|
||||
if (name === "isNaN") {
|
||||
// The global's ToNumber turns a missing value into NaN.
|
||||
if (name === "isNaN" || name === "isFinite") {
|
||||
// The global's ToNumber turns a missing value into NaN, converts
|
||||
// strings, booleans and null, and calls valueOf/toString on objects.
|
||||
const x = lowerToNumberArgument(lowerer, args[0]!);
|
||||
return { kind: "libCall", fn: "num.isNaN", args: [x], type: BOOL, loc };
|
||||
const fn = name === "isNaN" ? "num.isNaN" : "number.isFinite";
|
||||
return { kind: "libCall", fn, args: [x], type: BOOL, loc };
|
||||
}
|
||||
const radix: IrExpr = args[1]
|
||||
? lowerer.lowerExprExpecting(args[1], F64)
|
||||
@@ -10461,32 +10457,28 @@ export function lowerStaticNumberGlobal(
|
||||
: lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
|
||||
}
|
||||
// STATIC parseFloat/isFinite over exactly-typed arguments —
|
||||
// parseInt's siblings (num.parseFloat is ECMA 19.2.4's decimal-
|
||||
// literal prefix parse in scr_string.c; a number-typed isFinite IS
|
||||
// Number.isFinite — the global's ToNumber coercion is the identity
|
||||
// there, ms's `isFinite(val)` guard). Other argument types retain
|
||||
// their native coercion checks or the SC2012 refusal. The probe never
|
||||
// emits — lowering is IR construction.
|
||||
if ((name === "parseFloat" || name === "isFinite") && args.length === 1) {
|
||||
if (name === "parseFloat") {
|
||||
const optional = optionalCallValue(lowerer, args[0]!);
|
||||
if (optional) {
|
||||
const parsed = lowerOptionalStringNumber(
|
||||
lowerer,
|
||||
optional,
|
||||
loc,
|
||||
"num.parseFloat",
|
||||
"parseFloat",
|
||||
);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
// STATIC parseFloat over string arguments — parseInt's sibling
|
||||
// (num.parseFloat is ECMA 19.2.4's decimal-literal prefix parse in
|
||||
// scr_string.c). Other argument types retain their native coercion
|
||||
// checks or the SC2012 refusal. The probe never emits — lowering is IR
|
||||
// construction.
|
||||
if (name === "parseFloat" && args.length === 1) {
|
||||
const optional = optionalCallValue(lowerer, args[0]!);
|
||||
if (optional) {
|
||||
const parsed = lowerOptionalStringNumber(
|
||||
lowerer,
|
||||
optional,
|
||||
loc,
|
||||
"num.parseFloat",
|
||||
"parseFloat",
|
||||
);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
const probed = tryLowerExpression(lowerer, args[0]!);
|
||||
if (name === "parseFloat" && probed?.type.kind === "string") {
|
||||
if (probed?.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [probed], type: F64, loc };
|
||||
}
|
||||
if (name === "parseFloat" && probed?.type.kind === "dyn") {
|
||||
if (probed?.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.parseFloat",
|
||||
@@ -10495,20 +10487,6 @@ export function lowerStaticNumberGlobal(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "isFinite" && probed?.type.kind === "f64") {
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [probed], type: BOOL, loc };
|
||||
}
|
||||
// A number the checker trusts but storage may hold missing (an
|
||||
// out-of-range read): the global's ToNumber makes undefined NaN.
|
||||
if (
|
||||
name === "isFinite" &&
|
||||
probed?.type.kind === "union" &&
|
||||
lowerer.armTag(probed.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
lowerer.stripUndefinedArm(probed.type).kind === "f64"
|
||||
) {
|
||||
const x = lowerOptionalNumber(lowerer, probed, loc, args[0]);
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [x], type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1420,9 +1420,83 @@ export function lowerOptionalNumber(
|
||||
* out-of-range read, a local or field holding one, or a hole — converts to
|
||||
* NaN instead of trusting the checker's number type. */
|
||||
export function lowerToNumberArgument(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
const operand = numberOperand(lowerer, node);
|
||||
return operand.convert ? operand.convert(operand.value) : operand.value;
|
||||
}
|
||||
|
||||
/** The ToNumber arguments of one call (the Math functions,
|
||||
* String.fromCharCode): Node evaluates every argument before it converts
|
||||
* any. When an argument's conversion can run program code (valueOf or
|
||||
* toString on a dynamic value) and a later argument follows, the arguments
|
||||
* evaluate into temporaries first and convert in order after. `lowerOther`
|
||||
* lowers an argument that takes no conversion (a spread's array), or returns
|
||||
* null for a ToNumber argument; with temporaries, such an array is copied
|
||||
* where it is evaluated, since JavaScript reads a spread's elements then,
|
||||
* before a later conversion's valueOf could change them. Returns the
|
||||
* temporaries' statements (empty when none are needed) and the converted
|
||||
* values. */
|
||||
export function lowerToNumberArguments(
|
||||
lowerer: Lowerer,
|
||||
nodes: readonly ts.Expression[],
|
||||
lowerOther?: (node: ts.Expression) => IrExpr | null,
|
||||
): { stmts: IrStmt[]; values: IrExpr[] } {
|
||||
const operands = nodes.map((node): NumberOperand => {
|
||||
const other = lowerOther?.(node);
|
||||
return other
|
||||
? { value: other, convert: null, runsCode: false, spread: true }
|
||||
: numberOperand(lowerer, node);
|
||||
});
|
||||
const late = operands.slice(0, -1).some((operand) => operand.runsCode);
|
||||
if (!late) {
|
||||
const values = operands.map((o) => (o.convert ? o.convert(o.value) : o.value));
|
||||
return { stmts: [], values };
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
const values = operands.map((operand) => {
|
||||
if (operand.value.kind === "numLit") return operand.value;
|
||||
const temp = lowerer.declareHiddenLocal("%numberArgument", operand.value.type);
|
||||
const loc = operand.value.loc;
|
||||
const init: IrExpr =
|
||||
operand.spread && operand.value.type.kind === "array"
|
||||
? {
|
||||
kind: "arrIntrinsic",
|
||||
method: "slice",
|
||||
receiver: operand.value,
|
||||
args: [numLit(0, loc), numLit(Infinity, loc)],
|
||||
type: operand.value.type,
|
||||
loc,
|
||||
}
|
||||
: operand.value;
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init, loc });
|
||||
const read = varRef(temp.id, operand.value.type, loc);
|
||||
return operand.convert ? operand.convert(read) : read;
|
||||
});
|
||||
return { stmts, values };
|
||||
}
|
||||
|
||||
/** An argument a ToNumber site lowers: its evaluated value and the
|
||||
* conversion that makes it a number (null when the value already is one).
|
||||
* `runsCode`: the conversion can call program code. `spread`: the value is
|
||||
* a spread's array (lowerToNumberArguments' `lowerOther`). */
|
||||
interface NumberOperand {
|
||||
value: IrExpr;
|
||||
convert: ((value: IrExpr) => IrExpr) | null;
|
||||
runsCode: boolean;
|
||||
spread?: boolean;
|
||||
}
|
||||
|
||||
function numberOperand(lowerer: Lowerer, node: ts.Expression): NumberOperand {
|
||||
let inner = node;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
const loc = locOf(inner);
|
||||
const number = (value: IrExpr): NumberOperand => ({ value, convert: null, runsCode: false });
|
||||
const dynToNumber = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const isOptionalNumber = (value: IrExpr): boolean =>
|
||||
value.type.kind === "union" &&
|
||||
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
@@ -1433,12 +1507,54 @@ export function lowerToNumberArgument(lowerer: Lowerer, node: ts.Expression): Ir
|
||||
? lowerAbsenceProbe(lowerer, inner)
|
||||
: null;
|
||||
if (optional && isOptionalNumber(optional))
|
||||
return lowerOptionalNumber(lowerer, optional, loc, inner);
|
||||
// A conditional builds each branch for the destination.
|
||||
if (ts.isConditionalExpression(inner)) return lowerer.lowerExprExpecting(node, F64);
|
||||
return number(lowerOptionalNumber(lowerer, optional, loc, inner));
|
||||
// A conditional with number branches builds each branch as a number;
|
||||
// other branches box, and the value converts like any dynamic value.
|
||||
if (ts.isConditionalExpression(inner)) {
|
||||
const branch = (b: ts.Expression) => lowerer.mapTypeOf(lowerer.typeOf(b))?.kind;
|
||||
if (branch(inner.whenTrue) === "f64" && branch(inner.whenFalse) === "f64")
|
||||
return number(lowerer.lowerExprExpecting(node, F64));
|
||||
return { value: lowerer.lowerExprExpecting(node, DYN), convert: dynToNumber, runsCode: true };
|
||||
}
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (isOptionalNumber(value)) return lowerOptionalNumber(lowerer, value, loc, inner);
|
||||
return lowerer.coerceInto(node, value, F64);
|
||||
if (isOptionalNumber(value)) return number(lowerOptionalNumber(lowerer, value, loc, inner));
|
||||
// Other values convert with ToNumber: strings parse as numeric literals,
|
||||
// booleans and null are numbers, and checked-dynamic values call
|
||||
// valueOf/toString or throw Node's TypeError for a Symbol or BigInt.
|
||||
const num = (n: number): IrExpr => ({ kind: "numLit", value: n, type: F64, loc });
|
||||
const pure = (convert: (value: IrExpr) => IrExpr): NumberOperand => ({
|
||||
value,
|
||||
convert,
|
||||
runsCode: false,
|
||||
});
|
||||
switch (value.type.kind) {
|
||||
case "string":
|
||||
return pure((v) => ({ kind: "libCall", fn: "num.fromString", args: [v], type: F64, loc }));
|
||||
case "bool":
|
||||
return pure((v) => ({
|
||||
kind: "ternary",
|
||||
cond: v,
|
||||
then: num(1),
|
||||
else_: num(0),
|
||||
type: F64,
|
||||
loc,
|
||||
}));
|
||||
case "nullT":
|
||||
return pure((v) => defaultAfterUndefined(v, num(0)));
|
||||
case "undefinedT":
|
||||
return pure((v) => defaultAfterUndefined(v, num(NaN)));
|
||||
case "dyn":
|
||||
return { value, convert: dynToNumber, runsCode: true };
|
||||
case "union":
|
||||
if (lowerer.dynConvertible(value.type))
|
||||
return {
|
||||
value: lowerer.coerceToExpected(value, DYN),
|
||||
convert: dynToNumber,
|
||||
runsCode: true,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return number(lowerer.coerceInto(node, value, F64));
|
||||
}
|
||||
|
||||
/** The stored value of an identifier whose storage holds an undefined arm
|
||||
|
||||
@@ -2,7 +2,7 @@ import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerToNumberArgument } from "./lower-exprs.js";
|
||||
import { lowerToNumberArguments } from "./lower-exprs.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
|
||||
import {
|
||||
@@ -2415,8 +2415,14 @@ export function lowerStdlibMethodCall(
|
||||
const staticMath = isMath ? own(STATIC_MATH_FNS, name) : undefined;
|
||||
// A missing element (a hole bound by a loop, a local or field holding a
|
||||
// missing read, or the read itself) converts to NaN, ToNumber's answer
|
||||
// for undefined, instead of trusting the checker's number type.
|
||||
const mathArg = (a: ts.Expression): IrExpr => lowerToNumberArgument(lowerer, a);
|
||||
// for undefined, instead of trusting the checker's number type. The
|
||||
// arguments are all evaluated before any converts (lowerToNumberArguments).
|
||||
const mathArgs = (
|
||||
nodes: readonly ts.Expression[],
|
||||
lowerOther?: (node: ts.Expression) => IrExpr | null,
|
||||
) => lowerToNumberArguments(lowerer, nodes, lowerOther);
|
||||
const afterArguments = (stmts: IrStmt[], result: IrExpr): IrExpr =>
|
||||
stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
// Math.hypot, and Math.max/Math.min with a spread among other arguments
|
||||
// (`Math.max(...xs, 5)`): the arguments in order, spreads flattened, as
|
||||
// one number[] the runtime folds.
|
||||
@@ -2427,8 +2433,8 @@ export function lowerStdlibMethodCall(
|
||||
call.arguments.nodes.length > 1 &&
|
||||
call.arguments.nodes.some(ts.isSpreadElement)))
|
||||
) {
|
||||
const elems = call.arguments.nodes.map((a) =>
|
||||
ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : mathArg(a),
|
||||
const { stmts, values: elems } = mathArgs(call.arguments.nodes, (a) =>
|
||||
ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : null,
|
||||
);
|
||||
const spreads = call.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : []));
|
||||
const packed: IrExpr = {
|
||||
@@ -2439,7 +2445,7 @@ export function lowerStdlibMethodCall(
|
||||
loc,
|
||||
};
|
||||
const fn = name === "max" ? "math.maxArr" : name === "min" ? "math.minArr" : staticMath.fn;
|
||||
return { kind: "libCall", fn, args: [packed], type: F64, loc };
|
||||
return afterArguments(stmts, { kind: "libCall", fn, args: [packed], type: F64, loc });
|
||||
}
|
||||
if (staticMath && call.arguments.nodes.every((a) => !ts.isSpreadElement(a))) {
|
||||
// Math.max/Math.min at ANY plain arity — Node's are variadic. The
|
||||
@@ -2455,16 +2461,16 @@ export function lowerStdlibMethodCall(
|
||||
if (call.arguments.nodes.length === 0) {
|
||||
return { kind: "numLit", value: name === "max" ? -Infinity : Infinity, type: F64, loc };
|
||||
}
|
||||
const args = call.arguments.nodes.map(mathArg);
|
||||
const { stmts, values: args } = mathArgs(call.arguments.nodes);
|
||||
let acc = args[0]!;
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
acc = { kind: "libCall", fn: staticMath.fn, args: [acc, args[i]!], type: F64, loc };
|
||||
}
|
||||
return acc;
|
||||
return afterArguments(stmts, acc);
|
||||
}
|
||||
if (call.arguments.nodes.length === staticMath.arity) {
|
||||
const args = call.arguments.nodes.map(mathArg);
|
||||
return { kind: "libCall", fn: staticMath.fn, args, type: F64, loc };
|
||||
const { stmts, values: args } = mathArgs(call.arguments.nodes);
|
||||
return afterArguments(stmts, { kind: "libCall", fn: staticMath.fn, args, type: F64, loc });
|
||||
}
|
||||
}
|
||||
// `Math.max(...xs)` / `Math.min(...xs)` over a number[]: a STATIC
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6905-isfinite-coercion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6906-tonumber-arguments.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Global isFinite applies ToNumber to its argument before the test, like
|
||||
// global isNaN: strings parse as numeric literals, booleans and null are 0
|
||||
// and 1, undefined is NaN, and objects convert through valueOf/toString.
|
||||
// Number.isFinite stays strict and answers false for anything not a number.
|
||||
const values: unknown[] = ["12", "", " 0x10 ", "abc", "1e400", true, false, null, undefined, {}, [], [7], [1, 2]];
|
||||
for (const value of values) {
|
||||
console.log(JSON.stringify(value) ?? "undefined", isFinite(value as any), Number.isFinite(value));
|
||||
}
|
||||
|
||||
const u: unknown = 12;
|
||||
console.log(isFinite(u as any));
|
||||
|
||||
let calls = "";
|
||||
const boxed: unknown = { valueOf: () => ((calls += "v"), 5) };
|
||||
console.log(isFinite(boxed as any), calls);
|
||||
const text: unknown = { toString: () => "Infinity" };
|
||||
console.log(isFinite(text as any));
|
||||
|
||||
class Cache {
|
||||
#maxAge: unknown;
|
||||
constructor(maxAge?: number) {
|
||||
this.#maxAge = maxAge;
|
||||
}
|
||||
expires(): boolean {
|
||||
return isFinite(this.#maxAge as any);
|
||||
}
|
||||
}
|
||||
console.log(new Cache(1000).expires(), new Cache().expires());
|
||||
|
||||
const throwing: unknown[] = [Symbol("s"), 10n];
|
||||
for (const value of throwing) {
|
||||
try {
|
||||
isFinite(value as any);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError);
|
||||
}
|
||||
}
|
||||
console.log(isFinite(1 / 0), isFinite(-1 / 0), isFinite(0 / 0), isFinite(-0));
|
||||
@@ -0,0 +1,97 @@
|
||||
// Arguments that the global number functions convert with ToNumber, in a
|
||||
// JavaScript file where they arrive as strings, booleans, null, undefined
|
||||
// or dynamic values: isNaN, isFinite, Math functions and
|
||||
// String.fromCharCode match Node for each.
|
||||
console.log(isNaN("12"), isNaN("abc"), isNaN(true), isNaN(null), isNaN(undefined));
|
||||
console.log(isFinite("12"), isFinite("abc"), isFinite(true), isFinite(null), isFinite(undefined));
|
||||
console.log(Math.floor("3.7"), Math.max("4", 2), Math.abs(false), Math.sqrt(null), String.fromCharCode("65", true));
|
||||
|
||||
const parsed = JSON.parse('["12", " 0x10 ", "", "1e400", true, null, {}, [], [7]]');
|
||||
for (const value of parsed) console.log(JSON.stringify(value), isNaN(value), isFinite(value), Math.trunc(value));
|
||||
|
||||
let calls = "";
|
||||
const boxed = { valueOf: () => ((calls += "v"), 4.5), toString: () => ((calls += "s"), "9") };
|
||||
console.log(isFinite(boxed), isNaN(boxed), Math.round(boxed), calls);
|
||||
|
||||
class LruCache {
|
||||
constructor(config) {
|
||||
this.maxAge = config.maxAge;
|
||||
}
|
||||
hasMaxAge() {
|
||||
return isFinite(this.maxAge);
|
||||
}
|
||||
}
|
||||
console.log(new LruCache({ maxAge: 1000 }).hasMaxAge(), new LruCache({}).hasMaxAge());
|
||||
|
||||
for (const value of [Symbol("s"), 10n]) {
|
||||
try {
|
||||
isFinite(value);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// A conditional argument converts whichever branch it takes.
|
||||
for (const flag of [true, false]) {
|
||||
console.log(isFinite(flag ? "12" : "abc"), isNaN(flag ? null : "x"), Math.floor(flag ? "3.7" : true));
|
||||
}
|
||||
|
||||
// Every argument is evaluated before any converts, as in Node: valueOf runs
|
||||
// after the last argument, and an argument that throws stops the call
|
||||
// before any conversion.
|
||||
let order = "";
|
||||
const valued = (name, n) => ({ valueOf: () => ((order += "v" + name), n) });
|
||||
const arg = (name, n) => ((order += "e" + name), valued(name, n));
|
||||
console.log(Math.max(arg("a", 1), arg("b", 5), arg("c", 3)), order);
|
||||
order = "";
|
||||
console.log(Math.pow(arg("a", 2), arg("b", 10)), order);
|
||||
order = "";
|
||||
console.log(Math.hypot(arg("a", 3), ...[4], arg("c", 12)), order);
|
||||
order = "";
|
||||
console.log(String.fromCharCode(arg("a", 72), arg("b", 105)), order);
|
||||
order = "";
|
||||
try {
|
||||
Math.max(
|
||||
arg("a", 1),
|
||||
(() => {
|
||||
order += "throw";
|
||||
throw new Error("boom");
|
||||
})(),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e.message, order);
|
||||
}
|
||||
order = "";
|
||||
try {
|
||||
Math.min(
|
||||
{
|
||||
valueOf() {
|
||||
order += "x";
|
||||
throw new TypeError("first");
|
||||
},
|
||||
},
|
||||
arg("b", 2),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e.message, order);
|
||||
}
|
||||
|
||||
// A spread reads its elements where it is evaluated, before an earlier
|
||||
// argument's valueOf runs.
|
||||
const values = [1, 2];
|
||||
const mutator = {
|
||||
valueOf() {
|
||||
values[0] = 100;
|
||||
values.push(50);
|
||||
return 3;
|
||||
},
|
||||
};
|
||||
console.log(Math.max(mutator, ...values), values.join(","));
|
||||
const more = [4];
|
||||
const resetter = {
|
||||
valueOf() {
|
||||
more[0] = -1;
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
console.log(Math.hypot(resetter, ...more, 3), more.join(","));
|
||||
Reference in New Issue
Block a user