fix(javascript): preserve effects through yielded array references (#1437)

This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-10 13:00:11 +08:00
1 parent dc5af6d68f
commit 0a14ca41b4
4 files changed
+188 -4

No files matched your search

@@ -565,9 +565,21 @@ const bindAssignment = (
else if (t.isForOfStatement(node) || t.isForInStatement(node))
for (const pattern of t.isVariableDeclaration(node.left)
? node.left.declarations.map(({ id }) => id)
: [node.left])
: [node.left]) {
for (const identifier of assignedPatternIdentifiers(pattern))
addAssignment(identifier, node, scope, state);
if (t.isForOfStatement(node))
// Keep the loop value unknown, but preserve references yielded into
// each bound slot so writes/escapes can reach their iterable origins.
bindPattern({
pattern,
initializer: node,
scope,
state,
mutable: true,
referenceOnly: true,
});
}
};
const assignedPatternIdentifiers = (
@@ -780,7 +792,11 @@ const bindPattern = (input: BindPatternInput): void => {
}
if (t.isIdentifier(pattern)) {
if (referenceOnly) {
const binding = scope.bindings.get(pattern.name);
const binding = resolveSemanticBindingFromScope(
scope,
pattern.name,
state,
);
if (binding !== undefined && initializer !== null)
binding.referenceInitializers.push({
node: initializer,
@@ -8,8 +8,9 @@ interface IterationSourceValue {
readonly projection: readonly (string | number | null)[];
}
/** An implicit iterator receiver, with guards for destructuring defaults. */
/** An iterator receiver or yielded reference store, with default guards. */
export interface SemanticIterationSource extends IterationSourceValue {
readonly kind: "iteration" | "reference-store";
readonly mutation: t.Node;
readonly fallbackSources?: readonly IterationSourceValue[];
readonly requiredSources?: readonly IterationSourceValue[];
@@ -30,8 +31,10 @@ const add = (
source: IterationSourceValue,
mutation: t.Node,
guards: SourceGuards,
kind: SemanticIterationSource["kind"] = "iteration",
): void => {
sources.push({
kind,
node: source.node,
projection: source.projection,
mutation,
@@ -66,6 +69,13 @@ const visit = (
guards: SourceGuards = {},
): void => {
const { sources, mutation } = context;
if (t.isForOfStatement(mutation) && t.isMemberExpression(pattern)) {
// Storing a yielded reference in a property has no lexical loop binding.
// Preserve its escape rather than assuming later writes cannot reach it.
if (source !== undefined)
add(sources, source, mutation, guards, "reference-store");
return;
}
if (t.isTSParameterProperty(pattern)) {
visit(context, pattern.parameter, source, guards);
return;
@@ -324,7 +324,7 @@ export const collectSemanticMemberMutations = (
bindings: new Set(),
effect: "escape",
originAt: source.mutation,
iterationOnly: true,
iterationOnly: source.kind === "iteration",
});
if (t.isAssignmentExpression(node)) markTarget(node.left, node);
else if (t.isUpdateExpression(node)) markTarget(node.argument, node);
@@ -369,6 +369,19 @@ export const collectSemanticMemberMutations = (
const { initializer } = reference;
if (referenceSourcesInactive(initializer, state))
pendingReferences.push(reference);
else if (t.isForOfStatement(initializer.node))
deferIterable({
node: initializer.node.right,
projection: [],
path: [null, ...reference.path],
fallbackPath: [],
bindings: reference.bindings,
effect: reference.effect,
originAt: initializer.node,
...(reference.mutation === undefined
? {}
: { mutation: reference.mutation }),
});
else if (
initializer.copyKind === "array-rest" &&
initializer.copyProjectionOffset !== undefined