mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Count holes written through dynamic views of typed arrays
This commit is contained in:
@@ -6309,7 +6309,110 @@ export class Lowerer {
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
// Element ABIs of the native arrays a value converted to a dynamic type
|
||||
// can reach: the conversion keeps each such array as a live view, so
|
||||
// stores through the dynamic value write the native storage.
|
||||
const collectNativeViews = (): Set<string> => {
|
||||
const keys = new Set<string>();
|
||||
const seen = new Set<string>();
|
||||
const collect = (type: IrType | null): void => {
|
||||
if (!type) return;
|
||||
const id = typeKey(type);
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
if (type.kind === "array") {
|
||||
if (type.elem.kind !== "dyn") keys.add(typeKey(type.elem));
|
||||
collect(type.elem);
|
||||
} else if (type.kind === "union") {
|
||||
for (const arm of this.unions.get(type.unionId)?.arms ?? []) collect(arm);
|
||||
} else if (type.kind === "record") {
|
||||
for (const field of this.shapes.get(type.shapeId)?.fields ?? []) collect(field.type);
|
||||
} else if (type.kind === "object") {
|
||||
for (const field of this.classes.get(type.className)?.fields.values() ?? [])
|
||||
collect(field);
|
||||
} else if (type.kind === "map") {
|
||||
collect(type.key);
|
||||
collect(type.value);
|
||||
} else if (type.kind === "set") collect(type.elem);
|
||||
else if (type.kind === "promise") collect(type.inner);
|
||||
};
|
||||
const holdsDynamic = (type: IrType | null, depth: number): boolean => {
|
||||
if (!type || depth > 4) return false;
|
||||
if (type.kind === "dyn") return true;
|
||||
if (type.kind === "array" || type.kind === "set") return holdsDynamic(type.elem, depth + 1);
|
||||
if (type.kind === "map") return holdsDynamic(type.value, depth + 1);
|
||||
if (type.kind === "promise") return holdsDynamic(type.inner, depth + 1);
|
||||
if (type.kind === "union")
|
||||
return (this.unions.get(type.unionId)?.arms ?? []).some((arm) =>
|
||||
holdsDynamic(arm, depth + 1),
|
||||
);
|
||||
if (type.kind === "record")
|
||||
return (this.shapes.get(type.shapeId)?.fields ?? []).some((field) =>
|
||||
holdsDynamic(field.type, depth + 1),
|
||||
);
|
||||
return false;
|
||||
};
|
||||
const mapped = (type: ts.Type | null | undefined): IrType | null =>
|
||||
type ? panicSafe(() => this.mapTypeOf(type), null) : null;
|
||||
const flow = (source: ts.Expression, target: ts.Type | null | undefined): void => {
|
||||
if (target === undefined) return;
|
||||
const into = target === null ? null : mapped(target);
|
||||
// An unanswerable target may be dynamic.
|
||||
if (target !== null && into !== null && !holdsDynamic(into, 0)) return;
|
||||
const from = typeOrNull(source);
|
||||
if (from === null) keys.add("*");
|
||||
else collect(mapped(from));
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isAsExpression(node) || ts.isTypeAssertion(node))
|
||||
flow(node.expression, typeOrNull(node));
|
||||
else if (ts.isVariableDeclaration(node) && node.type && node.initializer)
|
||||
flow(node.initializer, typeOrNull(node.name));
|
||||
else if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
flow(node.right, typeOrNull(node.left));
|
||||
else if (ts.isReturnStatement(node) && node.expression)
|
||||
flow(node.expression, contextualOrNull(node.expression));
|
||||
else if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
||||
for (const arg of node.arguments?.nodes ?? [])
|
||||
flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg));
|
||||
} else if (ts.isPropertyAssignment(node))
|
||||
flow(node.initializer, contextualOrNull(node.initializer));
|
||||
else if (ts.isShorthandPropertyAssignment(node)) {
|
||||
const name = node.name as ts.Expression;
|
||||
flow(name, contextualOrNull(name));
|
||||
} else if (ts.isArrayLiteralExpression(node)) {
|
||||
for (const element of node.elements.nodes)
|
||||
if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element))
|
||||
flow(element, contextualOrNull(element));
|
||||
} else if (ts.isArrowFunction(node) && !ts.isBlock(node.body))
|
||||
flow(node.body as ts.Expression, contextualOrNull(node.body as ts.Expression));
|
||||
};
|
||||
for (const sf of sourceFiles)
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
// A checker panic hides this conversion: any array.
|
||||
if (!panicSafe(() => (visit(node), true), false)) keys.add("*");
|
||||
return undefined;
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
const dynamicType = (type: ts.Type): boolean => {
|
||||
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true;
|
||||
if (this.checker.isArrayType(type) && !this.checker.isTupleType(type)) {
|
||||
const element = this.checker.getTypeArguments(type as ts.TypeReference)[0];
|
||||
if (element && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0)
|
||||
return true;
|
||||
}
|
||||
return panicSafe(() => this.mapTypeOf(type)?.kind === "dyn", false);
|
||||
};
|
||||
const elementStates = new ArrayElementStates({
|
||||
isDynamicArray: (node) => {
|
||||
const type = typeOrNull(node);
|
||||
return type === null || dynamicType(type);
|
||||
},
|
||||
nativeViewKeys: collectNativeViews,
|
||||
elementAdmitsUndefined: (key) => {
|
||||
siteDependencies?.read(key);
|
||||
return optionalElementKeys.has(key);
|
||||
|
||||
@@ -37,6 +37,13 @@ export interface ElementStateHost {
|
||||
ownerOf(node: ts.Expression): ts.Symbol | null;
|
||||
/** The owning slot a newly built array is stored into. */
|
||||
allocationOwner(node: ts.Expression): ts.Symbol | null;
|
||||
/** `node` is a dynamic value (`any`, `unknown`, `unknown[]`): when it is
|
||||
* a live view of a native array, a store through it writes that array's
|
||||
* storage. */
|
||||
isDynamicArray(node: ts.Expression): boolean;
|
||||
/** Element keys of the native arrays a value converted to a dynamic type
|
||||
* can reach, as live views ("*" for any array). */
|
||||
nativeViewKeys(): ReadonlySet<string>;
|
||||
/** Debug provenance for a new fact. */
|
||||
note: ((fact: "undefined" | "holes", key: string, site: ts.Node | undefined) => void) | null;
|
||||
/** The scan read, or added to, the facts of an element key, of an owning
|
||||
@@ -48,6 +55,8 @@ export interface ElementStateHost {
|
||||
|
||||
/** The fact name of the facts shared by every array. */
|
||||
const ANY_ARRAY = "*";
|
||||
/** The fact name of holes stored through dynamic views. */
|
||||
const VIEW_HOLES = "%view";
|
||||
|
||||
/** What an array value can contain besides present values. */
|
||||
interface Contents {
|
||||
@@ -81,6 +90,10 @@ export class ArrayElementStates {
|
||||
* whose element ABI is decided per generic instantiation. */
|
||||
private anyUndefined = false;
|
||||
private anyHoley = false;
|
||||
/** A store through a dynamic value can leave holes; they land in every
|
||||
* native array such a value views. */
|
||||
private viewHoles = false;
|
||||
private viewKeys: ReadonlySet<string> | null = null;
|
||||
/** Facts of arrays owned by one storage slot. */
|
||||
private readonly undefinedOwners = new Set<ts.Symbol>();
|
||||
private readonly holeyOwners = new Set<ts.Symbol>();
|
||||
@@ -330,14 +343,32 @@ export class ArrayElementStates {
|
||||
this.host.read?.(ANY_ARRAY);
|
||||
if (keys === null) return null;
|
||||
for (const key of keys) this.host.read?.(key);
|
||||
this.host.read?.(VIEW_HOLES);
|
||||
return {
|
||||
undefined:
|
||||
this.anyUndefined ||
|
||||
keys.some((key) => this.undefinedKeys.has(key) || this.host.elementAdmitsUndefined(key)),
|
||||
holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key)),
|
||||
holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key) || this.viewed(key)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Holes stored through a dynamic view can be in arrays of this key. */
|
||||
private viewed(key: string): boolean {
|
||||
if (!this.viewHoles) return false;
|
||||
this.viewKeys ??= this.host.nativeViewKeys();
|
||||
return this.viewKeys.has(key) || this.viewKeys.has(ANY_ARRAY);
|
||||
}
|
||||
|
||||
/** A store through `receiver`, which has no native element key, can
|
||||
* leave holes. Only a dynamic value can be a view of a native array. */
|
||||
private addViewHoles(receiver: ts.Expression): boolean {
|
||||
if (this.viewHoles || !this.host.isDynamicArray(receiver)) return false;
|
||||
this.viewHoles = true;
|
||||
this.host.changed?.(VIEW_HOLES);
|
||||
this.host.note?.("holes", VIEW_HOLES, this.site);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Reading every index of `node` (a spread, an iteration, `Array.from`)
|
||||
* can produce `undefined`. Only native arrays can: other iterables reject
|
||||
* an undefined value when it is stored. */
|
||||
@@ -427,7 +458,11 @@ export class ArrayElementStates {
|
||||
/** A store through `receiver` can leave holes. */
|
||||
private addHolesTo(receiver: ts.Expression): boolean {
|
||||
const owner = this.host.ownerOf(receiver);
|
||||
if (owner === null) return this.addHoles(this.host.arrayKeys(receiver));
|
||||
if (owner === null) {
|
||||
const keys = this.host.arrayKeys(receiver);
|
||||
if (keys !== null && keys.length === 0) return this.addViewHoles(receiver);
|
||||
return this.addHoles(keys);
|
||||
}
|
||||
if (this.holeyOwners.has(owner)) return false;
|
||||
this.holeyOwners.add(owner);
|
||||
this.host.changed?.(owner);
|
||||
@@ -490,7 +525,8 @@ export class ArrayElementStates {
|
||||
const undefinedValue = storesValue && this.host.mayBeUndefined(node.right);
|
||||
if (!holes && !undefinedValue) return false;
|
||||
const keys = this.host.arrayKeys(target.expression);
|
||||
if (keys !== null && keys.length === 0) return false;
|
||||
if (keys !== null && keys.length === 0)
|
||||
return holes && !namesProperty(target) && this.addViewHoles(target.expression);
|
||||
let changed = false;
|
||||
if (holes && this.addHolesTo(target.expression)) changed = true;
|
||||
if (undefinedValue && this.addUndefinedTo(target.expression)) changed = true;
|
||||
@@ -501,8 +537,6 @@ export class ArrayElementStates {
|
||||
target.name.text === "length" &&
|
||||
!isShrinkingLength(node, target)
|
||||
) {
|
||||
const keys = this.host.arrayKeys(target.expression);
|
||||
if (keys !== null && keys.length === 0) return false;
|
||||
return this.addHolesTo(target.expression);
|
||||
}
|
||||
return false;
|
||||
@@ -574,10 +608,16 @@ export class ArrayElementStates {
|
||||
));
|
||||
if (!reflective) return false;
|
||||
let changed = false;
|
||||
for (const arg of args) {
|
||||
// The written object: `Reflect.apply`'s receiver argument, otherwise
|
||||
// the first argument.
|
||||
const written = this.host.isGlobal(owner, "Reflect") && method === "apply" ? 1 : 0;
|
||||
for (const [index, arg] of args.entries()) {
|
||||
const target = ts.isSpreadElement(arg) ? arg.expression : arg;
|
||||
const keys = this.host.arrayKeys(target);
|
||||
if (keys !== null && keys.length === 0) continue;
|
||||
if (keys !== null && keys.length === 0) {
|
||||
if (index === written && this.addViewHoles(target)) changed = true;
|
||||
continue;
|
||||
}
|
||||
if (this.addUndefinedTo(target)) changed = true;
|
||||
if (this.addHolesTo(target)) changed = true;
|
||||
}
|
||||
@@ -761,6 +801,14 @@ function isAppendIndex(target: ts.ElementAccessExpression): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** `o["name"]`: a string literal key that is not an array index writes a
|
||||
* named property, never an element. */
|
||||
function namesProperty(target: ts.ElementAccessExpression): boolean {
|
||||
const key = peel(target.argumentExpression);
|
||||
if (!ts.isStringLiteral(key) && !ts.isNoSubstitutionTemplateLiteral(key)) return false;
|
||||
return !/^(0|[1-9][0-9]*)$/.test(key.text) || Number(key.text) >= 2 ** 32 - 1;
|
||||
}
|
||||
|
||||
/** `xs.length = 0`, `xs.length = xs.length - k` and `xs.length -= k`
|
||||
* truncate; truncation never leaves holes. */
|
||||
function isShrinkingLength(
|
||||
|
||||
@@ -102,6 +102,31 @@ console.log(copy.map((item) => weigh(item)).join(","));
|
||||
expect(signature(spread, "weigh")).toBe("weigh(Item|undefined): f64");
|
||||
});
|
||||
|
||||
test("holes stored through a dynamic view widen callbacks over the arrays it views", async () => {
|
||||
const viewed = await lower(`${ITEM}
|
||||
const items: Item[] = [new Item(1)];
|
||||
const view: unknown[] = items;
|
||||
view[3] = new Item(2);
|
||||
console.log(items.findIndex((item) => weigh(item) > 1));
|
||||
`);
|
||||
expect(signature(viewed, "weigh")).toBe("weigh(Item|undefined): f64");
|
||||
// A dynamic store cannot reach an element type no dynamic value views.
|
||||
const other = await lower(`${ITEM}
|
||||
const items: Item[] = [new Item(1)];
|
||||
const values: unknown[] = [1];
|
||||
values[3] = 2;
|
||||
console.log(items.findIndex((item) => weigh(item) > 1));
|
||||
`);
|
||||
expect(signature(other, "weigh")).toBe("weigh(Item): f64");
|
||||
// A view without stores that leave holes keeps the parameters.
|
||||
const read = await lower(`${ITEM}
|
||||
const items: Item[] = [new Item(1)];
|
||||
const view: unknown[] = items;
|
||||
console.log(view.length, items.findIndex((item) => weigh(item) > 1));
|
||||
`);
|
||||
expect(signature(read, "weigh")).toBe("weigh(Item): f64");
|
||||
});
|
||||
|
||||
test("holes that only their owning slot can observe stay out of callbacks", async () => {
|
||||
const module = await lower(`${ITEM}
|
||||
class Cache {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6067-view-holes-in-typed-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Holes written through an `unknown[]` or `any` view of a typed array are
|
||||
// holes of the typed array: for...of visits them as undefined and the find
|
||||
// family passes them to its callback, as in Node. The views reach the array
|
||||
// through a binding, an `any` parameter, a generic instantiated with
|
||||
// `unknown`, a record field, a class field and a nested array.
|
||||
|
||||
const numbers: number[] = [1, 2];
|
||||
const numbersView: unknown[] = numbers;
|
||||
numbersView[3] = 5;
|
||||
for (const value of numbers) console.log("numbers", value, typeof value);
|
||||
console.log(numbers.findIndex((value) => value === undefined));
|
||||
console.log(numbers.findLastIndex((value) => value === undefined));
|
||||
console.log(numbers.find((value) => value !== undefined && value > 1));
|
||||
|
||||
const grown: string[] = ["a"];
|
||||
const grownView: unknown[] = grown;
|
||||
grownView.length = 3;
|
||||
for (const value of grown) console.log("grown", value);
|
||||
|
||||
function growAny(values: any): void {
|
||||
values[2] = 9;
|
||||
}
|
||||
const passed: number[] = [7];
|
||||
growAny(passed);
|
||||
for (const value of passed) console.log("passed", value);
|
||||
|
||||
function keep<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
const kept: number[] = [3];
|
||||
(keep<unknown>(kept) as unknown[])[2] = 4;
|
||||
for (const value of kept) console.log("kept", value);
|
||||
|
||||
const boxed: number[] = [1];
|
||||
const box: unknown = { values: boxed };
|
||||
(box as { values: unknown[] }).values[2] = 3;
|
||||
for (const value of boxed) console.log("boxed", value);
|
||||
|
||||
class Holder {
|
||||
values: number[] = [1, 2];
|
||||
}
|
||||
const holder = new Holder();
|
||||
const holderView: unknown = holder;
|
||||
(holderView as { values: unknown[] }).values[3] = 4;
|
||||
for (const value of holder.values) console.log("holder", value);
|
||||
|
||||
const nested: number[][] = [[1]];
|
||||
const nestedView: unknown[][] = nested;
|
||||
nestedView[0]![2] = 3;
|
||||
for (const value of nested[0]!) console.log("nested", value);
|
||||
|
||||
// A named property written through a view is not an element.
|
||||
const named: number[] = [1, 2];
|
||||
const bag = named as unknown as Record<string, unknown>;
|
||||
bag["label"] = "x";
|
||||
for (const value of named) console.log("named", value);
|
||||
console.log(named.length, bag["label"]);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3967, "san": 4623 }
|
||||
Reference in New Issue
Block a user