mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep object and array literals that hold an evolving empty array on that array
An object or array literal that held an evolving `const xs = []` between
writes of different element types (`const box = { items: xs }` or
`const rows = [xs]` after `xs.push(1)`, before `xs.push("two")`) took the
element type the checker gives the array at that point, so it held a copy.
`box.items.length` and `box.items === xs` printed `1 false`, where Node
prints `2 true`. Such a literal now builds as a checked-dynamic literal that
keeps the array, and an unannotated binding of it takes the checked-dynamic
type. This covers shorthand properties (`{ xs }`), nested literals, spreads
of a literal holder, aliases of a holder and names destructured from one.
Element reads through an alias or holder (`rows[0]`, `alias[1]`) read the
checked-dynamic value instead of checking it against the checker's type,
which lists only the elements written before the alias or holder was taken.
A shorthand property now counts as a read of the array when its reads are
compared, so `{ xs }` before a write of another element type makes the
array checked-dynamic too.
Fixes #1641.
This commit is contained in:
@@ -65,6 +65,7 @@ import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
|
||||
import { isClassCallback } from "../class-callbacks.js";
|
||||
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
|
||||
import { lowerFieldCompoundValue } from "./updates.js";
|
||||
import { readsHeldEvolvingArray } from "../lower-stmts.js";
|
||||
|
||||
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
||||
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
||||
@@ -751,12 +752,16 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
: lowerer.maybeNarrow(arr, expr.expression);
|
||||
}
|
||||
|
||||
// An alias or container of an evolving array whose reads disagree
|
||||
// (`rows[0]` after `const rows = [xs]`) holds elements the checker's type
|
||||
// does not list, so its element read stays checked-dynamic.
|
||||
const heldEvolving = arr.type.kind === "dyn" && readsHeldEvolvingArray(lowerer, expr.expression);
|
||||
// The checker sees an array but the VALUE lowered checked-dynamic (a
|
||||
// jsdoc-typed default export from a .js module — signature 14): the
|
||||
// read rides the dynCheck boundary when the array shape is one the checked-dynamic tree
|
||||
// can validate, and fences by name when it is not — an arrayGet over a
|
||||
// dyn receiver is never emitted (the validator ICE).
|
||||
if (arr.type.kind === "dyn") {
|
||||
if (arr.type.kind === "dyn" && !heldEvolving) {
|
||||
if (
|
||||
canDynCheckTo(
|
||||
receiverIr,
|
||||
@@ -782,6 +787,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
locOf(expr.argumentExpression),
|
||||
expr.argumentExpression,
|
||||
);
|
||||
if (heldEvolving)
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: arr,
|
||||
key: lowerer.coerceToExpected(index, DYN),
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
if (
|
||||
arr.type.kind === "array" &&
|
||||
(index.type.kind === "dyn" ||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { fenceSymbolFieldCopy } from "../symbol-fields.js";
|
||||
import { planUnionRetag } from "../union-retag.js";
|
||||
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
|
||||
import { builtinGlobalValue } from "./property-access.js";
|
||||
import { holdsEvolvingArray } from "../lower-stmts.js";
|
||||
|
||||
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
|
||||
* contextual type when tsc has one (annotated declarations, arguments,
|
||||
@@ -397,7 +398,10 @@ export function lowerDynObjectLiteral(
|
||||
? lowerer.lowerLambda(prop, true)
|
||||
: ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop)
|
||||
? shorthandBuiltinValue(lowerer, prop)!
|
||||
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
|
||||
: !boxValue &&
|
||||
(ts.isObjectLiteralExpression(valueExpr) ||
|
||||
(ts.isArrayLiteralExpression(valueExpr) &&
|
||||
holdsEvolvingArray(lowerer, valueExpr)))
|
||||
? lowerer.lowerExprExpecting(valueExpr, DYN)
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
|
||||
|
||||
@@ -4920,12 +4920,16 @@ export function bindPatternTarget(
|
||||
}
|
||||
// Preserve checked-value pattern bindings when the inferred type
|
||||
// cannot represent them without a refusal or an alias-breaking copy.
|
||||
// A name destructured from a container of an evolving array
|
||||
// (evolvingArrayHold) may hold the array, which the checker types by
|
||||
// the elements written before it.
|
||||
const dynStays =
|
||||
value.type.kind === "dyn" &&
|
||||
(() => {
|
||||
((() => {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(name));
|
||||
return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped);
|
||||
})();
|
||||
})() ||
|
||||
readsHeldEvolvingArray(lowerer, name));
|
||||
let type = dynStays
|
||||
? DYN
|
||||
: (lowerer.runtimeOptionalBindingType(name, lowerer.irTypeOf(name)) ??
|
||||
@@ -5054,13 +5058,16 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
// type once the reads settle it.
|
||||
const written: ts.Expression[] = [];
|
||||
// The file's identifiers are indexed by name once (identifiersNamed), so
|
||||
// each binding resolves only the uses spelled like it.
|
||||
// each binding resolves only the uses spelled like it. A shorthand
|
||||
// property (`{ xs }`) is a use too.
|
||||
for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
|
||||
if (
|
||||
node === decl.name ||
|
||||
node.pos < scope.pos ||
|
||||
node.end > scope.end ||
|
||||
lowerer.checker.getSymbolAtLocation(node) !== symbol
|
||||
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
||||
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
|
||||
: lowerer.checker.getSymbolAtLocation(node)) !== symbol
|
||||
)
|
||||
continue;
|
||||
let target: ts.Node = node;
|
||||
@@ -5118,28 +5125,105 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
return result;
|
||||
}
|
||||
|
||||
/** An unannotated alias of an evolving array binding whose reads disagree
|
||||
* (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the
|
||||
* checker types the alias by the elements written before it, but both names
|
||||
* hold one array, so the alias takes the binding's checked-dynamic type and
|
||||
* sees the later writes. Aliases of such an alias follow it. Null for other
|
||||
* bindings. */
|
||||
function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
let current = decl;
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
let initializer = current.initializer;
|
||||
while (initializer !== undefined && ts.isParenthesizedExpression(initializer))
|
||||
initializer = initializer.expression;
|
||||
if (initializer === undefined || !ts.isIdentifier(initializer)) return null;
|
||||
const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration;
|
||||
/** How a TypeScript expression holds an evolving array binding whose reads
|
||||
* disagree (evolvingArrayType answers checked-dynamic): "evolving" for the
|
||||
* binding itself, "alias" for an unannotated binding initialized with it or
|
||||
* with an alias of it (`const alias = xs`), and "container" for an object or
|
||||
* array literal with it as a property, element or spread source at any
|
||||
* depth (`{ items: xs }`, `[xs]`), an unannotated binding initialized with
|
||||
* or destructured from such a literal, and a property or element read of a
|
||||
* container (`box.items`). The checker types an alias or container by the
|
||||
* elements written before it was taken, but it holds the same array. Null
|
||||
* for other expressions. */
|
||||
function evolvingArrayHold(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
depth = 0,
|
||||
): "evolving" | "alias" | "container" | null {
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (depth >= 8) return null;
|
||||
const binding = (symbol: ts.Symbol | undefined): "evolving" | "alias" | "container" | null => {
|
||||
const source = symbol?.valueDeclaration;
|
||||
// A name destructured from a container (`const { items } = box`) may
|
||||
// be the array or another container.
|
||||
if (source !== undefined && ts.isBindingElement(source) && ts.isIdentifier(source.name)) {
|
||||
const host = hostVariableDeclarationOf(source.name);
|
||||
return host !== null &&
|
||||
!host.type &&
|
||||
host.initializer !== undefined &&
|
||||
evolvingArrayHold(lowerer, host.initializer, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
}
|
||||
if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null;
|
||||
const evolved = evolvingArrayType(lowerer, source);
|
||||
if (evolved !== null) return evolved.kind === "dyn" ? evolved : null;
|
||||
current = source;
|
||||
if (evolved !== null) return evolved.kind === "dyn" ? "evolving" : null;
|
||||
if (source.initializer === undefined) return null;
|
||||
const held = evolvingArrayHold(lowerer, source.initializer, depth + 1);
|
||||
return held === "evolving" ? "alias" : held;
|
||||
};
|
||||
if (ts.isIdentifier(expr)) return binding(lowerer.checker.getSymbolAtLocation(expr));
|
||||
if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr))
|
||||
return evolvingArrayHold(lowerer, expr.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
// A spread of the array itself (`{ ...xs }`, `[...xs]`) copies its
|
||||
// elements, as in Node; a spread of a container copies its references.
|
||||
if (ts.isObjectLiteralExpression(expr)) {
|
||||
for (const property of expr.properties.nodes) {
|
||||
const held = ts.isPropertyAssignment(property)
|
||||
? evolvingArrayHold(lowerer, property.initializer, depth + 1)
|
||||
: ts.isShorthandPropertyAssignment(property)
|
||||
? binding(lowerer.checker.getShorthandAssignmentValueSymbol(property))
|
||||
: ts.isSpreadAssignment(property) &&
|
||||
evolvingArrayHold(lowerer, property.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
if (held !== null) return "container";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(expr)) {
|
||||
for (const element of expr.elements.nodes) {
|
||||
const held = ts.isSpreadElement(element)
|
||||
? evolvingArrayHold(lowerer, element.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null
|
||||
: evolvingArrayHold(lowerer, element, depth + 1);
|
||||
if (held !== null) return "container";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a TypeScript expression is an alias or container of an evolving
|
||||
* array binding whose reads disagree (evolvingArrayHold). Its value is
|
||||
* checked-dynamic, and the checker's type for it lists only the elements
|
||||
* written before it was taken, so element reads through it stay
|
||||
* checked-dynamic instead of checking against that type. */
|
||||
export function readsHeldEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
const held = evolvingArrayHold(lowerer, expr);
|
||||
return held === "alias" || held === "container";
|
||||
}
|
||||
|
||||
/** Whether a TypeScript object or array literal holds an evolving array
|
||||
* binding whose reads disagree (evolvingArrayHold), so it builds as a
|
||||
* checked-dynamic literal that keeps the array. */
|
||||
export function holdsEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
return evolvingArrayHold(lowerer, expr) === "container";
|
||||
}
|
||||
|
||||
/** An unannotated binding initialized with an evolving array binding whose
|
||||
* reads disagree, or with an alias or container of one (evolvingArrayHold):
|
||||
* it takes the array's checked-dynamic type, so it sees the later writes.
|
||||
* Null for other bindings. */
|
||||
function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
if (decl.initializer === undefined) return null;
|
||||
return evolvingArrayHold(lowerer, decl.initializer) === null ? null : DYN;
|
||||
}
|
||||
|
||||
/** A single static type for an initializer-less JavaScript `let` whose
|
||||
* later plain writes all agree. An evolving JS local reads as `undefined`
|
||||
* until control flow proves a write, so a scalar slot is sound only when
|
||||
@@ -5834,7 +5918,7 @@ export function lowerVarDecl(
|
||||
!decl.type &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
) {
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl);
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayHolderType(lowerer, decl);
|
||||
if (evolved !== null) {
|
||||
type = evolved;
|
||||
init = lowerer.lowerExprExpecting(decl.initializer, evolved);
|
||||
|
||||
@@ -614,6 +614,7 @@ import {
|
||||
lowerForStatement,
|
||||
matchGroupsPatternSource,
|
||||
isGuardedRecordRead,
|
||||
holdsEvolvingArray,
|
||||
} from "./lower-stmts.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
@@ -10655,7 +10656,8 @@ export class Lowerer {
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts)
|
||||
this.excessJsContextualProperty(x, parts) ||
|
||||
holdsEvolvingArray(this, x)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
}
|
||||
|
||||
@@ -77,3 +77,51 @@ function spreadSame(): number {
|
||||
return xs.reduce((sum, v) => sum + v, 0);
|
||||
}
|
||||
console.log(spreadSame());
|
||||
|
||||
// An object or array literal that holds the array between the writes
|
||||
// (`{ items: xs }`, `[xs]`, a shorthand property, a nested literal), a
|
||||
// binding of such a literal, and a name destructured from one hold the
|
||||
// same array, so they see the later writes.
|
||||
function objectHolder(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const box = { items: xs, label: "box" };
|
||||
xs.push("two");
|
||||
box.items.push(3);
|
||||
return `${box.items.length} ${box.items === xs} ${box.items[1]} ${box.label} ${xs.join(",")}`;
|
||||
}
|
||||
function arrayHolder(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const rows = [xs, [5]];
|
||||
xs.push("two");
|
||||
let total = 0;
|
||||
for (const row of rows) total += row.length;
|
||||
return `${rows[0].length} ${rows[0] === xs} ${rows[0][1]} ${total}`;
|
||||
}
|
||||
function shorthandHolder(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const box = { xs, count: 3 };
|
||||
xs.push("two");
|
||||
return `${box.xs.length} ${box.xs === xs} ${box.count}`;
|
||||
}
|
||||
function nestedHolder(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const box = { inner: [xs], pair: [{ items: xs }] };
|
||||
const outer = { box };
|
||||
xs.push("two");
|
||||
return `${box.inner[0].length} ${box.pair[0].items === xs} ${outer.box.inner[0][1]}`;
|
||||
}
|
||||
function holderAliases(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const box = { items: xs };
|
||||
const other = box;
|
||||
const { items } = box;
|
||||
xs.push("two");
|
||||
return `${other.items.length} ${items.length} ${items === xs} ${JSON.stringify(other)}`;
|
||||
}
|
||||
console.log(objectHolder(), "|", arrayHolder(), "|", shorthandHolder());
|
||||
console.log(nestedHolder(), "|", holderAliases());
|
||||
|
||||
Reference in New Issue
Block a user