fix(javascript): retain stored receiver capture authority

This commit is contained in:
morluto committed 2026-10-24 08:22:14 +08:00
1 parent d3a33fcfca
commit eb721ebbf1
3 files changed
+284 -23

No files matched your search

@@ -33,6 +33,13 @@ import {
evaluateSemanticExpression,
} from "./javascriptSemanticValues.js";
import { semanticContainer } from "./javascriptSemanticSlots.js";
import {
semanticProjectedSourceFactsAtRead,
semanticShiftCapturedSelections,
semanticNormalizeCapturedSelections,
type SemanticCapturedSelection,
} from "./javascriptSemanticProjectedSources.js";
import {
semanticArrayPropertyReferences,
semanticClassCandidates,
@@ -84,6 +91,13 @@ import {
} from "./javascriptAstValues.js";
type PropertyPath = JavaScriptSemanticPropertyPath;
interface MutationSelectionContext {
readonly memberReadAt?: t.Node | undefined;
readonly capturedSelections?:
| readonly SemanticCapturedSelection[]
| undefined;
}
type ValueEffect = "write" | "escape";
/** Only replacing or possibly selecting an iterator-bearing prototype slot changes iteration. */
@@ -570,7 +584,7 @@ export function* collectSemanticMemberMutationsSteps(
// A capture key already ends the array; a path stays one nested element.
const lifetimeKey = (captureKey: string, path: PropertyPath): string =>
`${captureKey.slice(0, -1)},${JSON.stringify(path)}]`;
const pendingReferences: {
const pendingReferences: (MutationSelectionContext & {
readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number];
readonly path: PropertyPath;
readonly bindings: ReadonlySet<string>;
@@ -580,7 +594,7 @@ export function* collectSemanticMemberMutationsSteps(
readonly widenedGetterResult?: true;
readonly definiteReceiver: boolean;
readonly assignedSources?: readonly SemanticIterationSource[];
}[] = [];
})[] = [];
const iterableReferences: IterableReference[] = [];
const iterableReferenceKeys = new WeakMap<t.Node, Set<string>>();
const deferIterable = (reference: IterableReference): void => {
@@ -594,6 +608,14 @@ export function* collectSemanticMemberMutationsSteps(
reference.mutation?.end,
reference.originAt?.start,
reference.originAt?.end,
reference.memberReadAt?.start,
reference.memberReadAt?.end,
reference.capturedSelections?.map(({ at, depth, ownEnumerable }) => [
at.start,
at.end,
depth,
ownEnumerable,
]),
reference.iterationOnly,
reference.widenedGetterResult === true,
reference.definiteReceiver === true,
@@ -637,26 +659,41 @@ export function* collectSemanticMemberMutationsSteps(
widenedGetterResult = false,
assignedSources?: readonly SemanticIterationSource[],
definiteReceiver = true,
selection?: MutationSelectionContext,
): void => {
const pending: (ReferencedValue & {
readonly bindings: ReadonlySet<string>;
readonly originAt: t.Node | undefined;
readonly definiteReceiver: boolean;
})[] = [
const pending: (ReferencedValue &
MutationSelectionContext & {
readonly bindings: ReadonlySet<string>;
readonly originAt: t.Node | undefined;
readonly definiteReceiver: boolean;
})[] = [
{
node,
path,
bindings,
originAt,
definiteReceiver,
memberReadAt: selection?.memberReadAt ?? mutation ?? originAt,
capturedSelections: selection?.capturedSelections,
...(ownerInitializer === undefined ? {} : { ownerInitializer }),
},
];
// Visit shallower alias traversals first so their exact prefixes suppress
// longer loop paths before those paths enumerate every assignment order.
for (let cursor = 0; cursor < pending.length; cursor++) {
const current = pending[cursor];
if (current === undefined) break;
const candidate = pending[cursor];
if (candidate === undefined) break;
// A capture deeper than the selected path is no longer applicable. Keeping
// it would manufacture new contexts on a repeated source alias cycle.
const current = candidate.capturedSelections?.length
? {
...candidate,
capturedSelections: semanticNormalizeCapturedSelections(
candidate.capturedSelections,
candidate.path.length,
),
}
: candidate;
const expression = unwrapJavaScriptExpression(current.node).node;
const required = semanticRequireOrigin(expression, state);
if (required !== undefined) {
@@ -719,11 +756,23 @@ export function* collectSemanticMemberMutationsSteps(
// An alias restored from a saved reference can revisit the same binding
// at an earlier capture or a shorter getter-receiver path. Only a
// repeated capture and path closes a cycle.
const captureKey = captureKeyOf(
binding,
current.originAt,
current.ownerInitializer,
);
const captureSelectionIdentity = JSON.stringify([
current.memberReadAt?.start,
current.memberReadAt?.end,
current.capturedSelections?.map(({ at, depth, ownEnumerable }) => [
at.start,
at.end,
depth,
ownEnumerable,
]),
]);
// Ancestry closes repeated source identities before recursively selected
// copy vectors grow; independent contexts remain distinct in coverage.
const captureKey = JSON.stringify([
captureKeyOf(binding, current.originAt, current.ownerInitializer),
current.memberReadAt?.start,
current.memberReadAt?.end,
]);
const lifetime = lifetimeKey(captureKey, current.path);
if (current.bindings.has(lifetime)) continue;
// A self alias such as `node = node[key]` revisits its capture with the
@@ -756,6 +805,9 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt,
current.ownerInitializer,
widenedGetterResult,
undefined,
undefined,
current,
);
continue;
}
@@ -788,6 +840,7 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt?.end,
current.ownerInitializer?.start,
current.ownerInitializer?.end,
captureSelectionIdentity,
]);
const previous = selections.get(selectionIdentity);
const excludedKeys =
@@ -844,6 +897,10 @@ export function* collectSemanticMemberMutationsSteps(
mutation,
current.originAt,
current.ownerInitializer,
false,
undefined,
undefined,
current,
);
}
// Keep an antichain of exact paths. Widening distinct member keys would
@@ -860,7 +917,7 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt?.start ?? -1
}\u0001${current.originAt?.end ?? -1}\u0001${
current.ownerInitializer?.start ?? -1
}\u0001${current.ownerInitializer?.end ?? -1}\u0001${widenedGetterResult}\u0001${current.definiteReceiver}\u0001${assignedSources?.[0]?.target?.start ?? -1}`;
}\u0001${current.ownerInitializer?.end ?? -1}\u0001${widenedGetterResult}\u0001${current.definiteReceiver}\u0001${assignedSources?.[0]?.target?.start ?? -1}\u0001${captureSelectionIdentity}`;
let covering = coveringPaths.get(pathState);
if (covering === undefined) {
covering = new SemanticPropertyPathCoverage();
@@ -883,6 +940,7 @@ export function* collectSemanticMemberMutationsSteps(
nested.add(captureKey);
// Returning a literal's receiver reaches that allocation, not the
// binding's older values read while evaluating its initializer.
let retainsInitializers = true;
if (!primitiveWrite) {
for (const origin of origins.directOrigins) {
const shared = semanticMutableBuiltinOrigin(origin, path);
@@ -898,6 +956,27 @@ export function* collectSemanticMemberMutationsSteps(
...origins.initializers,
...origins.referenceInitializers,
];
const receiver =
effect === "write" &&
current.path.length > 1 &&
binding.memberWrites.some(
(write) =>
write.assignedSources !== undefined &&
write.path.length < current.path.length,
)
? semanticProjectedSourceFactsAtRead(
binding,
current.node,
current.path.slice(0, -1),
state,
false,
current.memberReadAt ?? current.originAt ?? current.node,
current.capturedSelections,
)
: undefined;
// A defined owner result already includes every eligible initializer;
// replaying them separately duplicates selection/capture histories.
retainsInitializers = receiver === undefined;
if (effect === "escape")
binding.escapedPaths.push({
path,
@@ -905,7 +984,7 @@ export function* collectSemanticMemberMutationsSteps(
receiverInitializers,
...(current.originAt === undefined
? {}
: { receiverAt: current.originAt }),
: { receiverAt: current.memberReadAt ?? current.originAt }),
});
else
binding.memberWrites.push({
@@ -917,11 +996,34 @@ export function* collectSemanticMemberMutationsSteps(
receiverInitializers,
...(current.originAt === undefined
? {}
: { receiverAt: current.originAt }),
: { receiverAt: current.memberReadAt ?? current.originAt }),
...(assignedSources === undefined ? {} : { assignedSources }),
});
clearSemanticValueCaches(state);
for (const initializer of origins.initializers)
// Resolve a stored child at the receiver capture, before selecting
// the final written slot. The source owner preserves replacements
// and captures; a local container does not isolate its children.
if (receiver !== undefined) {
for (const source of receiver.sources)
pending.push({
node: source.node,
path: [...source.path, current.path.at(-1)!],
bindings: nested,
definiteReceiver: false,
originAt: source.node,
memberReadAt: current.memberReadAt,
capturedSelections: semanticNormalizeCapturedSelections([
...(semanticShiftCapturedSelections(
current.capturedSelections,
source.path.length - (current.path.length - 1),
) ?? []),
...(source.capturedSelections ?? []),
]),
});
}
for (const initializer of retainsInitializers
? origins.initializers
: [])
pending.push({
node: initializer.node,
path: [...initializer.projection, ...path],
@@ -932,9 +1034,16 @@ export function* collectSemanticMemberMutationsSteps(
origins.referenceInitializers.length ===
1,
originAt: initializer.node,
memberReadAt: current.memberReadAt,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
initializer.projection.length,
),
});
}
for (const initializer of origins.referenceInitializers) {
for (const initializer of retainsInitializers
? origins.referenceInitializers
: []) {
const copiedPath = semanticCopiedReferencePath(
initializer,
path,
@@ -957,6 +1066,11 @@ export function* collectSemanticMemberMutationsSteps(
effect,
...(widenedGetterResult ? { widenedGetterResult: true } : {}),
originAt: initializer.node,
memberReadAt: current.memberReadAt,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
copiedPath.length - path.length,
),
...(mutation === undefined ? {} : { mutation }),
});
}
@@ -974,6 +1088,17 @@ export function* collectSemanticMemberMutationsSteps(
bindings: current.bindings,
definiteReceiver: current.definiteReceiver && key !== null,
originAt: current.originAt,
memberReadAt: current.memberReadAt,
capturedSelections: semanticNormalizeCapturedSelections([
...(semanticShiftCapturedSelections(
current.capturedSelections,
1,
) ?? []),
...(current.originAt !== undefined &&
unwrapJavaScriptExpression(current.originAt).node === expression
? [{ at: current.originAt, depth: 1 }]
: []),
]),
});
} else {
const references =
@@ -985,6 +1110,13 @@ export function* collectSemanticMemberMutationsSteps(
)
: expandedValues(expression, current.path, effect);
for (const value of references) {
if (
value.inherited &&
current.capturedSelections?.some(
(capture) => capture.depth === 1 && capture.ownEnumerable,
)
)
continue;
if (
value.escapes &&
(effect !== "escape" ||
@@ -999,6 +1131,9 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt,
value.ownerInitializer,
widenedGetterResult || value.widenedGetterResult === true,
undefined,
undefined,
current,
);
else if (value.iterableFallbackPath !== undefined)
deferIterable({
@@ -1011,6 +1146,11 @@ export function* collectSemanticMemberMutationsSteps(
...(assignedSources === undefined ? {} : { assignedSources }),
effect,
originAt: current.originAt,
memberReadAt: current.memberReadAt,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
value.path.length - current.path.length,
),
...(widenedGetterResult ? { widenedGetterResult: true } : {}),
...(mutation === undefined ? {} : { mutation }),
});
@@ -1034,6 +1174,24 @@ export function* collectSemanticMemberMutationsSteps(
(key) => key !== null && typeof key !== "object",
),
originAt: current.originAt,
memberReadAt: current.memberReadAt,
capturedSelections: semanticNormalizeCapturedSelections([
...(semanticShiftCapturedSelections(
current.capturedSelections,
value.path.length - current.path.length,
) ?? []),
...(value.capturedSelectionAt === undefined
? []
: [
{
at: value.capturedSelectionAt,
depth: value.path.length - current.path.length + 1,
...(value.ownEnumerable
? { ownEnumerable: true as const }
: {}),
},
]),
]),
});
}
}
@@ -1369,6 +1527,8 @@ export function* collectSemanticMemberMutationsSteps(
? { widenedGetterResult: true }
: {}),
originAt: initializer.node,
memberReadAt: reference.memberReadAt,
capturedSelections: reference.capturedSelections,
...(reference.mutation === undefined
? {}
: { mutation: reference.mutation }),
@@ -1394,6 +1554,8 @@ export function* collectSemanticMemberMutationsSteps(
? { widenedGetterResult: true }
: {}),
originAt: reference.originAt,
memberReadAt: reference.memberReadAt,
capturedSelections: reference.capturedSelections,
...(reference.mutation === undefined
? {}
: { mutation: reference.mutation }),
@@ -1410,6 +1572,7 @@ export function* collectSemanticMemberMutationsSteps(
reference.widenedGetterResult === true,
reference.assignedSources,
reference.definiteReceiver,
reference,
);
}
for (const [index, reference] of [...iterableReferences].entries()) {
@@ -1438,6 +1601,7 @@ export function* collectSemanticMemberMutationsSteps(
reference.widenedGetterResult === true,
reference.assignedSources,
reference.definiteReceiver === true,
reference,
);
}
// Native Map lookup methods do not expose their receiver to user code.
@@ -1470,7 +1634,7 @@ interface ReferencedValue extends JavaScriptSemanticObjectReference {
readonly iterableFallbackPath?: PropertyPath;
}
interface IterableReference extends ReferencedValue {
interface IterableReference extends ReferencedValue, MutationSelectionContext {
readonly projection: PropertyPath;
readonly fallbackPath: PropertyPath;
readonly bindings: ReadonlySet<string>;