fix(javascript): preserve capture and property-key effect ownership

This commit is contained in:
morluto
2026-10-24 04:14:34 +08:00
parent b3433d0187
commit c6ac60acfe
7 changed files with 129 additions and 7 deletions
+8 -2
View File
@@ -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;