fix(javascript): preserve initializer execution and lexical ownership

This commit is contained in:
morluto committed 2026-10-24 02:29:44 +08:00
1 parent 5cbcd58550
commit fba4faf88a
8 files changed
+488 -41

No files matched your search

@@ -9,6 +9,7 @@ import * as t from "@babel/types";
import {
semanticComputedMethodKey,
semanticBranchMayBeSkipped,
semanticPriorStatementMayExit,
semanticIterationCardinality,
} from "./javascriptSemanticEvaluationOrder.js";
@@ -116,6 +117,7 @@ import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutio
interface BindPatternInput {
readonly iterationSources?: JavaScriptSemanticBindingState["initializers"][number]["iterationSources"];
readonly pattern: t.Node;
readonly entryBody?: t.BlockStatement;
readonly initializer: t.Node | null;
readonly scope: JavaScriptSemanticScopeState;
readonly state: JavaScriptSemanticAnalysisState;
@@ -1067,6 +1069,19 @@ function* collectDefinitionsSteps(
node: t.CallExpression;
scopeId: string;
}[] = [];
const displacedVarInitializers: {
node: t.VariableDeclarator;
scope: JavaScriptSemanticScopeState;
owner: JavaScriptSemanticScopeState;
}[] = [];
const thrownValues: {
node: t.ThrowStatement;
ancestors: readonly t.Node[];
}[] = [];
const legacyBlockFunctions: {
node: t.FunctionDeclaration;
scope: JavaScriptSemanticScopeState;
}[] = [];
let needsPrototypeFacts = false;
const functionBodies: {
readonly body: t.BlockStatement;
@@ -1117,6 +1132,21 @@ function* collectDefinitionsSteps(
parentScope = dynamicScope;
}
bindOuterDeclaration(node, parentScope, state);
if (
t.isFunctionDeclaration(node) &&
node.id != null &&
parentScope.kind === "block"
)
legacyBlockFunctions.push({ node, scope: parentScope });
if (
t.isVariableDeclarator(node) &&
node.init != null &&
t.isIdentifier(node.id)
) {
const owner = semanticVariableScope(parentScope, parent, state);
if (owner !== parentScope)
displacedVarInitializers.push({ node, scope: parentScope, owner });
}
const nested = nestedScope(node, parent, parentScope, state);
if (nested !== undefined) {
stack.push(nested);
@@ -1141,6 +1171,8 @@ function* collectDefinitionsSteps(
state,
});
bindInnerDeclaration(node, parent, scope, state);
if (t.isThrowStatement(node))
thrownValues.push({ node, ancestors: readAncestors() });
// Every supported global eval origin starts with one of these decoded
// identifiers, including computed properties and destructured aliases.
if (t.isIdentifier(node) && globalEvalRoots.has(node.name))
@@ -1180,6 +1212,94 @@ function* collectDefinitionsSteps(
stack.pop();
},
});
// A var declaration is instantiated in its variable environment, while its
// initializer assigns through the lexical environment at the source site.
for (const { node, scope, owner } of displacedVarInitializers) {
if (!t.isIdentifier(node.id) || node.init == null) continue;
const selected = resolveSemanticBindingFromScope(
scope,
node.id.name,
state,
);
const declared = owner.bindings.get(node.id.name);
if (
selected === undefined ||
selected === declared ||
declared === undefined
)
continue;
for (let index = declared.initializers.length - 1; index >= 0; index--)
if (declared.initializers[index]?.node === node.init)
declared.initializers.splice(index, 1);
addAssignment(
node.id,
node.init,
scope,
state,
state.conditionallySkippedExpressions.has(node.init),
node,
);
}
for (const { node, ancestors } of thrownValues) {
let child: t.Node = node;
for (let index = ancestors.length - 1; index >= 0; index--) {
const parent = ancestors[index]!;
if (t.isFunction(parent) || t.isClass(parent)) break;
if (
t.isTryStatement(parent) &&
parent.handler != null &&
child === parent.block
) {
const scope = state.scopeByNode.get(parent.handler);
if (scope !== undefined && parent.handler.param != null)
bindPattern({
pattern: parent.handler.param,
initializer: node.argument,
scope,
state,
mutable: true,
referenceOnly: true,
entryBody: parent.handler.body,
});
break;
}
child = parent;
}
}
for (const { node, scope } of legacyBlockFunctions) {
if (
node.id == null ||
semanticStrictnessAt(state.parentsByNode.get(node) ?? node, state) ===
"strict"
)
continue;
let outer =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
while (outer !== undefined) {
const binding = outer.bindings.get(node.id.name);
if (
binding !== undefined &&
!binding.declarationKinds.has("var") &&
binding.kind !== "parameter"
)
break;
if (
outer.kind === "function" ||
outer.kind === "program" ||
outer.kind === "static-block"
) {
if (binding !== undefined)
addAssignment(node.id, node, outer, state, false, node);
break;
}
outer =
outer.parentScopeId === null
? undefined
: state.scopesById.get(outer.parentScopeId);
}
}
if (needsPrototypeFacts)
yield* indexPrototypeUncertaintySteps(program, state);
// All declarations must exist before resolving writes, including hoisted
@@ -1365,7 +1485,11 @@ const writeMayBeSkipped = (
t.isStaticBlock(parent)
)
return false;
if (semanticBranchMayBeSkipped(parent, child)) return true;
if (
semanticBranchMayBeSkipped(parent, child) ||
semanticPriorStatementMayExit(parent, child)
)
return true;
child = parent;
}
return false;
@@ -1421,7 +1545,7 @@ const bindInnerDeclaration = (
? { declarationKind: parent.kind }
: {}),
});
else if (t.isClassExpression(node) && t.isIdentifier(node.id))
else if (t.isClass(node) && t.isIdentifier(node.id))
addBinding({
state,
scope,
@@ -1588,7 +1712,8 @@ const mappedArgumentsOwner = (
}
if (!t.isIdentifier(value)) return undefined;
if (semanticBindingLookupAt(state, value, "arguments").kind !== "ambient")
const lookup = semanticBindingLookupAt(state, value, "arguments");
if (lookup.kind !== "implicit-arguments" && lookup.kind !== "ambient")
return undefined;
let owner: t.Function | undefined;
for (
@@ -1634,12 +1759,12 @@ const mappedArgumentsOwner = (
return {
scope,
parameters: mapped,
strictness: mappedArgumentsStrictness(owner, state),
strictness: semanticStrictnessAt(owner, state),
};
};
const mappedArgumentsStrictness = (
owner: t.Function,
const semanticStrictnessAt = (
owner: t.Node,
state: JavaScriptSemanticAnalysisState,
): MappedArgumentsOwner["strictness"] => {
for (
@@ -1737,6 +1862,9 @@ const nestedScope = (
location: range(switchOwner ?? node),
bindingsComplete: true,
bindings: new Map(),
...(t.isFunction(node) && !t.isArrowFunctionExpression(node)
? { implicitArguments: true as const }
: {}),
};
state.scopes.push(scope);
state.scopesById.set(scope.scopeId, scope);
@@ -1914,6 +2042,9 @@ const bindPattern = (input: BindPatternInput): void => {
binding.referenceInitializers.push({
node: initializer,
projection,
...(input.entryBody === undefined
? {}
: { entryBody: input.entryBody }),
...(assignmentReference === undefined ? {} : { assignment: true }),
...(copyKind === undefined ? {} : { copyKind }),
...(copyProjectionOffset === undefined
@@ -2136,7 +2267,8 @@ const addAssignment = (
const mutation = writeSite ?? initializer;
if (mutation === null) return;
const lookup = semanticBindingLookupFromScope(scope, identifier.name, state);
if (lookup.kind === "captured") return;
if (lookup.kind === "captured" || lookup.kind === "implicit-arguments")
return;
const binding = "binding" in lookup ? lookup.binding : undefined;
conditionallySkipped ||=
lookup.kind !== "binding" && lookup.kind !== "ambient";