mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 20:06:14 +00:00
fix(javascript): bound self-alias mutation paths that never prune
Detect unprunable repeated self-alias prefixes at the JavaScript mutation owner, record conservative escape effects, and reuse settled repeated-prefix checks. Preserve finite-path precision and deeper-write uncertainty. Refs #1665. Co-authored-by: Martin Klein <36695017+flitzrrr@users.noreply.github.com>
This commit is contained in:
1 parent
817aef4068
commit
e365f3715f
2 files changed
+113
-11
No files matched your search
@@ -46,7 +46,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): Generator<void, void> {
|
||||
const parents = state.parentsByNode;
|
||||
const normalizePath = observableMutationPaths(state);
|
||||
const observablePaths = observableMutationPaths(state);
|
||||
const returnedReferences = createSemanticReturnedReferences(state);
|
||||
const recordedEffects = new Set<string>();
|
||||
const selections = new Map<string, readonly string[]>();
|
||||
@@ -128,18 +128,48 @@ 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 lifetime = JSON.stringify([
|
||||
const capture = [
|
||||
binding.bindingId,
|
||||
current.originAt?.start,
|
||||
current.originAt?.end,
|
||||
current.path,
|
||||
]);
|
||||
];
|
||||
const lifetime = JSON.stringify([...capture, current.path]);
|
||||
if (current.bindings.has(lifetime)) continue;
|
||||
// A self alias such as `node = node[key]` revisits its capture with the
|
||||
// same prefix added on every cycle, so exact paths never repeat. When
|
||||
// that prefix can never be pruned, escaping it once covers every longer
|
||||
// effect, including writes below shared children.
|
||||
let repeatedPrefix: PropertyPath | undefined;
|
||||
for (let start = 1; start <= current.path.length; start++) {
|
||||
const suffix = current.path.slice(start);
|
||||
if (!current.bindings.has(JSON.stringify([...capture, suffix])))
|
||||
continue;
|
||||
repeatedPrefix = current.path.slice(0, start);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
repeatedPrefix !== undefined &&
|
||||
observablePaths.repeatsUnpruned(binding.bindingId, repeatedPrefix)
|
||||
) {
|
||||
markValue(
|
||||
current.node,
|
||||
repeatedPrefix,
|
||||
new Set(),
|
||||
"escape",
|
||||
mutation,
|
||||
current.originAt,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Union compatible copied-slot selections at each binding. Carrying every
|
||||
// combination of exclusions through branch diamonds is exponential.
|
||||
// Preserve paths that can change observed slots or Array prototype
|
||||
// effects; canonicalize suffixes that cannot distinguish lattice facts.
|
||||
let path = normalizePath(binding.bindingId, current.path, effect);
|
||||
let path = observablePaths.normalize(
|
||||
binding.bindingId,
|
||||
current.path,
|
||||
effect,
|
||||
);
|
||||
if (path === null) continue;
|
||||
const selectorIndex = path.findIndex(
|
||||
(key) => key !== null && typeof key === "object",
|
||||
@@ -698,11 +728,17 @@ interface MutationObserverNode {
|
||||
|
||||
const observableMutationPaths = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): ((
|
||||
bindingId: string,
|
||||
path: PropertyPath,
|
||||
effect: ValueEffect,
|
||||
) => PropertyPath | null) => {
|
||||
): {
|
||||
readonly normalize: (
|
||||
bindingId: string,
|
||||
path: PropertyPath,
|
||||
effect: ValueEffect,
|
||||
) => PropertyPath | null;
|
||||
readonly repeatsUnpruned: (
|
||||
bindingId: string,
|
||||
prefix: PropertyPath,
|
||||
) => boolean;
|
||||
} => {
|
||||
const bindingNodes = new Map<string, MutationObserverNode>();
|
||||
const expressions = new WeakMap<t.Node, MutationObserverNode>();
|
||||
const pendingExpressions: {
|
||||
@@ -897,7 +933,11 @@ const observableMutationPaths = (
|
||||
}
|
||||
};
|
||||
settle();
|
||||
return (bindingId, path, effect) => {
|
||||
const normalize = (
|
||||
bindingId: string,
|
||||
path: PropertyPath,
|
||||
effect: ValueEffect,
|
||||
): PropertyPath | null => {
|
||||
let observer = bindingNodes.get(bindingId);
|
||||
if (observer === undefined) return path;
|
||||
if (path.some((key) => key !== null && typeof key === "object"))
|
||||
@@ -919,6 +959,44 @@ const observableMutationPaths = (
|
||||
}
|
||||
return path;
|
||||
};
|
||||
// A projection's origins depend only on its source origins and key. Once
|
||||
// repeating a prefix revisits an origin set, every longer repetition stays
|
||||
// observable and normalization can never prune it. The answer depends only
|
||||
// on the binding and prefix, so every revisit of a long chain shares it.
|
||||
const repeated = new Map<string, boolean>();
|
||||
const repeatsUnpruned = (
|
||||
bindingId: string,
|
||||
prefix: PropertyPath,
|
||||
): boolean => {
|
||||
const key = JSON.stringify([bindingId, prefix]);
|
||||
const cached = repeated.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
let observer = bindingNodes.get(bindingId);
|
||||
const signatures = new Set<string>();
|
||||
let result = false;
|
||||
while (observer !== undefined && prefix.length > 0) {
|
||||
observer = project(observer, prefix);
|
||||
settle();
|
||||
if ((observer.origins?.size ?? 0) === 0) break;
|
||||
const signature = JSON.stringify(
|
||||
[...(observer.origins ?? [])]
|
||||
.map((origin) =>
|
||||
typeof origin === "string"
|
||||
? origin
|
||||
: `${String(origin.start)}:${String(origin.end)}`,
|
||||
)
|
||||
.sort(compareUnicodeCodePoints),
|
||||
);
|
||||
if (signatures.has(signature)) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
signatures.add(signature);
|
||||
}
|
||||
repeated.set(key, result);
|
||||
return result;
|
||||
};
|
||||
return { normalize, repeatsUnpruned };
|
||||
};
|
||||
|
||||
const arrayReferencedValues = (
|
||||
|
||||
Reference in new issue
Block a user