mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Read typed-array elements through non-null assertions as undefined where Node does (#2067)
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
lowerDynMemberAssignment,
|
||||
lowerOptionalArrayIndexRead,
|
||||
lowerOptionalNumber,
|
||||
assertedReadObservesUndefined,
|
||||
recordKeyResultOk,
|
||||
representedClassFieldTarget,
|
||||
runtimeOptionalElementKey,
|
||||
@@ -381,13 +382,18 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
if (index.type.kind !== "f64") {
|
||||
lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys");
|
||||
}
|
||||
// `t[i]!` asserts presence: the numeric element read (NaN when
|
||||
// invalid). Every other read answers undefined for an invalid index,
|
||||
// like Node.
|
||||
// `t[i]!` performs no runtime check, but its static type is a number:
|
||||
// it takes the numeric element read (NaN when invalid), except where
|
||||
// the value reaches a use that tells undefined from NaN (an equality, a
|
||||
// string conversion, `typeof`, an array element), which reads undefined
|
||||
// for an invalid index like `t[i]`. Every other read answers undefined
|
||||
// for an invalid index, like Node.
|
||||
// A read whose every consumer treats undefined like NaN (see
|
||||
// nan-coded-reads.ts) is the same plain numeric read.
|
||||
if (
|
||||
(ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) ||
|
||||
(ts.isNonNullExpression(expr.parent) &&
|
||||
expr.parent.expression === expr &&
|
||||
!assertedReadObservesUndefined(lowerer, expr.parent)) ||
|
||||
lowerer.nanCodedReads?.isNumericRead(expr) === true
|
||||
) {
|
||||
return {
|
||||
|
||||
@@ -1024,13 +1024,14 @@ function assertionOperand(
|
||||
)
|
||||
)
|
||||
return null;
|
||||
// Array and typed-array element reads keep their own handling of a
|
||||
// missing element.
|
||||
// Array element reads keep their own handling of a missing element. A
|
||||
// typed-array read is optional here only where its use tells undefined
|
||||
// from NaN (assertedReadObservesUndefined).
|
||||
let operand: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
if (ts.isElementAccessExpression(operand)) {
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(operand.expression))?.kind;
|
||||
if (kind === "array" || kind === "bytes") return null;
|
||||
if (kind === "array") return null;
|
||||
}
|
||||
const use = convertingUseOf(lowerer, expr);
|
||||
if (use === null) return null;
|
||||
@@ -1047,6 +1048,7 @@ function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number"
|
||||
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
|
||||
const parent = outer.parent;
|
||||
if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "value" : null;
|
||||
if (ts.isTypeOfExpression(parent)) return "value";
|
||||
if (ts.isPrefixUnaryExpression(parent)) {
|
||||
if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value";
|
||||
return parent.operator === ts.SyntaxKind.MinusToken ||
|
||||
@@ -1143,6 +1145,23 @@ const NUMERIC_COMPOUNDS = new Set<ts.SyntaxKind>([
|
||||
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
||||
]);
|
||||
|
||||
/** Whether the use of `t[i]!` tells undefined from NaN: a converting use
|
||||
* that takes the value itself (see convertingUseOf), or a destination that
|
||||
* holds a missing element (see assertedElementKeepsUndefined) other than a
|
||||
* return, whose slot has the function's number type. */
|
||||
export function assertedReadObservesUndefined(
|
||||
lowerer: Lowerer,
|
||||
node: ts.NonNullExpression,
|
||||
): boolean {
|
||||
if (convertingUseOf(lowerer, node) === "value") return true;
|
||||
let outer: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
|
||||
const parent = outer.parent;
|
||||
if (ts.isReturnStatement(parent) || (ts.isArrowFunction(parent) && parent.body === outer))
|
||||
return false;
|
||||
return assertedElementKeepsUndefined(lowerer, node);
|
||||
}
|
||||
|
||||
export function runtimeOptionalAssertionErases(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
|
||||
@@ -6488,6 +6488,22 @@ export class Lowerer {
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
// `t[i]!` on a typed array, stored into an array, keeps an invalid
|
||||
// index's undefined (assertedReadObservesUndefined): `!` performs no
|
||||
// runtime check. A read whose consumers treat undefined like NaN stores
|
||||
// NaN instead.
|
||||
const assertedReadMayBeUndefined = (node: ts.Expression): boolean => {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isNonNullExpression(e)) return false;
|
||||
let read = e.expression;
|
||||
while (ts.isParenthesizedExpression(read)) read = read.expression;
|
||||
return (
|
||||
ts.isElementAccessExpression(read) &&
|
||||
this.mapTypeOf(this.typeOf(read.expression))?.kind === "bytes" &&
|
||||
!nanCoded.isNumericRead(read)
|
||||
);
|
||||
};
|
||||
const elementStates = new ArrayElementStates({
|
||||
elementAdmitsUndefined: (key) => {
|
||||
siteDependencies?.read(key);
|
||||
@@ -6497,7 +6513,7 @@ export class Lowerer {
|
||||
allocationOwner: (node) => ownership.allocationOwner(node),
|
||||
arrayKeys: arrayKeysOf,
|
||||
mayBeUndefined: (node) => {
|
||||
if (explicitlyNonNull(node)) return false;
|
||||
if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node);
|
||||
if (mayBeOptional(node)) return true;
|
||||
// `undefined as unknown as T` keeps the undefined value.
|
||||
for (
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7394-non-null-typed-array-reads.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Non-null assertions on typed-array element reads. `!` performs no
|
||||
// runtime check: an index outside the array reads undefined, as in Node,
|
||||
// wherever the use tells undefined from NaN (equality, string conversion,
|
||||
// typeof, Object.is, an array element), and NaN where it converts the value
|
||||
// to a number.
|
||||
|
||||
const int32 = new Int32Array([1, -2, 3]);
|
||||
const float64 = new Float64Array([1.5, NaN, -0]);
|
||||
const uint8 = new Uint8Array([7, 255]);
|
||||
|
||||
function show(v: unknown): string {
|
||||
return Object.is(v, -0) ? "-0" : String(v);
|
||||
}
|
||||
|
||||
function line(label: string, parts: unknown[]): void {
|
||||
console.log(label.padEnd(12), parts.map(show).join(" "));
|
||||
}
|
||||
|
||||
for (const k of [0, 1, 2, 3, -1, 1.5]) {
|
||||
line(`int32 ${k}`, [
|
||||
int32[k]! === int32[k]!,
|
||||
int32[k]! !== int32[0]!,
|
||||
int32[k]! === undefined,
|
||||
int32[k]! == null,
|
||||
Object.is(int32[k]!, undefined),
|
||||
typeof int32[k]!,
|
||||
`${int32[k]!}`,
|
||||
"v=" + int32[k]!,
|
||||
String(int32[k]!),
|
||||
int32[k]! + 1,
|
||||
int32[k]! < 2,
|
||||
[int32[k]!].length,
|
||||
]);
|
||||
line(`float64 ${k}`, [
|
||||
float64[k]! === float64[k]!,
|
||||
float64[k]! === undefined,
|
||||
Object.is(float64[k]!, NaN),
|
||||
typeof float64[k]!,
|
||||
`${float64[k]!}`,
|
||||
float64[k]! * 2,
|
||||
-float64[k]!,
|
||||
]);
|
||||
line(`uint8 ${k}`, [
|
||||
uint8[k]! === uint8[k]!,
|
||||
uint8[k]! == undefined,
|
||||
typeof uint8[k]!,
|
||||
`${uint8[k]!}`,
|
||||
uint8[k]! | 0,
|
||||
uint8[k]! > 100,
|
||||
]);
|
||||
}
|
||||
|
||||
const optional: number | undefined = uint8[5]!;
|
||||
const listed = [int32[9]!, float64[0]!];
|
||||
line("slots", [optional, listed[0], listed[1], listed.length]);
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i <= int32.length; i++) total += int32[i]!;
|
||||
line("sum", [total]);
|
||||
|
||||
// Arrays that store an asserted read past the end hold undefined, which
|
||||
// their callbacks see.
|
||||
const pushed: number[] = [];
|
||||
pushed.push(int32[7]!);
|
||||
pushed.push(uint8[1]!);
|
||||
const literal = [float64[5]!, float64[0]!];
|
||||
const assigned: number[] = [1, 2];
|
||||
assigned[0] = uint8[9]!;
|
||||
let visited = 0;
|
||||
pushed.forEach((v) => {
|
||||
visited += v;
|
||||
});
|
||||
line("callbacks", [
|
||||
pushed.map((v) => v * 2).join(","),
|
||||
visited,
|
||||
literal.map((v) => typeof v).join(","),
|
||||
literal.filter((v) => v === undefined).length,
|
||||
assigned.map((v) => v + 1).join(","),
|
||||
assigned.some((v) => v === undefined),
|
||||
]);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4300, "san": 3900 }
|
||||
Reference in New Issue
Block a user