fix(javascript): preserve uncertainty after references escape to calls (#1292)

* fix(javascript): preserve uncertainty after references escape to calls

* fix(javascript): track iterator root escapes and preserve static captures

* fix(javascript): scope comparison unknowns to their evidence

* fix(javascript): track parameter origins at function body entry

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
You Wish
2026-10-09 18:53:42 -04:00
committed by GitHub
parent d0e606930a
commit 355a292198
17 changed files with 2574 additions and 220 deletions
@@ -0,0 +1,163 @@
import * as t from "@babel/types";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
interface IterationSourceValue {
readonly node: t.Node;
readonly projection: readonly (string | number | null)[];
}
/** An implicit iterator receiver, with guards for destructuring defaults. */
export interface SemanticIterationSource extends IterationSourceValue {
readonly mutation: t.Node;
readonly fallbackSources?: readonly IterationSourceValue[];
readonly requiredSources?: readonly IterationSourceValue[];
}
interface PatternSource extends IterationSourceValue {
// A rest binding is a fresh array whose children retain their source indices.
readonly restStartIndex?: number;
}
interface SourceGuards {
readonly fallbackSources?: readonly IterationSourceValue[];
readonly requiredSources?: readonly IterationSourceValue[];
}
const add = (
sources: SemanticIterationSource[],
source: IterationSourceValue,
mutation: t.Node,
guards: SourceGuards,
): void => {
sources.push({
node: source.node,
projection: source.projection,
mutation,
...guards,
});
};
const project = (
source: PatternSource | undefined,
key: string | number | null,
): PatternSource | undefined => {
if (source === undefined) return undefined;
const index = key === null ? null : semanticArrayIndex(key);
return {
node: source.node,
projection: [
...source.projection,
source.restStartIndex !== undefined && index !== null
? source.restStartIndex + index
: key,
],
};
};
interface PatternCollection {
readonly sources: SemanticIterationSource[];
readonly mutation: t.Node;
}
const visit = (
context: PatternCollection,
pattern: t.Node,
source: PatternSource | undefined,
guards: SourceGuards = {},
): void => {
const { sources, mutation } = context;
if (t.isTSParameterProperty(pattern)) {
visit(context, pattern.parameter, source, guards);
return;
}
if (t.isAssignmentPattern(pattern)) {
visit(context, pattern.left, source, {
...guards,
requiredSources: [
...(guards.requiredSources ?? []),
...(source === undefined ? [] : [source]),
],
});
visit(
context,
pattern.left,
{ node: pattern.right, projection: [] },
{
...guards,
fallbackSources: [
...(guards.fallbackSources ?? []),
...(source === undefined ? [] : [source]),
],
},
);
return;
}
if (t.isObjectPattern(pattern)) {
for (const property of pattern.properties) {
// Object rest creates a fresh object and has no iterable binding pattern.
if (t.isRestElement(property)) continue;
visit(
context,
property.value,
project(
source,
semanticStaticPropertyKey(property.key, property.computed),
),
guards,
);
}
return;
}
if (t.isArrayPattern(pattern)) {
if (source !== undefined && source.restStartIndex === undefined)
add(sources, source, mutation, guards);
pattern.elements.forEach((element, index) => {
if (element === null) return;
if (t.isRestElement(element))
visit(
context,
element.argument,
source === undefined
? undefined
: {
node: source.node,
projection: source.projection,
restStartIndex: (source.restStartIndex ?? 0) + index,
},
guards,
);
else visit(context, element, project(source, index), guards);
});
return;
}
if (t.isRestElement(pattern))
visit(context, pattern.argument, undefined, guards);
};
/** Describe implicit iteration sources without deciding their runtime values. */
export const semanticIterationSources = (
node: t.Node,
parent: t.Node | null,
): readonly SemanticIterationSource[] => {
const sources: SemanticIterationSource[] = [];
const context = { sources, mutation: node };
if (t.isVariableDeclarator(node) && node.init != null)
visit(context, node.id, { node: node.init, projection: [] });
else if (t.isAssignmentExpression(node) && node.operator === "=")
visit(context, node.left, { node: node.right, projection: [] });
else if (t.isForOfStatement(node)) {
add(sources, { node: node.right, projection: [] }, node, {});
const source = { node: node.right, projection: [null] };
if (t.isVariableDeclaration(node.left)) {
for (const declaration of node.left.declarations)
visit(context, declaration.id, source);
} else visit(context, node.left, source);
} else if (t.isSpreadElement(node) && t.isArrayExpression(parent))
add(sources, { node: node.argument, projection: [] }, node, {});
else if (t.isYieldExpression(node) && node.delegate && node.argument != null)
add(sources, { node: node.argument, projection: [] }, node, {});
else if (t.isFunction(node))
for (const parameter of node.params) visit(context, parameter, undefined);
return sources;
};