mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(javascript): retain private receiver and iterative disposal effects
This commit is contained in:
@@ -147,3 +147,87 @@ it("retains shared child effects from selected setters", () => {
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("exposes custom instanceof receivers but keeps object equality noncoercing", () => {
|
||||
expect(
|
||||
returned(`
|
||||
const state = { value: 1, [Symbol.hasInstance]() { this.value = 2; return false; } };
|
||||
const result = {} instanceof state;
|
||||
return state.value;
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
expect(
|
||||
returned(`
|
||||
const state = { value: 1 };
|
||||
const target = { [Symbol.hasInstance](value) { value.value = 2; return false; } };
|
||||
const result = state instanceof target;
|
||||
return state.value;
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
expect(
|
||||
returned(`
|
||||
const state = { value: 1, [Symbol.toPrimitive]() { this.value = 2; return 0; } };
|
||||
const equal = state == state; const unequal = state != {};
|
||||
return state.value;
|
||||
`),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
});
|
||||
|
||||
it("exposes class private storage reached by an invoked getter", () => {
|
||||
expect(
|
||||
returned(`
|
||||
const shared = { value: 1 };
|
||||
class Box { #state = shared; get trigger() { this.#state.value = 2; return 0; } }
|
||||
new Box().trigger;
|
||||
return shared.value;
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("retains disposal of per-iteration resources through existing loop origins", () => {
|
||||
expect(
|
||||
returned(`
|
||||
const state = { value: 1, [Symbol.dispose]() { this.value = 2; } };
|
||||
for (using owned of [state]) {}
|
||||
return state.value;
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("does not expose a returned resource as an unchanged object snapshot", () => {
|
||||
const value = returned(`
|
||||
const state = { value: 1, [Symbol.dispose]() { this.value = 2; } };
|
||||
using owned = state;
|
||||
return state;
|
||||
`);
|
||||
expect(
|
||||
value?.status === "object"
|
||||
? value.properties.find(({ name }) => name === "value")?.value.status
|
||||
: value?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("retains earlier iteration disposal uncertainty for reused outer resources", () => {
|
||||
expect(
|
||||
returned(`
|
||||
const state = { value: 1, [Symbol.dispose]() { this.value = 2; } };
|
||||
for (const index of [0, 1]) {
|
||||
using owned = state;
|
||||
const snapshot = state.value;
|
||||
if (index === 1) return snapshot;
|
||||
}
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("follows superclass getter effects through the selected receiver", () => {
|
||||
expect(
|
||||
returned(`
|
||||
const shared = { value: 1 };
|
||||
class Base { get trigger2() { this.state.value = 2; return 0; } }
|
||||
class Box extends Base { state = shared; get trigger() { return super.trigger2; } }
|
||||
new Box().trigger;
|
||||
return shared.value;
|
||||
`)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ class GetterResultWalker {
|
||||
}));
|
||||
}
|
||||
|
||||
#ownsThis(expression: t.ThisExpression): boolean {
|
||||
#ownsThis(expression: t.ThisExpression | t.Super): boolean {
|
||||
let owner = this.query.state.parentsByNode.get(expression);
|
||||
while (
|
||||
owner !== undefined &&
|
||||
@@ -269,7 +269,10 @@ class GetterResultWalker {
|
||||
});
|
||||
}
|
||||
|
||||
#thisValue(current: GetterWork, expression: t.ThisExpression): void {
|
||||
#thisValue(
|
||||
current: GetterWork,
|
||||
expression: t.ThisExpression | t.Super,
|
||||
): void {
|
||||
if (!this.#ownsThis(expression)) return;
|
||||
const { getter, owner, receiver = owner } = this.job.invocation;
|
||||
if (current.privateRead !== undefined && t.isClass(owner)) {
|
||||
@@ -362,7 +365,8 @@ class GetterResultWalker {
|
||||
#visit(current: GetterWork): void {
|
||||
const expression = unwrapJavaScriptExpression(current.node).node;
|
||||
if (t.isIdentifier(expression) && this.#alias(current, expression)) return;
|
||||
if (t.isThisExpression(expression)) this.#thisValue(current, expression);
|
||||
if (t.isThisExpression(expression) || t.isSuper(expression))
|
||||
this.#thisValue(current, expression);
|
||||
else if (
|
||||
t.isMemberExpression(expression) ||
|
||||
t.isOptionalMemberExpression(expression)
|
||||
@@ -400,17 +404,18 @@ export const expandSemanticGetterResult = (
|
||||
): void => new GetterResultWalker(job, query, resolvers).run();
|
||||
|
||||
/** Project an effect operand through the same local aliases and `this` owner as getter results. */
|
||||
export const semanticGetterReceiverPaths = (
|
||||
export const semanticGetterReceiverReferences = (
|
||||
getter: t.ObjectMethod | t.ClassMethod,
|
||||
operand: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): readonly PropertyPath[] => {
|
||||
): readonly Reference[] => {
|
||||
const parent = state.parentsByNode.get(getter);
|
||||
const owner = t.isClassBody(parent)
|
||||
? state.parentsByNode.get(parent)
|
||||
: parent;
|
||||
if (!t.isObjectExpression(owner) && !t.isClass(owner)) return [];
|
||||
const query = new SemanticGetterQuery(state);
|
||||
let privateReceiver = false;
|
||||
const references = query.run(
|
||||
() => {
|
||||
query.schedule({ getter, owner }, []);
|
||||
@@ -423,7 +428,12 @@ export const semanticGetterReceiverPaths = (
|
||||
{
|
||||
objectReferences: (_receiver, path) => [{ node: getter, path }],
|
||||
classReferences: (_lookup, path) => [{ node: getter, path }],
|
||||
privateReceiver: () => false,
|
||||
privateReceiver: () => {
|
||||
// Keep private-field initializer identity from the existing
|
||||
// getter owner; private slots are not public property paths.
|
||||
privateReceiver = true;
|
||||
return true;
|
||||
},
|
||||
bases: () => [],
|
||||
containerReferences: (node, path) =>
|
||||
t.isObjectExpression(node)
|
||||
@@ -433,7 +443,5 @@ export const semanticGetterReceiverPaths = (
|
||||
[{ node: operand }],
|
||||
).run(),
|
||||
);
|
||||
return references
|
||||
.filter(({ node }) => node === getter)
|
||||
.map(({ path }) => path);
|
||||
return references.filter(({ node }) => node === getter || privateReceiver);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
/** Operands exposed to user-defined primitive conversion by language operations. */
|
||||
export const semanticCoercionOperands = (node: t.Node): readonly t.Node[] => {
|
||||
export const semanticCoercionOperands = (
|
||||
node: t.Node,
|
||||
isObject: (operand: t.Node) => boolean = () => false,
|
||||
): readonly t.Node[] => {
|
||||
if (t.isBinaryExpression(node)) {
|
||||
if (["===", "!==", "instanceof"].includes(node.operator)) return [];
|
||||
if (["===", "!=="].includes(node.operator)) return [];
|
||||
if (node.operator === "instanceof") return [node.left, node.right];
|
||||
if (
|
||||
["==", "!="].includes(node.operator) &&
|
||||
isObject(node.left) &&
|
||||
isObject(node.right)
|
||||
)
|
||||
return [];
|
||||
return node.operator === "in" ? [node.left] : [node.left, node.right];
|
||||
}
|
||||
if (t.isUnaryExpression(node) && ["+", "-", "~"].includes(node.operator))
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
type SemanticIterationSource,
|
||||
type SemanticPropertyReadSource,
|
||||
} from "./javascriptSemanticIterationSources.js";
|
||||
import { semanticGetterReceiverPaths } from "./javascriptSemanticGetterResults.js";
|
||||
import { semanticGetterReceiverReferences } from "./javascriptSemanticGetterResults.js";
|
||||
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
||||
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
|
||||
import {
|
||||
@@ -370,26 +370,35 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
);
|
||||
};
|
||||
const returnedReferences = createSemanticReturnedReferences(state);
|
||||
const getterEffectPaths = (
|
||||
const getterEffects = (
|
||||
root: t.Node,
|
||||
path: PropertyPath,
|
||||
lookup: "prototype" | "own" = "prototype",
|
||||
): readonly PropertyPath[] => {
|
||||
): readonly JavaScriptSemanticObjectReference[] => {
|
||||
if (!accessorKeyMayExist(path.at(-1), "get")) return [];
|
||||
const pending = [{ path, lookup }];
|
||||
const pending = [{ node: root, path, lookup }];
|
||||
const examined = new Set<string>();
|
||||
const effects: PropertyPath[] = [];
|
||||
const effects: JavaScriptSemanticObjectReference[] = [];
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const { path: selected, lookup: selectedLookup } = pending[cursor]!;
|
||||
const identity = JSON.stringify([selected, selectedLookup]);
|
||||
const {
|
||||
node: selectedRoot,
|
||||
path: selected,
|
||||
lookup: selectedLookup,
|
||||
} = pending[cursor]!;
|
||||
const identity = JSON.stringify([
|
||||
selectedRoot.start,
|
||||
selectedRoot.end,
|
||||
selected,
|
||||
selectedLookup,
|
||||
]);
|
||||
if (examined.has(identity)) continue;
|
||||
examined.add(identity);
|
||||
if (examined.size > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return [[]];
|
||||
return [{ node: root, path: [] }];
|
||||
}
|
||||
const methods = localAccessorMethods(
|
||||
root,
|
||||
selectedRoot,
|
||||
selected,
|
||||
"get",
|
||||
state,
|
||||
@@ -398,7 +407,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
);
|
||||
const receiver = selected.slice(0, -1);
|
||||
if (methods === undefined) {
|
||||
effects.push(receiver);
|
||||
effects.push({ node: selectedRoot, path: receiver });
|
||||
continue;
|
||||
}
|
||||
for (const method of methods) {
|
||||
@@ -424,15 +433,23 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
semanticReferenceSourcesInactive(operand.guards, state)))
|
||||
)
|
||||
continue;
|
||||
for (const suffix of semanticGetterReceiverPaths(
|
||||
for (const reference of semanticGetterReceiverReferences(
|
||||
method,
|
||||
operand.node,
|
||||
state,
|
||||
)) {
|
||||
const target = [...receiver, ...suffix];
|
||||
if (operand.kind === "escape") effects.push(target);
|
||||
const receiverReference = reference.node === method;
|
||||
const targetRoot = receiverReference
|
||||
? selectedRoot
|
||||
: reference.node;
|
||||
const target = receiverReference
|
||||
? [...receiver, ...reference.path]
|
||||
: reference.path;
|
||||
if (operand.kind === "escape")
|
||||
effects.push({ node: targetRoot, path: target });
|
||||
else
|
||||
pending.push({
|
||||
node: targetRoot,
|
||||
path: [...target, ...operand.path],
|
||||
lookup: operand.lookup,
|
||||
});
|
||||
@@ -1211,14 +1228,16 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
originAt: source.mutation,
|
||||
iterationOnly: source.kind === "iteration",
|
||||
});
|
||||
for (const operand of semanticCoercionOperands(node))
|
||||
for (const operand of semanticCoercionOperands(node, (operand) => {
|
||||
const value = evaluateSemanticExpression(operand, state, node);
|
||||
return value.status === "object" || value.status === "array";
|
||||
}))
|
||||
markEscaped(operand, node);
|
||||
if (t.isTemplateLiteral(node) && !t.isTaggedTemplateExpression(parent))
|
||||
for (const expression of node.expressions)
|
||||
markEscaped(expression, node);
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
node.init != null &&
|
||||
t.isVariableDeclaration(parent) &&
|
||||
(parent.kind === "using" || parent.kind === "await using")
|
||||
) {
|
||||
@@ -1240,27 +1259,27 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
"escape",
|
||||
exit,
|
||||
node.id,
|
||||
node.init,
|
||||
node.init ?? undefined,
|
||||
);
|
||||
else markEscaped(node.init, exit);
|
||||
else if (node.init != null) markEscaped(node.init, exit);
|
||||
}
|
||||
}
|
||||
for (const source of semanticPropertyReadSources(node, parent)) {
|
||||
if (semanticReferenceSourcesInactive(source, state)) continue;
|
||||
for (const path of getterEffectPaths(
|
||||
for (const effect of getterEffects(
|
||||
source.node,
|
||||
source.projection,
|
||||
source.lookup,
|
||||
))
|
||||
markEscaped(source.node, source.mutation, path);
|
||||
markEscaped(effect.node, source.mutation, effect.path);
|
||||
}
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
// The write owner classifies direct and destructuring stores once.
|
||||
if (!writeOnlyMembers.has(node))
|
||||
for (const path of getterEffectPaths(node.object, [
|
||||
for (const effect of getterEffects(node.object, [
|
||||
semanticStaticPropertyKey(node.property, node.computed),
|
||||
]))
|
||||
markEscaped(node.object, node, path);
|
||||
markEscaped(effect.node, node, effect.path);
|
||||
}
|
||||
if (writeTarget !== null) markTarget(writeTarget, node);
|
||||
else if (
|
||||
|
||||
@@ -164,8 +164,10 @@ export const semanticScopeExitEffect = (scope: t.Node): t.Node => {
|
||||
const cached = scopeExitMarkers.get(scope);
|
||||
if (cached !== undefined) return cached;
|
||||
const marker = t.emptyStatement();
|
||||
marker.start = scope.end;
|
||||
marker.end = scope.end;
|
||||
if (scope.end !== undefined) {
|
||||
marker.start = scope.end;
|
||||
marker.end = scope.end;
|
||||
}
|
||||
if (scope.loc != null)
|
||||
marker.loc = { ...scope.loc, start: scope.loc.end, end: scope.loc.end };
|
||||
scopeExitMarkers.set(scope, marker);
|
||||
@@ -183,8 +185,18 @@ const withinScopeExit = (
|
||||
node !== undefined;
|
||||
node = parents.get(node)
|
||||
) {
|
||||
if (node === scope) return true;
|
||||
if (t.isFunction(node)) return false;
|
||||
if (node === scope) {
|
||||
for (
|
||||
let owner = parents.get(scope);
|
||||
owner !== undefined;
|
||||
owner = parents.get(owner)
|
||||
) {
|
||||
if (t.isLoop(owner)) return false;
|
||||
if (t.isFunction(owner)) break;
|
||||
}
|
||||
return !t.isLoop(scope);
|
||||
}
|
||||
if (t.isFunction(node) || t.isLoop(node)) return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user