mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep aliases of evolving empty arrays on the array they alias (#1812)
This commit is contained in:
@@ -4799,9 +4799,10 @@ export function lowerVarDeclList(
|
||||
* `never[]` or `any[]` and each later read by the elements written before
|
||||
* it, and reads inside closures as the unsettled array. When every read
|
||||
* with a settled element type maps to the same array type, the binding has
|
||||
* that type. When reads disagree or the binding is reassigned, it is
|
||||
* checked-dynamic, as the same binding is at module scope. Null for other
|
||||
* bindings. */
|
||||
* that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`,
|
||||
* `xs.push(...values)`, `xs[i] = v`) stores a value that type's element does
|
||||
* not hold, or the binding is reassigned, it is checked-dynamic, as the same
|
||||
* binding is at module scope. Null for other bindings. */
|
||||
function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
||||
let initializer: ts.Expression = decl.initializer;
|
||||
@@ -4823,6 +4824,9 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
};
|
||||
const scope = owner(decl);
|
||||
let result: IrType | null = null;
|
||||
// The values the binding's writes store, checked against the element
|
||||
// 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.
|
||||
for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
|
||||
@@ -4842,6 +4846,22 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
||||
if (reassigned) return DYN;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === target &&
|
||||
(parent.name.text === "push" || parent.name.text === "unshift") &&
|
||||
ts.isCallExpression(parent.parent) &&
|
||||
parent.parent.expression === parent
|
||||
)
|
||||
written.push(...parent.parent.arguments.nodes);
|
||||
else if (
|
||||
ts.isElementAccessExpression(parent) &&
|
||||
parent.expression === target &&
|
||||
ts.isBinaryExpression(parent.parent) &&
|
||||
parent.parent.left === parent &&
|
||||
parent.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
written.push(parent.parent.right);
|
||||
const t = lowerer.typeOf(node);
|
||||
if (unsettled(t)) continue;
|
||||
const mapped = lowerer.mapTypeOf(t);
|
||||
@@ -4849,7 +4869,49 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
return DYN;
|
||||
result = mapped;
|
||||
}
|
||||
return result ?? DYN;
|
||||
if (result === null || result.kind !== "array") return DYN;
|
||||
// The element types a write stores: its value's, or for a spread
|
||||
// (`xs.push(...values)`) the source array's element or tuple's elements.
|
||||
// Another spread source stores elements this check cannot see.
|
||||
const storedTypes = (value: ts.Expression): ts.Type[] | null => {
|
||||
if (!ts.isSpreadElement(value)) return [lowerer.typeOf(value)];
|
||||
const source = lowerer.typeOf(value.expression);
|
||||
if (lowerer.checker.isArrayType(source) || lowerer.checker.isTupleType(source))
|
||||
return [...lowerer.checker.getTypeArguments(source as ts.TypeReference)];
|
||||
return null;
|
||||
};
|
||||
for (const value of written) {
|
||||
const stored = storedTypes(value);
|
||||
if (stored === null) return DYN;
|
||||
for (const type of stored) {
|
||||
const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(type));
|
||||
if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null))
|
||||
return DYN;
|
||||
}
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A single static type for an initializer-less JavaScript `let` whose
|
||||
@@ -5565,7 +5627,7 @@ export function lowerVarDecl(
|
||||
!decl.type &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
) {
|
||||
const evolved = evolvingArrayType(lowerer, decl);
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl);
|
||||
if (evolved !== null) {
|
||||
type = evolved;
|
||||
init = lowerer.lowerExprExpecting(decl.initializer, evolved);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7006-evolving-array-aliases.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// An unannotated `const xs = []` inside a function whose writes store values
|
||||
// of more than one element type: an alias taken between the writes
|
||||
// (`const alias = xs`) and an alias of that alias name the same array, so
|
||||
// they see every later write, as in Node. A write of another element type
|
||||
// after reads that agree on one type also keeps the array untyped.
|
||||
function chain(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const a = xs;
|
||||
const b = a;
|
||||
xs.push("two");
|
||||
b.push(3);
|
||||
return `${xs.join(",")} ${a.length} ${b.length} ${a === xs} ${b === xs}`;
|
||||
}
|
||||
function viaAlias(): string {
|
||||
const xs = [];
|
||||
xs.push("a");
|
||||
const alias = xs;
|
||||
alias.push("b");
|
||||
xs.push(2);
|
||||
return `${xs.join(",")} ${alias.join(",")}`;
|
||||
}
|
||||
function letAlias(): string {
|
||||
const xs = [];
|
||||
xs.push(true);
|
||||
let alias = xs;
|
||||
xs.push("t");
|
||||
const seen = alias.length;
|
||||
alias = [];
|
||||
return `${seen} ${alias.length} ${xs.length}`;
|
||||
}
|
||||
function nested(flag: boolean): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
if (flag) {
|
||||
const inner = xs;
|
||||
xs.push("n");
|
||||
return `${inner.length} ${inner.join(":")}`;
|
||||
}
|
||||
return `${xs.length}`;
|
||||
}
|
||||
function elementWrite(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const alias = xs;
|
||||
xs[1] = "one";
|
||||
return `${alias.length} ${alias.join("+")}`;
|
||||
}
|
||||
function agreeing(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const alias = xs;
|
||||
xs.push(2);
|
||||
return `${alias.length} ${alias === xs} ${alias.reduce((sum, v) => sum + v, 0)}`;
|
||||
}
|
||||
function total(values: number[]): number {
|
||||
return values.length;
|
||||
}
|
||||
function passed(): string {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const alias = xs;
|
||||
const n = total(alias);
|
||||
xs.push("z");
|
||||
return `${n} ${alias.length}`;
|
||||
}
|
||||
console.log(chain());
|
||||
console.log(viaAlias(), "|", letAlias());
|
||||
console.log(nested(true), "|", nested(false), "|", elementWrite());
|
||||
console.log(agreeing(), "|", passed());
|
||||
|
||||
function spreadSame(): number {
|
||||
const xs = [];
|
||||
xs.push(1);
|
||||
const more = [2, 3];
|
||||
xs.push(...more);
|
||||
return xs.reduce((sum, v) => sum + v, 0);
|
||||
}
|
||||
console.log(spreadSame());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2292, "san": 3445 }
|
||||
Reference in New Issue
Block a user