Handle scalar unions in static Number conversion (#444)

This commit is contained in:
Chris Tate
2026-09-25 16:59:58 -05:00
committed by GitHub
parent a9d68b8e87
commit 951b8bc4e4
7 changed files with 107 additions and 7 deletions
@@ -3864,13 +3864,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
}
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
if (optionalNumber) return optionalNumber;
const nullishNumber = lowerNullishNumber(lowerer, arg, argNode, loc);
if (nullishNumber) return nullishNumber;
const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc);
if (scalarUnionNumber) return scalarUnionNumber;
lowerer.noLowering(
`Number of ${lowerer.fmt(arg.type)} values`,
argNode,
arg.type.kind === "union"
? "numbers, booleans, and strings lower (the full ToNumber string grammar included) — narrow the union first"
? "unions of numbers, booleans, strings, null, and undefined lower — narrow other arms first"
: undefined,
);
}
@@ -5067,12 +5067,14 @@ function lowerStringMethodCallWithOptionalArgs(
return changed ? { ...lowered, args } : lowered;
}
function lowerNullishNumber(lowerer: Lowerer, arg: IrExpr, node: ts.Expression, loc: SrcLoc): IrExpr | null {
if (arg.type.kind !== "union" || !lowerer.unions.get(arg.type.unionId)?.arms.every(isUnitType)) return null;
const key = `number.nullish:${arg.type.unionId}`;
function lowerScalarUnionNumber(lowerer: Lowerer, arg: IrExpr, node: ts.Expression, loc: SrcLoc): IrExpr | null {
if (arg.type.kind !== "union" || !lowerer.unions.get(arg.type.unionId)?.arms.every(
(arm) => arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm)
)) return null;
const key = `number.scalar:${arg.type.unionId}`;
let helper = lowerer.widthHelpers.get(key);
if (!helper) {
helper = `%number.nullish.${lowerer.widthHelpers.size}`;
helper = `%number.scalar.${lowerer.widthHelpers.size}`;
lowerer.widthHelpers.set(key, helper);
const value = varRef("value.0", arg.type, loc);
const nan: IrExpr = {
@@ -7003,6 +7003,12 @@
],
"diags": []
},
"<repo>/tests/corpus/3038-number-scalar-unions.ts": {
"order": [
"<repo>/tests/corpus/3038-number-scalar-unions.ts"
],
"diags": []
},
"<repo>/tests/corpus/304-compound-incdec.ts": {
"order": [
"<repo>/tests/corpus/304-compound-incdec.ts"
+24
View File
@@ -0,0 +1,24 @@
let calls = 0;
function candidate(index: number): number | string | boolean | null | undefined {
calls++;
if (index === 0) return -0;
if (index === 1) return 3.5;
if (index === 2) return "0x10";
if (index === 3) return true;
if (index === 4) return false;
if (index === 5) return null;
if (index === 6) return undefined;
return "invalid";
}
for (let index = 0; index < 8; index++) {
const value = Number(candidate(index));
console.log(index, String(value), String(1 / value), calls);
}
function stringOrBoolean(flag: boolean): string | boolean {
return flag ? " 7 " : false;
}
console.log(Number(stringOrBoolean(true)), Number(stringOrBoolean(false)));
+6
View File
@@ -47,6 +47,9 @@
"test/built-ins/Math/floor/S15.8.2.9_A3.js",
"test/built-ins/Number/S9.3_A1_T1.js",
"test/built-ins/Number/S9.3_A2_T1.js",
"test/built-ins/Number/S9.3_A3_T1.js",
"test/built-ins/Number/S9.3_A4.1_T1.js",
"test/built-ins/Number/S9.3_A4.2_T1.js",
"test/built-ins/Number/isNaN/nan.js",
"test/built-ins/RegExp/prototype/test/S15.10.6.3_A1_T16.js",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js",
@@ -146,6 +149,9 @@
"test/built-ins/Math/floor/S15.8.2.9_A3.js": "895ceeb2d4ae505d08e9246888c2e4a4526779d9edac16320ebf629330f56929",
"test/built-ins/Number/S9.3_A1_T1.js": "fcbdfbe9aa72ee937fb45993fce18a022bc238f0abfd0bbe033530a6dcd5c0db",
"test/built-ins/Number/S9.3_A2_T1.js": "fdcc56650335e1a4c2096afa6299ff8e4fc13453bb5bf1222b6530a565ad3fcb",
"test/built-ins/Number/S9.3_A3_T1.js": "00778de5a3600592ddff6213354aefd2e20e019823b631b8f9d5e713df0d9b0a",
"test/built-ins/Number/S9.3_A4.1_T1.js": "4f08a7fd71e72dc06e01adf249e7880a13ab1eedea4d0be3c8280aa758a45b66",
"test/built-ins/Number/S9.3_A4.2_T1.js": "fdf435ae47263f0372cce90bf25eb8ddae8d0abdc9dce30d4a3b098727a9ce6b",
"test/built-ins/Number/isNaN/nan.js": "d513a909b333f40762bc08a9e2d1435d4ca097ed8b6da76497d0201a7264589e",
"test/built-ins/RegExp/prototype/test/S15.10.6.3_A1_T16.js": "c96f0402289d55e41a2e0ef322060c5a143b599f456b94dc5ebe0493bc689f42",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js": "5b69e2580d17931baf5c9780a1372f863d52077e7e75eae8d8f9f5e09e633a14",
@@ -0,0 +1,12 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: |
Result of number conversion from boolean value is 1 if the argument is
true, else is +0
es5id: 9.3_A3_T1
description: False and true convert to Number by explicit transformation
---*/
assert.sameValue(Number(false), +0, 'Number(false) must return +0');
assert.sameValue(Number(true), 1, 'Number(true) must return 1');
@@ -0,0 +1,16 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: |
Result of number conversion from number value equals to the input
argument (no conversion)
es5id: 9.3_A4.1_T1
description: >
Some numbers including Number.MAX_VALUE and Number.MIN_VALUE are
converted to Number with explicit transformation
---*/
assert.sameValue(Number(13), 13, 'Number(13) must return 13');
assert.sameValue(Number(-13), -13, 'Number(-13) must return -13');
assert.sameValue(Number(1.3), 1.3, 'Number(1.3) must return 1.3');
assert.sameValue(Number(-1.3), -1.3, 'Number(-1.3) must return -1.3');
@@ -0,0 +1,34 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: |
Result of number conversion from number value equals to the input
argument (no conversion)
es5id: 9.3_A4.2_T1
description: >
Number.NaN, +0, -0, Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY, Number.MAX_VALUE and Number.MIN_VALUE
convert to Number by explicit transformation
---*/
// CHECK#1
assert.sameValue(Number(NaN), NaN, 'Number(true) returns NaN');
assert.sameValue(Number(+0), +0, 'Number(+0) must return +0');
assert.sameValue(Number(-0), -0, 'Number(-0) must return -0');
assert.sameValue(
Number(Number.POSITIVE_INFINITY),
Number.POSITIVE_INFINITY,
'Number(Number.POSITIVE_INFINITY) returns Number.POSITIVE_INFINITY'
);
assert.sameValue(
Number(Number.NEGATIVE_INFINITY),
Number.NEGATIVE_INFINITY,
'Number(Number.NEGATIVE_INFINITY) returns Number.NEGATIVE_INFINITY'
);
assert.sameValue(Number(Number.MAX_VALUE), Number.MAX_VALUE, 'Number(Number.MAX_VALUE) returns Number.MAX_VALUE');
assert.sameValue(Number(Number.MIN_VALUE), Number.MIN_VALUE, 'Number(Number.MIN_VALUE) returns Number.MIN_VALUE');