mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 02:45:58 +00:00
fix(javascript): preserve values captured before later assignments (#4196)
Select binding facts at reads without mixing whole-binding and temporal values. Share conditional activation rules, preserve partial expression order, and bound immutable ancestry work. Memoize temporal results within each query using initializer identity, capture point, budget and dependency proofs; preserve depth limits and source Evidence. Fixes #3881
This commit is contained in:
1 parent
e2db768012
commit
0cc56493be
13 files changed
+1286
-273
No files matched your search
@@ -1,5 +1,10 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
delayedInstanceFieldValue,
|
||||
semanticBranchMayBeSkipped,
|
||||
} from "./javascriptSemanticEvaluationOrder.js";
|
||||
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
@@ -12,6 +17,23 @@ interface StatementPosition {
|
||||
readonly declaratorIndex: number;
|
||||
}
|
||||
|
||||
interface BindingReadPosition {
|
||||
readonly position: StatementPosition | null;
|
||||
readonly repeating: boolean;
|
||||
}
|
||||
|
||||
interface ExpressionSiblings {
|
||||
readonly common: t.Node;
|
||||
readonly earlier: t.Node;
|
||||
readonly later: t.Node;
|
||||
}
|
||||
|
||||
interface ExpressionAncestry {
|
||||
readonly source: t.Node;
|
||||
readonly ancestors: ReadonlyMap<t.Node, t.Node | undefined>;
|
||||
readonly siblings: WeakMap<t.Node, ExpressionSiblings | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializer positions depend only on a node and its parent map, which one
|
||||
* analysis builds once and never changes. Different mutations and initializers
|
||||
@@ -22,6 +44,14 @@ interface InitializerPositionCache {
|
||||
readonly direct: WeakMap<t.Node, StatementPosition | null>;
|
||||
readonly mutation: WeakMap<t.Node, StatementPosition | null>;
|
||||
readonly reference: WeakMap<t.Node, StatementPosition | null>;
|
||||
readonly read: WeakMap<t.Node, BindingReadPosition>;
|
||||
readonly evaluationChildren: WeakMap<
|
||||
t.Node,
|
||||
ReadonlyMap<t.Node, number> | null
|
||||
>;
|
||||
// One source context bounds retained ancestry facts to O(AST), even when
|
||||
// an expression contains many distinct writes. Reuse its descendant paths.
|
||||
expressionAncestry: ExpressionAncestry | undefined;
|
||||
}
|
||||
|
||||
const initializerPositionCaches = new WeakMap<
|
||||
@@ -38,6 +68,9 @@ const initializerPositionCache = (
|
||||
direct: new WeakMap(),
|
||||
mutation: new WeakMap(),
|
||||
reference: new WeakMap(),
|
||||
read: new WeakMap(),
|
||||
evaluationChildren: new WeakMap(),
|
||||
expressionAncestry: undefined,
|
||||
};
|
||||
initializerPositionCaches.set(parents, created);
|
||||
return created;
|
||||
@@ -85,14 +118,7 @@ export const semanticTopLevelEffectFollowsCapture = (
|
||||
let child: t.Node | undefined;
|
||||
while (node !== undefined) {
|
||||
if (t.isFunction(node) || t.isLoop(node)) return null;
|
||||
if (
|
||||
(t.isClassProperty(node) ||
|
||||
t.isClassPrivateProperty(node) ||
|
||||
t.isClassAccessorProperty(node)) &&
|
||||
!node.static &&
|
||||
node.value === child
|
||||
)
|
||||
return null;
|
||||
if (delayedInstanceFieldValue(node, child)) return null;
|
||||
if (t.isVariableDeclarator(node)) declarator = node;
|
||||
if (selected === null && t.isStatement(node))
|
||||
selected = statementPosition(node, parents, declarator);
|
||||
@@ -112,59 +138,132 @@ export const semanticTopLevelEffectFollowsCapture = (
|
||||
before.declaratorIndex !== after.declaratorIndex
|
||||
)
|
||||
return false;
|
||||
return expressionEvaluationPrecedes(capture, effect, parents);
|
||||
return expressionEvaluationOrder(capture, effect, parents) === "before";
|
||||
};
|
||||
|
||||
/** Prove ordering only where JavaScript evaluates sibling expressions left-to-right. */
|
||||
const expressionEvaluationPrecedes = (
|
||||
/** Memoize descendant paths toward the currently compared source expression. */
|
||||
const expressionSiblings = (
|
||||
earlier: t.Node,
|
||||
later: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): ExpressionSiblings | null => {
|
||||
const cache = initializerPositionCache(parents);
|
||||
let context = cache.expressionAncestry;
|
||||
if (context?.source !== earlier) {
|
||||
const ancestors = new Map<t.Node, t.Node | undefined>();
|
||||
let child: t.Node | undefined;
|
||||
for (
|
||||
let node: t.Node | undefined = earlier;
|
||||
node !== undefined;
|
||||
node = parents.get(node)
|
||||
) {
|
||||
ancestors.set(node, child);
|
||||
child = node;
|
||||
}
|
||||
context = { source: earlier, ancestors, siblings: new WeakMap() };
|
||||
cache.expressionAncestry = context;
|
||||
}
|
||||
const path: t.Node[] = [];
|
||||
let result: ExpressionSiblings | null = null;
|
||||
for (
|
||||
let node: t.Node | undefined = later;
|
||||
node !== undefined;
|
||||
node = parents.get(node)
|
||||
) {
|
||||
if (context.ancestors.has(node)) {
|
||||
const sourceChild = context.ancestors.get(node);
|
||||
const readChild = path.at(-1);
|
||||
if (sourceChild !== undefined && readChild !== undefined)
|
||||
result = { common: node, earlier: sourceChild, later: readChild };
|
||||
break;
|
||||
}
|
||||
const cached = context.siblings.get(node);
|
||||
if (cached !== undefined) {
|
||||
result = cached;
|
||||
break;
|
||||
}
|
||||
path.push(node);
|
||||
}
|
||||
for (const node of path) context.siblings.set(node, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Index existing child arrays once; wide literals/calls need no repeated copies. */
|
||||
const expressionChildIndices = (
|
||||
node: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): ReadonlyMap<t.Node, number> | null => {
|
||||
const cache = initializerPositionCache(parents).evaluationChildren;
|
||||
const cached = cache.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const children = ((): readonly (t.Node | null)[] | null => {
|
||||
if (t.isSequenceExpression(node)) return node.expressions;
|
||||
if (t.isArrayExpression(node)) return node.elements;
|
||||
if (t.isObjectExpression(node)) return node.properties;
|
||||
if (t.isObjectProperty(node))
|
||||
return node.computed ? [node.key, node.value] : [node.value];
|
||||
if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
return node.arguments;
|
||||
if (t.isBinaryExpression(node) || t.isLogicalExpression(node))
|
||||
return [node.left, node.right];
|
||||
if (t.isConditionalExpression(node)) return [node.test];
|
||||
return null;
|
||||
})();
|
||||
if (children === null) {
|
||||
cache.set(node, null);
|
||||
return null;
|
||||
}
|
||||
const indices = new Map<t.Node, number>();
|
||||
let index = 0;
|
||||
if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
indices.set(node.callee, index++);
|
||||
for (const child of children) {
|
||||
if (child !== null) indices.set(child, index);
|
||||
index++;
|
||||
}
|
||||
cache.set(node, indices);
|
||||
return indices;
|
||||
};
|
||||
|
||||
/** Unproved ordering stays distinct from proof that a write follows the read. */
|
||||
const expressionEvaluationOrder = (
|
||||
earlier: t.Node,
|
||||
later: t.Node | undefined,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): boolean => {
|
||||
if (later === undefined) return false;
|
||||
const earlierAncestors = new Set<t.Node>();
|
||||
let current: t.Node | undefined = earlier;
|
||||
while (current !== undefined) {
|
||||
earlierAncestors.add(current);
|
||||
current = parents.get(current);
|
||||
}
|
||||
const laterPath: t.Node[] = [];
|
||||
current = later;
|
||||
while (current !== undefined && !earlierAncestors.has(current)) {
|
||||
laterPath.push(current);
|
||||
current = parents.get(current);
|
||||
}
|
||||
const common = current;
|
||||
if (common === undefined || common === earlier || common === later)
|
||||
return false;
|
||||
let earlierChild: t.Node = earlier;
|
||||
while (parents.get(earlierChild) !== common) {
|
||||
const parent = parents.get(earlierChild);
|
||||
if (parent === undefined) return false;
|
||||
earlierChild = parent;
|
||||
}
|
||||
const laterChild = laterPath.at(-1);
|
||||
if (laterChild === undefined) return false;
|
||||
const orderedChildren = ((): readonly t.Node[] | null => {
|
||||
if (t.isSequenceExpression(common)) return common.expressions;
|
||||
if (t.isCallExpression(common) || t.isOptionalCallExpression(common))
|
||||
return [common.callee, ...common.arguments];
|
||||
if (t.isNewExpression(common)) return [common.callee, ...common.arguments];
|
||||
if (t.isBinaryExpression(common) || t.isLogicalExpression(common))
|
||||
return [common.left, common.right];
|
||||
if (t.isConditionalExpression(common)) return [common.test];
|
||||
return null;
|
||||
})();
|
||||
if (orderedChildren === null) return false;
|
||||
const earlierIndex = orderedChildren.indexOf(earlierChild);
|
||||
const laterIndex = orderedChildren.indexOf(laterChild);
|
||||
return earlierIndex >= 0 && laterIndex > earlierIndex;
|
||||
): "before" | "after" | "unresolved" => {
|
||||
if (later === undefined) return "unresolved";
|
||||
const siblings = expressionSiblings(earlier, later, parents);
|
||||
if (siblings === null) return "unresolved";
|
||||
const children = expressionChildIndices(siblings.common, parents);
|
||||
const earlierIndex = children?.get(siblings.earlier);
|
||||
const laterIndex = children?.get(siblings.later);
|
||||
if (
|
||||
earlierIndex === undefined ||
|
||||
laterIndex === undefined ||
|
||||
earlierIndex === laterIndex
|
||||
)
|
||||
return "unresolved";
|
||||
return earlierIndex < laterIndex ? "before" : "after";
|
||||
};
|
||||
|
||||
const computeDirectInitializerPosition = (
|
||||
node: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): StatementPosition | null => {
|
||||
if (
|
||||
(t.isAssignmentExpression(node) &&
|
||||
t.isIdentifier(unwrapJavaScriptExpression(node.left).node)) ||
|
||||
t.isUpdateExpression(node)
|
||||
)
|
||||
return directMutationPosition(node, parents);
|
||||
const parent = parents.get(node);
|
||||
if (t.isVariableDeclarator(parent) && parent.init === node) {
|
||||
const declaration = parents.get(parent);
|
||||
@@ -174,14 +273,10 @@ const computeDirectInitializerPosition = (
|
||||
}
|
||||
if (
|
||||
t.isAssignmentExpression(parent) &&
|
||||
parent.operator === "=" &&
|
||||
parent.right === node &&
|
||||
t.isIdentifier(parent.left)
|
||||
) {
|
||||
const statement = parents.get(parent);
|
||||
return t.isExpressionStatement(statement) && statement.expression === parent
|
||||
? statementPosition(statement, parents)
|
||||
: null;
|
||||
return directMutationPosition(parent, parents);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -231,8 +326,7 @@ const computeDirectMutationPosition = (
|
||||
if (
|
||||
parent === undefined ||
|
||||
t.isFunction(parent) ||
|
||||
(t.isLogicalExpression(parent) && parent.right === current) ||
|
||||
(t.isConditionalExpression(parent) && parent.test !== current)
|
||||
semanticBranchMayBeSkipped(parent, current)
|
||||
)
|
||||
return null;
|
||||
if (t.isStatement(parent)) return statementPosition(parent, parents);
|
||||
@@ -268,7 +362,15 @@ const computeReferenceInitializerPosition = (
|
||||
const direct = directInitializerPosition(current, parents);
|
||||
if (direct !== null) return direct;
|
||||
const parent = parents.get(current);
|
||||
if (parent === undefined || t.isStatement(parent) || t.isFunction(parent))
|
||||
if (t.isFunction(parent))
|
||||
return t.isBlockStatement(parent.body)
|
||||
? { body: parent.body.body, index: -1, declaratorIndex: -1 }
|
||||
: null;
|
||||
if (
|
||||
parent === undefined ||
|
||||
t.isStatement(parent) ||
|
||||
delayedInstanceFieldValue(parent, current)
|
||||
)
|
||||
return null;
|
||||
if (t.isVariableDeclarator(parent)) {
|
||||
const declaration = parents.get(parent);
|
||||
@@ -292,6 +394,19 @@ const referenceInitializerPosition = (
|
||||
return computed;
|
||||
};
|
||||
|
||||
/** Parameter-to-body copies and ordinary expressions share one position owner. */
|
||||
const initializerPosition = (
|
||||
initializer: JavaScriptSemanticBindingState["initializers"][number],
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
reference = false,
|
||||
): StatementPosition | null => {
|
||||
if (initializer.entryBody !== undefined)
|
||||
return { body: initializer.entryBody.body, index: -1, declaratorIndex: -1 };
|
||||
return reference
|
||||
? referenceInitializerPosition(initializer.node, parents)
|
||||
: directInitializerPosition(initializer.node, parents);
|
||||
};
|
||||
|
||||
/** Resolve origins at their use or capture, never at a later transitive effect. */
|
||||
export const semanticMutationInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
@@ -308,18 +423,11 @@ export const semanticMutationInitializers = (
|
||||
const origins = [
|
||||
...binding.initializers.map((initializer) => ({
|
||||
initializer,
|
||||
position:
|
||||
initializer.entryBody === undefined
|
||||
? directInitializerPosition(initializer.node, parents)
|
||||
: {
|
||||
body: initializer.entryBody.body,
|
||||
index: -1,
|
||||
declaratorIndex: -1,
|
||||
},
|
||||
position: initializerPosition(initializer, parents),
|
||||
})),
|
||||
...binding.referenceInitializers.map((initializer) => ({
|
||||
initializer,
|
||||
position: referenceInitializerPosition(initializer.node, parents),
|
||||
position: initializerPosition(initializer, parents, true),
|
||||
})),
|
||||
];
|
||||
if (
|
||||
@@ -329,27 +437,14 @@ export const semanticMutationInitializers = (
|
||||
)
|
||||
)
|
||||
return binding;
|
||||
const latest = origins.reduce<StatementPosition | null>(
|
||||
(latest, { position }) =>
|
||||
position !== null &&
|
||||
(latest === null || positionPrecedes(latest, position))
|
||||
? position
|
||||
: latest,
|
||||
null,
|
||||
);
|
||||
const retained = new Set(
|
||||
origins
|
||||
.filter(
|
||||
({ position }) =>
|
||||
position?.index === latest?.index &&
|
||||
position?.declaratorIndex === latest?.declaratorIndex,
|
||||
)
|
||||
.map(({ initializer }) => initializer),
|
||||
const { retained, replacesEntry } = retainedInitializerOrigins(
|
||||
origins,
|
||||
parents,
|
||||
);
|
||||
return {
|
||||
// A proven earlier assignment replaces even an import-equals value. A
|
||||
// capture before or inside an uncertain assignment retains the full binding.
|
||||
directOrigins: origins.length === 0 ? binding.directOrigins : [],
|
||||
directOrigins: replacesEntry ? [] : binding.directOrigins,
|
||||
initializers: binding.initializers.filter((initializer) =>
|
||||
retained.has(initializer),
|
||||
),
|
||||
@@ -359,96 +454,159 @@ export const semanticMutationInitializers = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Select a local alias at its read, before any later straight-line reassignment. */
|
||||
export const semanticReadInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
/** Reuse immutable ancestry facts across reads nested in the same expression. */
|
||||
const bindingReadPosition = (
|
||||
read: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): Pick<
|
||||
JavaScriptSemanticBindingState,
|
||||
"initializers" | "referenceInitializers"
|
||||
> => {
|
||||
let position: StatementPosition | null = null;
|
||||
): BindingReadPosition => {
|
||||
const cache = initializerPositionCache(parents).read;
|
||||
const path: t.Node[] = [];
|
||||
let current = read;
|
||||
for (
|
||||
let parent = parents.get(current);
|
||||
parent;
|
||||
parent = parents.get(current)
|
||||
) {
|
||||
if (t.isLoop(parent)) return binding;
|
||||
if (t.isFunction(parent)) break;
|
||||
if (position === null) {
|
||||
if (t.isVariableDeclarator(parent)) {
|
||||
const declaration = parents.get(parent);
|
||||
if (t.isVariableDeclaration(declaration))
|
||||
position = statementPosition(declaration, parents, parent);
|
||||
} else if (t.isStatement(parent))
|
||||
position = statementPosition(parent, parents);
|
||||
let result: BindingReadPosition;
|
||||
while (true) {
|
||||
const cached = cache.get(current);
|
||||
if (cached !== undefined) {
|
||||
result = cached;
|
||||
break;
|
||||
}
|
||||
path.push(current);
|
||||
const parent = parents.get(current);
|
||||
if (t.isLoop(parent)) {
|
||||
result = { position: null, repeating: true };
|
||||
break;
|
||||
}
|
||||
if (
|
||||
parent === undefined ||
|
||||
t.isFunction(parent) ||
|
||||
delayedInstanceFieldValue(parent, current)
|
||||
) {
|
||||
result = { position: null, repeating: false };
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return selectInitializers(binding, position, parents, true);
|
||||
for (let index = path.length - 1; index >= 0; index--) {
|
||||
const node = path[index];
|
||||
if (node === undefined) continue;
|
||||
const parent = parents.get(node);
|
||||
let position: StatementPosition | null = null;
|
||||
if (t.isVariableDeclarator(parent)) {
|
||||
const declaration = parents.get(parent);
|
||||
if (t.isVariableDeclaration(declaration))
|
||||
position = statementPosition(declaration, parents, parent);
|
||||
} else if (
|
||||
t.isStatement(parent) &&
|
||||
!t.isLoop(parent) &&
|
||||
!t.isFunction(parent)
|
||||
)
|
||||
position = statementPosition(parent, parents);
|
||||
if (position !== null && position !== result.position)
|
||||
result = { ...result, position };
|
||||
cache.set(node, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Select a local alias at its read, before any later straight-line reassignment. */
|
||||
type SemanticBindingRead = {
|
||||
readonly status: "ordered" | "unresolved";
|
||||
/** Unresolved reads retain all possible origins for conservative alias effects. */
|
||||
readonly binding: JavaScriptSemanticBindingState;
|
||||
};
|
||||
|
||||
export const semanticBindingAtRead = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
read: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): SemanticBindingRead => {
|
||||
if (
|
||||
binding.initializers.length === 0 &&
|
||||
binding.referenceInitializers.length === 0
|
||||
)
|
||||
return { status: "ordered", binding };
|
||||
const initializer = binding.initializers[0];
|
||||
const reference = binding.referenceInitializers[0];
|
||||
const sourcePosition =
|
||||
initializer === undefined
|
||||
? reference === undefined
|
||||
? null
|
||||
: initializerPosition(reference, parents, true)
|
||||
: initializerPosition(initializer, parents);
|
||||
const { position, repeating } = bindingReadPosition(read, parents);
|
||||
if (
|
||||
!repeating &&
|
||||
binding.initializers.length + binding.referenceInitializers.length === 1 &&
|
||||
sourcePosition !== null &&
|
||||
position !== null &&
|
||||
positionPrecedes(sourcePosition, position)
|
||||
)
|
||||
return { status: "ordered", binding };
|
||||
const uncertainOrder =
|
||||
repeating ||
|
||||
sourcePosition === null ||
|
||||
position === null ||
|
||||
sourcePosition.body !== position.body;
|
||||
if (uncertainOrder) return { status: "unresolved", binding };
|
||||
const selected = selectInitializers(binding, position, read, parents);
|
||||
if (selected === undefined) return { status: "unresolved", binding };
|
||||
if (selected === binding) return { status: "ordered", binding };
|
||||
return {
|
||||
status: "ordered",
|
||||
binding: {
|
||||
...binding,
|
||||
...selected,
|
||||
// Later assignment definitions do not describe the value captured here.
|
||||
definitions: binding.definitions.filter(
|
||||
({ kind }) => kind !== "assignment",
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const selectInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
position: StatementPosition | null,
|
||||
read: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
atRead = false,
|
||||
): Pick<
|
||||
JavaScriptSemanticBindingState,
|
||||
"initializers" | "referenceInitializers"
|
||||
> => {
|
||||
if (position === null) return binding;
|
||||
):
|
||||
| Pick<
|
||||
JavaScriptSemanticBindingState,
|
||||
"initializers" | "referenceInitializers"
|
||||
>
|
||||
| undefined => {
|
||||
if (position === null) return undefined;
|
||||
const origins = [
|
||||
...binding.initializers.map((initializer) => ({
|
||||
initializer,
|
||||
position:
|
||||
initializer.entryBody === undefined
|
||||
? directInitializerPosition(initializer.node, parents)
|
||||
: {
|
||||
body: initializer.entryBody.body,
|
||||
index: -1,
|
||||
declaratorIndex: -1,
|
||||
},
|
||||
position: initializerPosition(initializer, parents),
|
||||
})),
|
||||
...binding.referenceInitializers.map((initializer) => ({
|
||||
initializer,
|
||||
position: referenceInitializerPosition(initializer.node, parents),
|
||||
position: initializerPosition(initializer, parents, true),
|
||||
})),
|
||||
];
|
||||
if (
|
||||
origins.some(
|
||||
({ position: origin }) =>
|
||||
origin === null ||
|
||||
origin.body !== position.body ||
|
||||
(!atRead && !positionPrecedes(origin, position)),
|
||||
)
|
||||
)
|
||||
return binding;
|
||||
const preceding = atRead
|
||||
? origins.filter(
|
||||
({ position: origin }) =>
|
||||
origin !== null && positionPrecedes(origin, position),
|
||||
const preceding: typeof origins = [];
|
||||
for (const origin of origins) {
|
||||
const source = origin.position;
|
||||
if (source === null || source.body !== position.body) return undefined;
|
||||
if (positionPrecedes(source, position)) preceding.push(origin);
|
||||
else if (!positionPrecedes(position, source)) {
|
||||
if (
|
||||
source.index !== position.index ||
|
||||
source.declaratorIndex !== position.declaratorIndex
|
||||
)
|
||||
: origins;
|
||||
const latest = preceding.reduce<StatementPosition | null>(
|
||||
(latest, { position }) =>
|
||||
position !== null &&
|
||||
(latest === null || positionPrecedes(latest, position))
|
||||
? position
|
||||
: latest,
|
||||
null,
|
||||
);
|
||||
const retained = new Set(
|
||||
preceding
|
||||
.filter(
|
||||
({ position }) =>
|
||||
position?.index === latest?.index &&
|
||||
position?.declaratorIndex === latest?.declaratorIndex,
|
||||
)
|
||||
.map(({ initializer }) => initializer),
|
||||
);
|
||||
return undefined;
|
||||
const order = expressionEvaluationOrder(
|
||||
origin.initializer.node,
|
||||
read,
|
||||
parents,
|
||||
);
|
||||
// Failure to prove precedence is not proof that the write happens later.
|
||||
if (order === "unresolved") return undefined;
|
||||
if (order === "before") preceding.push(origin);
|
||||
}
|
||||
}
|
||||
const { retained } = retainedInitializerOrigins(preceding, parents);
|
||||
// Preserve the owner's identity when selection removes no facts. Consumers
|
||||
// can then reuse the binding's dependency-aware provenance memo rather than
|
||||
// treating every equivalent read as a new temporal snapshot.
|
||||
@@ -469,6 +627,56 @@ const selectInitializers = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Logical writes retain the preceding value as a possible alias. */
|
||||
const logicalInitializer = (
|
||||
node: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): boolean => {
|
||||
const parent = parents.get(node);
|
||||
const assignment = t.isAssignmentExpression(node)
|
||||
? node
|
||||
: t.isAssignmentExpression(parent) && parent.right === node
|
||||
? parent
|
||||
: undefined;
|
||||
return (
|
||||
t.isAssignmentExpression(assignment) &&
|
||||
["&&=", "||=", "??="].includes(assignment.operator)
|
||||
);
|
||||
};
|
||||
|
||||
/** Certain writes replace prior origins; short-circuit writes retain both. */
|
||||
const retainedInitializerOrigins = (
|
||||
origins: readonly {
|
||||
readonly initializer: JavaScriptSemanticBindingState["initializers"][number];
|
||||
readonly position: StatementPosition | null;
|
||||
}[],
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
) => {
|
||||
const latest = origins.reduce<StatementPosition | null>(
|
||||
(latest, { initializer, position }) =>
|
||||
position !== null &&
|
||||
!logicalInitializer(initializer.node, parents) &&
|
||||
(latest === null || positionPrecedes(latest, position))
|
||||
? position
|
||||
: latest,
|
||||
null,
|
||||
);
|
||||
return {
|
||||
replacesEntry: latest !== null,
|
||||
retained: new Set(
|
||||
origins
|
||||
.filter(
|
||||
({ position }) =>
|
||||
latest === null ||
|
||||
(position?.index === latest.index &&
|
||||
position?.declaratorIndex === latest.declaratorIndex) ||
|
||||
(position !== null && positionPrecedes(latest, position)),
|
||||
)
|
||||
.map(({ initializer }) => initializer),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/** Locate a capture only in an unconditional declaration or literal initializer. */
|
||||
export const semanticCapturePosition = (
|
||||
node: t.Node,
|
||||
@@ -500,7 +708,7 @@ export const semanticBindingPrecedesCapture = (
|
||||
const initializer = binding.initializers[0];
|
||||
if (binding.initializers.length !== 1 || initializer === undefined)
|
||||
return false;
|
||||
const sourcePosition = directInitializerPosition(initializer.node, parents);
|
||||
const sourcePosition = initializerPosition(initializer, parents);
|
||||
const capturePosition = semanticCapturePosition(capture, parents);
|
||||
return (
|
||||
sourcePosition !== null &&
|
||||
|
||||
Reference in new issue
Block a user