mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve capture and property-key effect ownership
This commit is contained in:
@@ -468,7 +468,9 @@ or deletion does not invoke the getter. Selected setters invalidate
|
||||
the receiver and its shared children on assignment, including through known
|
||||
array and prototype paths. Supported class descriptors use the same inherited
|
||||
field and own-property shadowing rules as class reference selection. Inert
|
||||
getters and ordinary data slots retain their supported precision.
|
||||
getters and ordinary data slots retain their supported precision. Exposing a
|
||||
constructed receiver also exposes its retained shared field references, using
|
||||
the same reference owner as selected getter effects.
|
||||
|
||||
Object spreads, rest copies and destructuring obtain their property reads from
|
||||
the shared pattern-source owner. Copies read own enumerable properties;
|
||||
@@ -476,11 +478,15 @@ ordinary destructuring can invoke an inherited getter. Operations requiring
|
||||
primitive conversion conservatively expose their conversion operands, including
|
||||
computed keys, arithmetic and untagged template interpolation. `using` and
|
||||
`await using` expose the retained resource at its lexical scope exit; earlier
|
||||
scalar reads retain their capture timing. Repeated loop iterations can expose
|
||||
scalar reads retain their capture timing, including reads before a later scope's
|
||||
disposal. Repeated loop iterations can expose
|
||||
a previous disposal and therefore retain uncertainty. Private getter fields
|
||||
retain their parsed initializer references; `super` reads use the selected
|
||||
receiver conservatively, including superclass reads whose lookup and receiver
|
||||
authority differ. Custom `instanceof` hooks expose their receiver and argument.
|
||||
Known unmodified intrinsic Symbol keys cannot overwrite string properties or
|
||||
invoke primitive conversion; their omitted symbol-slot inventory stays partial.
|
||||
Shadowed or visibly replaced Symbol references retain unknown key effects.
|
||||
These are source-derived effects,
|
||||
not execution results or proofs of accessor purity. Reflectively installed
|
||||
descriptors remain opaque; their setup calls retain ordinary argument/receiver
|
||||
|
||||
@@ -252,3 +252,30 @@ it("keeps same-receiver equality in an inert getter precise", () => {
|
||||
`),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
});
|
||||
|
||||
it("keeps intrinsic symbol keys separate from string overwrites without claiming complete inventory", () => {
|
||||
expect(
|
||||
returned(
|
||||
`const state={value:1,[Symbol.toPrimitive](){return 0;}};return state.value;`,
|
||||
),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`const state={value:1,[Symbol.toPrimitive](){return 0;}};`,
|
||||
);
|
||||
expect(ir.bindings.find(({ name }) => name === "state")?.value).toMatchObject(
|
||||
{ status: "object", unknownProperties: true, omittedProperties: 1 },
|
||||
);
|
||||
expect(
|
||||
returned(
|
||||
`const Symbol={toPrimitive:"value"};const state={value:1,[Symbol.toPrimitive]:2};return state.value;`,
|
||||
)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("exposes shared fields when a constructed receiver is passed to an opaque call", () => {
|
||||
expect(
|
||||
returned(
|
||||
`const shared={value:1};class Box{state=shared;} const box=new Box(); unknown(box);return shared.value;`,
|
||||
)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as t from "@babel/types";
|
||||
import { semanticKnownSymbolKey } from "./javascriptSemanticPropertyKeys.js";
|
||||
import {
|
||||
createAmbientPathResolver,
|
||||
retainSemanticAmbientEffect,
|
||||
@@ -1231,7 +1232,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
const value = evaluateSemanticExpression(operand, state, node);
|
||||
return value.status === "object" || value.status === "array";
|
||||
}))
|
||||
markEscaped(operand, node);
|
||||
if (!semanticKnownSymbolKey(operand, state)) markEscaped(operand, node);
|
||||
if (t.isTemplateLiteral(node) && !t.isTaggedTemplateExpression(parent))
|
||||
for (const expression of node.expressions)
|
||||
markEscaped(expression, node);
|
||||
@@ -1558,10 +1559,13 @@ const constructedReferenceValues = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
widenedGetterResult: boolean,
|
||||
): readonly ReferencedValue[] => {
|
||||
// Only retry results lost a suffix that could reach any constructed field.
|
||||
// Preserve that reachability through aliases and nested allocations, without
|
||||
// broadening ordinary whole-receiver escapes.
|
||||
const [key = widenedGetterResult ? null : undefined, ...remaining] = path;
|
||||
// Exposing a constructed receiver also exposes its retained field references,
|
||||
// just as exposing an object literal exposes its children. Widened results
|
||||
// retain that reachability after a suffix was lost.
|
||||
const [
|
||||
key = effect === "escape" || widenedGetterResult ? null : undefined,
|
||||
...remaining
|
||||
] = path;
|
||||
// Replacing a receiver property does not mutate the object it previously held.
|
||||
if (key === undefined || (effect === "write" && remaining.length === 0))
|
||||
return [];
|
||||
|
||||
@@ -1016,6 +1016,12 @@ export const semanticCapturePosition = (
|
||||
semanticStaticPropertyKey(parent.key, parent.computed) !== null
|
||||
)
|
||||
current = parent;
|
||||
else if (
|
||||
t.isSpreadElement(parent) &&
|
||||
parent.argument === current &&
|
||||
t.isObjectExpression(parents.get(parent))
|
||||
)
|
||||
current = parent;
|
||||
else if (t.isObjectExpression(parent) || t.isArrayExpression(parent))
|
||||
current = parent;
|
||||
else return null;
|
||||
@@ -1069,6 +1075,8 @@ export const semanticEscapeFollowsCapture = (
|
||||
!semanticBindingPrecedesCapture(binding, capture, parents)
|
||||
)
|
||||
return false;
|
||||
if (scopeExitOwners.has(escape))
|
||||
return semanticEffectFollowsReference(capture, escape, parents);
|
||||
// A literal allocates here. Aliases are evaluated at the same capture point,
|
||||
// so an outer/persistent origin still receives all of its own escape effects.
|
||||
const origin = unwrapJavaScriptExpression(initializer.node).node;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as t from "@babel/types";
|
||||
import { semanticKnownSymbolKey } from "./javascriptSemanticPropertyKeys.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
@@ -120,6 +121,12 @@ const instanceFieldReferences = (
|
||||
: current.klass.body.body.slice(0, before);
|
||||
for (const member of [...members].reverse()) {
|
||||
if (!isRuntimeJavaScriptClassProperty(member) || member.static) continue;
|
||||
if (
|
||||
propertyName !== null &&
|
||||
member.computed &&
|
||||
semanticKnownSymbolKey(member.key, state)
|
||||
)
|
||||
continue;
|
||||
const key = semanticStaticPropertyKey(member.key, member.computed);
|
||||
if (key !== null && shadowed.has(key)) continue;
|
||||
if (key !== null) shadowed.add(key);
|
||||
@@ -155,6 +162,7 @@ const classPropertyDescriptors = (
|
||||
klass: t.ClassDeclaration | t.ClassExpression,
|
||||
propertyName: string | null,
|
||||
kind: ClassPropertyLookup["kind"],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
initializedBefore?: t.ClassProperty,
|
||||
accessor: "get" | "set" = "get",
|
||||
) => {
|
||||
@@ -171,6 +179,13 @@ const classPropertyDescriptors = (
|
||||
? -1
|
||||
: klass.body.body.indexOf(initializedBefore);
|
||||
klass.body.body.forEach((member, index) => {
|
||||
if (
|
||||
(t.isClassMethod(member) || t.isClassProperty(member)) &&
|
||||
propertyName !== null &&
|
||||
member.computed &&
|
||||
semanticKnownSymbolKey(member.key, state)
|
||||
)
|
||||
return;
|
||||
if (
|
||||
before >= 0 &&
|
||||
index >= before &&
|
||||
@@ -334,6 +349,7 @@ class ClassReferenceWalk {
|
||||
klass,
|
||||
this.#name,
|
||||
this.lookup.kind,
|
||||
this.state,
|
||||
this.lookup.initializedBefore,
|
||||
this.context.accessor,
|
||||
);
|
||||
@@ -795,6 +811,14 @@ class ObjectReferenceWalk {
|
||||
this.#spread(property);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
property.computed &&
|
||||
this.#key !== undefined &&
|
||||
this.#key !== null &&
|
||||
typeof this.#key !== "object" &&
|
||||
semanticKnownSymbolKey(property.key, this.query.state)
|
||||
)
|
||||
return;
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (this.#prototype(property, name)) return;
|
||||
const kind =
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
||||
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
|
||||
|
||||
// These intrinsic properties hold symbols, so ToPropertyKey cannot produce a
|
||||
// string slot. Visible replacement of Symbol/the selected property voids proof.
|
||||
const wellKnownSymbols = new Set([
|
||||
"asyncIterator",
|
||||
"hasInstance",
|
||||
"isConcatSpreadable",
|
||||
"iterator",
|
||||
"match",
|
||||
"matchAll",
|
||||
"replace",
|
||||
"search",
|
||||
"species",
|
||||
"split",
|
||||
"toPrimitive",
|
||||
"toStringTag",
|
||||
"unscopables",
|
||||
"dispose",
|
||||
"asyncDispose",
|
||||
]);
|
||||
|
||||
/** A proved symbol key cannot shadow a selected string property. */
|
||||
export const semanticKnownSymbolKey = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const key = unwrapJavaScriptExpression(node).node;
|
||||
if (!t.isMemberExpression(key)) return false;
|
||||
const name = semanticStaticPropertyKey(key.property, key.computed);
|
||||
return (
|
||||
name !== null &&
|
||||
wellKnownSymbols.has(name) &&
|
||||
isUnmodifiedGlobalReference(key, state, ["Symbol", name])
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
semanticOpaqueBindingReadMayHaveChanged,
|
||||
} from "./javascriptSemanticOpaqueEffects.js";
|
||||
import * as t from "@babel/types";
|
||||
import { semanticKnownSymbolKey } from "./javascriptSemanticPropertyKeys.js";
|
||||
import { logicalRightOperand } from "./javascriptSymbolicControlFlow.js";
|
||||
|
||||
import {
|
||||
@@ -864,6 +865,16 @@ const evaluateObject = (
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
property.computed &&
|
||||
semanticKnownSymbolKey(property.key, context.state)
|
||||
) {
|
||||
// The string-slot lattice omits symbol names, but they cannot overwrite
|
||||
// established string fields. Keep the omitted inventory explicit.
|
||||
unknownProperties = true;
|
||||
if (omittedProperties !== null) omittedProperties += 1;
|
||||
continue;
|
||||
}
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (name === null) {
|
||||
unknownProperties = true;
|
||||
|
||||
Reference in New Issue
Block a user