mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
1111 lines
36 KiB
TypeScript
1111 lines
36 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
delayedInstanceFieldValue,
|
|
semanticComputedMethodKey,
|
|
semanticBranchMayBeSkipped,
|
|
semanticIterationCardinality,
|
|
} from "./javascriptSemanticEvaluationOrder.js";
|
|
|
|
import {
|
|
semanticStaticPropertyKey,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import { semanticScopeId } from "./javascriptSemanticState.js";
|
|
import type {
|
|
JavaScriptSemanticBindingState,
|
|
JavaScriptSemanticAnalysisState,
|
|
JavaScriptSemanticEffectTiming,
|
|
} from "./javascriptSemanticState.js";
|
|
|
|
interface StatementPosition {
|
|
readonly body: readonly t.Statement[];
|
|
readonly index: number;
|
|
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
|
|
* re-derive the same positions along shared ancestry, so retain the first
|
|
* result for as long as its parent map is reachable.
|
|
*/
|
|
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<
|
|
WeakMap<t.Node, t.Node>,
|
|
InitializerPositionCache
|
|
>();
|
|
|
|
const initializerPositionCache = (
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): InitializerPositionCache => {
|
|
const cached = initializerPositionCaches.get(parents);
|
|
if (cached !== undefined) return cached;
|
|
const created: InitializerPositionCache = {
|
|
direct: new WeakMap(),
|
|
mutation: new WeakMap(),
|
|
reference: new WeakMap(),
|
|
read: new WeakMap(),
|
|
evaluationChildren: new WeakMap(),
|
|
expressionAncestry: undefined,
|
|
};
|
|
initializerPositionCaches.set(parents, created);
|
|
return created;
|
|
};
|
|
|
|
const statementPosition = (
|
|
statement: t.Statement,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
declarator?: t.VariableDeclarator,
|
|
): StatementPosition | null => {
|
|
const directParent = parents.get(statement);
|
|
const enclosing =
|
|
(t.isExportNamedDeclaration(directParent) ||
|
|
t.isExportDefaultDeclaration(directParent)) &&
|
|
directParent.declaration === statement
|
|
? directParent
|
|
: statement;
|
|
const parent = parents.get(enclosing);
|
|
if (!t.isProgram(parent) && !t.isBlockStatement(parent)) return null;
|
|
const index = parent.body.indexOf(enclosing);
|
|
return index < 0
|
|
? null
|
|
: {
|
|
body: parent.body,
|
|
index,
|
|
declaratorIndex:
|
|
t.isVariableDeclaration(statement) && declarator !== undefined
|
|
? statement.declarations.indexOf(declarator)
|
|
: -1,
|
|
};
|
|
};
|
|
|
|
const positionPrecedes = (
|
|
left: StatementPosition,
|
|
right: StatementPosition,
|
|
): boolean =>
|
|
left.body === right.body &&
|
|
(left.index < right.index ||
|
|
(left.index === right.index &&
|
|
left.declaratorIndex >= 0 &&
|
|
left.declaratorIndex < right.declaratorIndex));
|
|
|
|
/** Order effects only in a non-repeating top-level activation. */
|
|
export const semanticTopLevelEffectFollowsCapture = (
|
|
capture: t.Node,
|
|
effect: t.Node | undefined,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
const position = (node: t.Node | undefined): StatementPosition | null => {
|
|
let selected: StatementPosition | null = null;
|
|
let declarator: t.VariableDeclarator | undefined;
|
|
let child: t.Node | undefined;
|
|
while (node !== undefined) {
|
|
if (t.isFunction(node) || t.isLoop(node)) return null;
|
|
if (delayedInstanceFieldValue(node, child)) return null;
|
|
if (t.isVariableDeclarator(node)) declarator = node;
|
|
if (t.isStatement(node)) {
|
|
const enclosing = statementPosition(node, parents, declarator);
|
|
if (enclosing !== null) selected = enclosing;
|
|
}
|
|
if (t.isProgram(node)) return selected;
|
|
child = node;
|
|
node = parents.get(node);
|
|
}
|
|
return null;
|
|
};
|
|
const before = position(capture);
|
|
const after = position(effect);
|
|
if (before === null || after === null) return false;
|
|
if (positionPrecedes(before, after)) return true;
|
|
if (
|
|
before.body !== after.body ||
|
|
before.index !== after.index ||
|
|
before.declaratorIndex !== after.declaratorIndex
|
|
)
|
|
return false;
|
|
return expressionEvaluationOrder(capture, effect, parents) === "before";
|
|
};
|
|
|
|
const scopeExitMarkers = new WeakMap<t.Node, t.EmptyStatement>();
|
|
const scopeExitOwners = new WeakMap<t.Node, t.Node>();
|
|
|
|
/** A derived scope-end effect keeps disposal separate from initializer evaluation. */
|
|
export const semanticScopeExitEffect = (scope: t.Node): t.Node => {
|
|
const cached = scopeExitMarkers.get(scope);
|
|
if (cached !== undefined) return cached;
|
|
const marker = t.emptyStatement();
|
|
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);
|
|
scopeExitOwners.set(marker, scope);
|
|
return marker;
|
|
};
|
|
|
|
const withinScopeExit = (
|
|
reference: t.Node,
|
|
scope: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
for (
|
|
let node: t.Node | undefined = reference;
|
|
node !== undefined;
|
|
node = parents.get(node)
|
|
) {
|
|
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;
|
|
};
|
|
|
|
/** Include expression order when a write operation contains the read site. */
|
|
export const semanticEffectFollowsReference = (
|
|
reference: t.Node,
|
|
effect: t.Node | undefined,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
if (reference === effect) return false;
|
|
const exit = effect === undefined ? undefined : scopeExitOwners.get(effect);
|
|
if (exit !== undefined) {
|
|
if (withinScopeExit(reference, exit, parents)) return true;
|
|
effect = exit;
|
|
}
|
|
let current: t.Node | undefined = reference;
|
|
let child: t.Node | undefined;
|
|
while (current !== undefined && current !== effect) {
|
|
if (t.isFunction(current)) break;
|
|
child = current;
|
|
current = parents.get(current);
|
|
}
|
|
const loopInput =
|
|
(t.isForInStatement(effect) || t.isForOfStatement(effect)) &&
|
|
child === effect.right;
|
|
let once = current;
|
|
child = undefined;
|
|
while (once !== undefined) {
|
|
const initialLoopInput =
|
|
(t.isForStatement(once) && child === once.init) ||
|
|
((t.isForInStatement(once) || t.isForOfStatement(once)) &&
|
|
child === once.right) ||
|
|
(once === effect && loopInput);
|
|
if (
|
|
t.isFunction(once) ||
|
|
(t.isLoop(once) && !initialLoopInput) ||
|
|
delayedInstanceFieldValue(once, child)
|
|
)
|
|
break;
|
|
child = once;
|
|
once = parents.get(once);
|
|
}
|
|
return (
|
|
(current === effect &&
|
|
once === undefined &&
|
|
(t.isAssignmentExpression(effect) ||
|
|
t.isUpdateExpression(effect) ||
|
|
t.isVariableDeclarator(effect) ||
|
|
t.isCallExpression(effect) ||
|
|
t.isOptionalCallExpression(effect) ||
|
|
t.isNewExpression(effect))) ||
|
|
(current === effect && once === undefined && loopInput) ||
|
|
semanticTopLevelEffectFollowsCapture(reference, effect, parents)
|
|
);
|
|
};
|
|
|
|
export const semanticEffectTiming = (
|
|
site: t.Node,
|
|
effect: t.Node | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticEffectTiming => {
|
|
if (effect === undefined) return "unknown";
|
|
const exit = scopeExitOwners.get(effect);
|
|
if (exit !== undefined) {
|
|
if (withinScopeExit(site, exit, state.parentsByNode)) return "after";
|
|
effect = exit;
|
|
}
|
|
// Invocation receives arguments after their values have been selected.
|
|
if (
|
|
site === effect &&
|
|
(t.isCallExpression(site) ||
|
|
t.isOptionalCallExpression(site) ||
|
|
t.isNewExpression(site))
|
|
)
|
|
return "after";
|
|
const loop = semanticEnclosingIteration(effect, state);
|
|
if (loop !== undefined) {
|
|
const cardinality = semanticIterationCardinality(
|
|
loop,
|
|
state.prototypeEnumerationMayAffect,
|
|
);
|
|
if (cardinality === "empty") return "after";
|
|
if (cardinality === "nonempty") {
|
|
const loopParent = state.parentsByNode.get(loop);
|
|
const crossesFunction = (node: t.Node): boolean => {
|
|
let current: t.Node | undefined = node;
|
|
while (current !== undefined) {
|
|
if (t.isFunction(current)) return true;
|
|
current = state.parentsByNode.get(current);
|
|
}
|
|
return false;
|
|
};
|
|
if (
|
|
loopParent === state.program &&
|
|
!crossesFunction(site) &&
|
|
typeof loop.start === "number" &&
|
|
typeof loop.end === "number" &&
|
|
typeof site.start === "number"
|
|
) {
|
|
if (site.start > loop.end) return "before";
|
|
if (site.start < loop.start) return "after";
|
|
}
|
|
}
|
|
}
|
|
if (semanticEffectFollowsReference(site, effect, state.parentsByNode))
|
|
return "after";
|
|
if (semanticEffectFollowsReference(effect, site, state.parentsByNode))
|
|
return "before";
|
|
return "unknown";
|
|
};
|
|
|
|
const semanticEnclosingIteration = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): t.ForInStatement | t.ForOfStatement | undefined => {
|
|
let current: t.Node | undefined = node;
|
|
while (current !== undefined) {
|
|
if (t.isForInStatement(current) || t.isForOfStatement(current))
|
|
return current;
|
|
if (t.isFunction(current)) return undefined;
|
|
current = state.parentsByNode.get(current);
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/** 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>,
|
|
): "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.isClassDeclaration(node) && node.id !== null)
|
|
return statementPosition(node, parents);
|
|
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);
|
|
return t.isVariableDeclaration(declaration)
|
|
? statementPosition(declaration, parents, parent)
|
|
: null;
|
|
}
|
|
if (
|
|
t.isAssignmentExpression(parent) &&
|
|
parent.right === node &&
|
|
t.isIdentifier(parent.left)
|
|
) {
|
|
return directMutationPosition(parent, parents);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const directInitializerPosition = (
|
|
node: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
const cache = initializerPositionCache(parents);
|
|
const cached = cache.direct.get(node);
|
|
if (cached !== undefined) return cached;
|
|
const computed = computeDirectInitializerPosition(node, parents);
|
|
cache.direct.set(node, computed);
|
|
return computed;
|
|
};
|
|
|
|
const computeDirectMutationPosition = (
|
|
node: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
if (t.isForOfStatement(node)) return statementPosition(node, parents);
|
|
if (t.isVariableDeclarator(node)) {
|
|
const declaration = parents.get(node);
|
|
return t.isVariableDeclaration(declaration)
|
|
? statementPosition(declaration, parents, node)
|
|
: null;
|
|
}
|
|
if (t.isSpreadElement(node)) {
|
|
const array = parents.get(node);
|
|
return t.isArrayExpression(array)
|
|
? semanticCapturePosition(array, parents)
|
|
: null;
|
|
}
|
|
if (
|
|
!t.isAssignmentExpression(node) &&
|
|
!t.isUpdateExpression(node) &&
|
|
!t.isUnaryExpression(node, { operator: "delete" }) &&
|
|
!t.isCallExpression(node) &&
|
|
!t.isOptionalCallExpression(node) &&
|
|
!t.isNewExpression(node) &&
|
|
!t.isTaggedTemplateExpression(node)
|
|
)
|
|
return null;
|
|
let current: t.Node = node;
|
|
while (true) {
|
|
const parent = parents.get(current);
|
|
if (
|
|
parent === undefined ||
|
|
t.isFunction(parent) ||
|
|
semanticBranchMayBeSkipped(parent, current)
|
|
)
|
|
return null;
|
|
if (t.isStatement(parent)) return statementPosition(parent, parents);
|
|
if (t.isVariableDeclarator(parent)) {
|
|
const declaration = parents.get(parent);
|
|
return t.isVariableDeclaration(declaration)
|
|
? statementPosition(declaration, parents, parent)
|
|
: null;
|
|
}
|
|
current = parent;
|
|
}
|
|
};
|
|
|
|
const directMutationPosition = (
|
|
node: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
const cache = initializerPositionCache(parents);
|
|
const cached = cache.mutation.get(node);
|
|
if (cached !== undefined) return cached;
|
|
const computed = computeDirectMutationPosition(node, parents);
|
|
cache.mutation.set(node, computed);
|
|
return computed;
|
|
};
|
|
|
|
/** Prove that an unconditional member replacement precedes this read. */
|
|
export const semanticDefiniteEffectPrecedesReference = (
|
|
effect: t.Node,
|
|
reference: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
const before = directMutationPosition(effect, parents);
|
|
const after = bindingReadPosition(reference, parents);
|
|
if (before === null || after.position === null || after.repeating)
|
|
return false;
|
|
if (positionPrecedes(before, after.position)) return true;
|
|
return (
|
|
before.body === after.position.body &&
|
|
before.index === after.position.index &&
|
|
before.declaratorIndex === after.position.declaratorIndex &&
|
|
expressionEvaluationOrder(effect, reference, parents) === "before"
|
|
);
|
|
};
|
|
|
|
const computeReferenceInitializerPosition = (
|
|
node: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
// Defaults live inside the declaration pattern rather than its initializer.
|
|
let current = node;
|
|
while (true) {
|
|
const direct = directInitializerPosition(current, parents);
|
|
if (direct !== null) return direct;
|
|
const parent = parents.get(current);
|
|
if (
|
|
t.isAssignmentExpression(parent, { operator: "=" }) &&
|
|
(t.isObjectPattern(parent.left) || t.isArrayPattern(parent.left))
|
|
)
|
|
return directMutationPosition(parent, parents);
|
|
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);
|
|
return t.isVariableDeclaration(declaration)
|
|
? statementPosition(declaration, parents, parent)
|
|
: null;
|
|
}
|
|
current = parent;
|
|
}
|
|
};
|
|
|
|
const referenceInitializerPosition = (
|
|
node: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
const cache = initializerPositionCache(parents);
|
|
const cached = cache.reference.get(node);
|
|
if (cached !== undefined) return cached;
|
|
const computed = computeReferenceInitializerPosition(node, parents);
|
|
cache.reference.set(node, computed);
|
|
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,
|
|
mutation: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): Pick<
|
|
JavaScriptSemanticBindingState,
|
|
"initializers" | "referenceInitializers" | "directOrigins"
|
|
> => {
|
|
const mutationPosition =
|
|
directMutationPosition(mutation, parents) ??
|
|
referenceInitializerPosition(mutation, parents);
|
|
if (mutationPosition === null) return binding;
|
|
const origins = [
|
|
...binding.initializers.map((initializer) => ({
|
|
initializer,
|
|
position: initializerPosition(initializer, parents),
|
|
})),
|
|
...binding.referenceInitializers.map((initializer) => ({
|
|
initializer,
|
|
position: initializerPosition(initializer, parents, true),
|
|
})),
|
|
];
|
|
if (
|
|
origins.some(
|
|
({ position }) =>
|
|
position === null || !positionPrecedes(position, mutationPosition),
|
|
)
|
|
)
|
|
return binding;
|
|
const { retained, replacesEntry } = retainedInitializerOrigins(
|
|
origins,
|
|
binding,
|
|
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: replacesEntry ? [] : binding.directOrigins,
|
|
initializers: binding.initializers.filter((initializer) =>
|
|
retained.has(initializer),
|
|
),
|
|
referenceInitializers: binding.referenceInitializers.filter((initializer) =>
|
|
retained.has(initializer),
|
|
),
|
|
};
|
|
};
|
|
|
|
/** Reuse immutable ancestry facts across reads nested in the same expression. */
|
|
const bindingReadPosition = (
|
|
read: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): BindingReadPosition => {
|
|
const cache = initializerPositionCache(parents).read;
|
|
const path: t.Node[] = [];
|
|
let current = read;
|
|
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) && !semanticComputedMethodKey(parent, current)) ||
|
|
delayedInstanceFieldValue(parent, current)
|
|
) {
|
|
result = { position: null, repeating: false };
|
|
break;
|
|
}
|
|
current = parent;
|
|
}
|
|
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";
|
|
/** A retained initializer executes on paths reaching this particular read. */
|
|
readonly hasDefiniteInitializer: boolean;
|
|
/** 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, hasDefiniteInitializer: false };
|
|
const [soleInitializer] = binding.initializers;
|
|
if (
|
|
binding.initializers.length === 1 &&
|
|
binding.referenceInitializers.length === 0 &&
|
|
binding.declarationKinds.size === 0 &&
|
|
binding.definitions.length === 1 &&
|
|
binding.definitions[0]?.kind === "function" &&
|
|
soleInitializer !== undefined &&
|
|
t.isFunctionDeclaration(soleInitializer.node)
|
|
)
|
|
return { status: "ordered", binding, hasDefiniteInitializer: true };
|
|
if (
|
|
soleInitializer !== undefined &&
|
|
t.isClass(soleInitializer.node) &&
|
|
binding.scopeId === semanticScopeId("class", soleInitializer.node) &&
|
|
!binding.mutable &&
|
|
binding.initializers.length === 1 &&
|
|
binding.referenceInitializers.length === 0 &&
|
|
!binding.definitions.some(({ kind }) => kind === "assignment")
|
|
) {
|
|
// The class self name is initialized before fields/static blocks run and
|
|
// before a body method can be invoked, but not during heritage/key evaluation.
|
|
const owner = soleInitializer.node;
|
|
let child = read;
|
|
let memberPart: t.Node | undefined;
|
|
for (
|
|
let parent = parents.get(child);
|
|
parent !== undefined;
|
|
child = parent, parent = parents.get(parent)
|
|
) {
|
|
if (parents.get(parent) === owner.body) memberPart = child;
|
|
if (parent === owner.body) {
|
|
if (
|
|
t.isClassMethod(child) || t.isClassPrivateMethod(child)
|
|
? memberPart === child.body
|
|
: t.isStaticBlock(child) ||
|
|
((t.isClassProperty(child) ||
|
|
t.isClassPrivateProperty(child) ||
|
|
t.isClassAccessorProperty(child)) &&
|
|
memberPart === child.value)
|
|
)
|
|
return { status: "ordered", binding, hasDefiniteInitializer: true };
|
|
break;
|
|
}
|
|
if (parent === owner) break;
|
|
}
|
|
}
|
|
const initializer = binding.initializers[0];
|
|
const reference = binding.referenceInitializers[0];
|
|
const origin = initializer ?? reference;
|
|
const sourcePosition =
|
|
initializer === undefined
|
|
? reference === undefined
|
|
? null
|
|
: initializerPosition(reference, parents, true)
|
|
: initializerPosition(initializer, parents);
|
|
const { position, repeating } = bindingReadPosition(read, parents);
|
|
// Coverage and latest-value selection are independent. A preceding write
|
|
// can cover entry even when another write's order remains unknown.
|
|
const hasDefiniteInitializer =
|
|
position !== null &&
|
|
[
|
|
...binding.initializers,
|
|
...binding.referenceInitializers.filter(({ assignment }) => assignment),
|
|
].some((initializer) => {
|
|
const source = initializerPosition(
|
|
initializer,
|
|
parents,
|
|
binding.referenceInitializers.includes(initializer),
|
|
);
|
|
return (
|
|
source !== null &&
|
|
positionPrecedes(source, position) &&
|
|
!initializerMayRetainEntry(initializer, binding, parents)
|
|
);
|
|
});
|
|
if (
|
|
!repeating &&
|
|
origin !== undefined &&
|
|
binding.initializers.length + binding.referenceInitializers.length === 1 &&
|
|
sourcePosition !== null &&
|
|
position !== null &&
|
|
positionPrecedes(sourcePosition, position)
|
|
)
|
|
return {
|
|
status: "ordered",
|
|
binding,
|
|
hasDefiniteInitializer,
|
|
};
|
|
const uncertainOrder =
|
|
repeating ||
|
|
sourcePosition === null ||
|
|
position === null ||
|
|
sourcePosition.body !== position.body;
|
|
if (uncertainOrder)
|
|
return { status: "unresolved", binding, hasDefiniteInitializer };
|
|
const selected = selectInitializers(binding, position, read, parents);
|
|
if (selected === undefined)
|
|
return { status: "unresolved", binding, hasDefiniteInitializer };
|
|
if (selected.binding === binding)
|
|
return {
|
|
status: "ordered",
|
|
binding,
|
|
hasDefiniteInitializer:
|
|
hasDefiniteInitializer || selected.hasDefiniteInitializer,
|
|
};
|
|
return {
|
|
status: "ordered",
|
|
hasDefiniteInitializer:
|
|
hasDefiniteInitializer || selected.hasDefiniteInitializer,
|
|
binding: {
|
|
...binding,
|
|
...selected.binding,
|
|
// 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>,
|
|
):
|
|
| {
|
|
readonly binding: Pick<
|
|
JavaScriptSemanticBindingState,
|
|
"initializers" | "referenceInitializers"
|
|
>;
|
|
readonly hasDefiniteInitializer: boolean;
|
|
}
|
|
| undefined => {
|
|
if (position === null) return undefined;
|
|
const origins = [
|
|
...binding.initializers.map((initializer) => ({
|
|
initializer,
|
|
position: initializerPosition(initializer, parents),
|
|
})),
|
|
...binding.referenceInitializers.map((initializer) => ({
|
|
initializer,
|
|
position: initializerPosition(initializer, parents, true),
|
|
})),
|
|
];
|
|
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
|
|
)
|
|
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, replacesEntry } = retainedInitializerOrigins(
|
|
preceding,
|
|
binding,
|
|
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.
|
|
if (
|
|
binding.initializers.every((initializer) => retained.has(initializer)) &&
|
|
binding.referenceInitializers.every((initializer) =>
|
|
retained.has(initializer),
|
|
)
|
|
)
|
|
return { binding, hasDefiniteInitializer: replacesEntry };
|
|
return {
|
|
hasDefiniteInitializer: replacesEntry,
|
|
binding: {
|
|
initializers: binding.initializers.filter((initializer) =>
|
|
retained.has(initializer),
|
|
),
|
|
referenceInitializers: binding.referenceInitializers.filter(
|
|
(initializer) => retained.has(initializer),
|
|
),
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Logical writes may keep prior aliases. An RHS fact alone does not prove an
|
|
* empty local: the complete binding must exclude incoming/default/other writes.
|
|
*/
|
|
const initializerMayRetainEntry = (
|
|
initializer: JavaScriptSemanticBindingState["initializers"][number],
|
|
binding: JavaScriptSemanticBindingState,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
const node = initializer.node,
|
|
parent = parents.get(node);
|
|
const assignment = t.isAssignmentExpression(node)
|
|
? node
|
|
: t.isAssignmentExpression(parent) && parent.right === node
|
|
? parent
|
|
: undefined;
|
|
if (
|
|
assignment === undefined ||
|
|
!["&&=", "||=", "??="].includes(assignment.operator)
|
|
)
|
|
return false;
|
|
const emptyLocal =
|
|
node === assignment.right &&
|
|
binding.kind === "variable" &&
|
|
!binding.runtimeImport &&
|
|
binding.initializers.length === 1 &&
|
|
binding.initializers[0] === initializer &&
|
|
binding.referenceInitializers.length === 0 &&
|
|
binding.directOrigins.length === 0 &&
|
|
binding.definitions.some(({ kind }) => kind === "variable") &&
|
|
binding.definitions.every(
|
|
({ kind }) => kind === "variable" || kind === "assignment",
|
|
);
|
|
return !emptyLocal;
|
|
};
|
|
|
|
/** Certain writes replace prior origins; short-circuit writes retain both. */
|
|
const retainedInitializerOrigins = (
|
|
origins: readonly {
|
|
readonly initializer: JavaScriptSemanticBindingState["initializers"][number];
|
|
readonly position: StatementPosition | null;
|
|
}[],
|
|
binding: JavaScriptSemanticBindingState,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
) => {
|
|
const latest = origins.reduce<StatementPosition | null>(
|
|
(latest, { initializer, position }) =>
|
|
position !== null &&
|
|
!initializerMayRetainEntry(initializer, binding, 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,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): StatementPosition | null => {
|
|
let current = node;
|
|
while (true) {
|
|
const direct = directInitializerPosition(current, parents);
|
|
if (direct !== null) return direct;
|
|
const parent = parents.get(current);
|
|
if (
|
|
t.isObjectProperty(parent) &&
|
|
parent.value === current &&
|
|
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;
|
|
}
|
|
};
|
|
|
|
/** Intermediate values must themselves have been initialized before capture. */
|
|
export const semanticBindingPrecedesCapture = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
capture: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
const initializer = binding.initializers[0];
|
|
if (binding.initializers.length !== 1 || initializer === undefined)
|
|
return false;
|
|
const sourcePosition = initializerPosition(initializer, parents);
|
|
const capturePosition = semanticCapturePosition(capture, parents);
|
|
return (
|
|
sourcePosition !== null &&
|
|
capturePosition !== null &&
|
|
positionPrecedes(sourcePosition, capturePosition)
|
|
);
|
|
};
|
|
|
|
/** A preceding initializer and capture in one body precede its later effect. */
|
|
export const semanticBindingEffectFollowsCapture = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
capture: t.Node,
|
|
effect: t.Node,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
if (!semanticBindingPrecedesCapture(binding, capture, parents)) return false;
|
|
const captured = semanticCapturePosition(capture, parents);
|
|
const written = directMutationPosition(effect, parents);
|
|
return (
|
|
captured !== null && written !== null && positionPrecedes(captured, written)
|
|
);
|
|
};
|
|
|
|
/** Ignore later escapes only while reading earlier references in one activation. */
|
|
export const semanticEscapeFollowsCapture = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
capture: t.Node,
|
|
escape: t.Node | undefined,
|
|
parents: WeakMap<t.Node, t.Node>,
|
|
): boolean => {
|
|
const initializer = binding.initializers[0];
|
|
if (
|
|
escape === undefined ||
|
|
initializer === undefined ||
|
|
!semanticBindingPrecedesCapture(binding, capture, parents)
|
|
)
|
|
return false;
|
|
// 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;
|
|
if (
|
|
!t.isObjectExpression(origin) &&
|
|
!t.isArrayExpression(origin) &&
|
|
!t.isIdentifier(origin) &&
|
|
!t.isMemberExpression(origin) &&
|
|
!t.isOptionalMemberExpression(origin)
|
|
)
|
|
return false;
|
|
const capturePosition = semanticCapturePosition(capture, parents);
|
|
const exit = scopeExitOwners.get(escape);
|
|
if (exit !== undefined) {
|
|
// Unlike a source-global ordering proof, this binding was initialized in
|
|
// the captured body. Its later sibling scope exits follow this activation.
|
|
const exitPosition = t.isStatement(exit)
|
|
? statementPosition(exit, parents)
|
|
: null;
|
|
return (
|
|
withinScopeExit(capture, exit, parents) ||
|
|
(capturePosition !== null &&
|
|
exitPosition !== null &&
|
|
positionPrecedes(capturePosition, exitPosition))
|
|
);
|
|
}
|
|
const escapePosition = directMutationPosition(escape, parents);
|
|
return (
|
|
capturePosition !== null &&
|
|
escapePosition !== null &&
|
|
positionPrecedes(capturePosition, escapePosition)
|
|
);
|
|
};
|