Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticMutationInitializers.ts
T
morlutoandShuaixinHuang b46f4fcebd fix(javascript): preserve loader authority for exact suffixes (#9477)
* fix(javascript): require exact loader suffix case

* fix(javascript): share exact loader mode across import resolution

* test(process): align symlink fixture with filesystem checkpoints

* test(javascript): normalize the module resolution oracle path separators

* test(frida): assert the CLI spawn Evidence contract

* test(mcp): use an unsupported analysis-view parent

* test(browser): separate source-location line bounds from parity

* fix(javascript): preserve uncertain loader provenance

* test(javascript): cover uncertain require resolution

* fix(javascript): retain conditional Electron evidence

* fix(javascript): preserve timed semantic provenance

* fix(javascript): retain conditional builtin effects

* test(javascript): distinguish bundled host authority

* fix(javascript): preserve nested loader invocation identity

* test(javascript): qualify Electron fixture authority

* fix(javascript): preserve loader invocation authority

* fix(javascript): respect source order for resolver escapes

* fix(javascript): check nested loader callable mutations

* fix(javascript): retain conditional constructor requests

* fix(reference): retain constructor loader relationships

* fix(reference): follow nested loader callable paths

* refactor(javascript): share callable mutation interpretation

* fix(javascript): require exact package loader suffixes

* refactor(javascript): unify conditional relation evidence

* refactor(javascript): unify conditional node evidence

* fix(javascript): retain unknown authority for mixed case sources

* fix(javascript): preserve loader uncertainty for dotfiles

* fix(reference): classify javascript suffix dotfiles

* test(reference): cover suffix dotfiles through cli

* fix(javascript): retain equivalent conditional built-in origins

* fix(javascript): preserve wrapped resolver operation

* fix(javascript): retain nested conditional built-in receivers

* test(javascript): declare CommonJS in native addon fixtures

* fix(javascript): retain wrapped resolver candidates

* style: format merged native command schema

* fix(javascript): preserve uncertain loader authority

* test(javascript): require candidate preload evidence

* test(javascript): cover reference loader member writes

---------

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
2026-11-09 13:21:24 +08:00

2058 lines
71 KiB
TypeScript

import * as t from "@babel/types";
import {
isErasedJavaScriptValueChild,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import { isTypeOnlyExportDeclaration } from "./javascriptModuleDeclaration.js";
import {
javaScriptRuntimeContext,
javaScriptThisOwner,
} from "./javascriptSourceParser.js";
import { semanticPropertyReadSources } from "./javascriptSemanticIterationSources.js";
import {
delayedInstanceFieldValue,
semanticMethodDefinitionExpression,
semanticBranchMayBeSkipped,
semanticIterationCardinality,
} from "./javascriptSemanticEvaluationOrder.js";
import {
staticObjectPropertyReader,
staticObjectOwnMethodReader,
semanticStaticPropertyKey,
semanticWriteSiteReadsFirst,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
semanticScopeId,
semanticPrototypeOwners,
type SemanticPrototypeOwner,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticEffectTiming,
} from "./javascriptSemanticState.js";
interface StatementPosition {
readonly body: readonly t.Statement[];
readonly index: number;
readonly declaratorIndex: number;
}
interface BindingReadPosition {
readonly position: StatementPosition | null;
readonly repeatingLoop: t.Loop | undefined;
}
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 topLevel: 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 enclosingFunction: WeakMap<t.Node, t.Function | null>;
readonly functionPosition: WeakMap<t.Function, StatementPosition | null>;
readonly statementIndices: WeakMap<
readonly t.Statement[],
ReadonlyMap<t.Statement, number>
>;
readonly moduleFunctionReads: WeakMap<
readonly t.Statement[],
{
readonly reads: WeakMap<t.FunctionDeclaration, number | null>;
readonly memberEffectsComplete: boolean;
readonly opaquePositions: WeakMap<t.Node, number | null>;
opaqueSitesIndexed: number;
firstOpaqueRead: number | null;
}
>;
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(),
topLevel: new WeakMap(),
mutation: new WeakMap(),
reference: new WeakMap(),
read: new WeakMap(),
enclosingFunction: new WeakMap(),
functionPosition: new WeakMap(),
statementIndices: new WeakMap(),
moduleFunctionReads: 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 cache = initializerPositionCache(parents).statementIndices;
let positions = cache.get(parent.body);
if (positions === undefined) {
positions = new Map(parent.body.map((entry, index) => [entry, index]));
cache.set(parent.body, positions);
}
const index = positions.get(enclosing) ?? -1;
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));
/** Statement position of a node evaluated once by the program, outside loops and functions. */
const computeTopLevelStatementPosition = (
node: t.Node | undefined,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
let selected: StatementPosition | null = null;
let declarator: t.VariableDeclarator | undefined;
let child: t.Node | undefined;
while (node !== undefined) {
if (
(t.isFunction(node) &&
(child === undefined ||
!semanticMethodDefinitionExpression(node, child))) ||
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;
};
// Effect ordering asks for the same capture and effect positions once per
// candidate record, so a growing record set would repeat each ancestry walk.
const topLevelStatementPosition = (
node: t.Node | undefined,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
if (node === undefined) return null;
const cache = initializerPositionCache(parents);
const cached = cache.topLevel.get(node);
if (cached !== undefined) return cached;
const computed = computeTopLevelStatementPosition(node, parents);
cache.topLevel.set(node, computed);
return computed;
};
/** 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 before = topLevelStatementPosition(capture, parents);
const after = topLevelStatementPosition(effect, parents);
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.repeatingLoop)
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"
);
};
/** Order a selected value before a later effect in this non-repeating body. */
export const semanticReadPrecedesEffectWithinActivation = (
read: t.Node,
effect: t.Node,
parents: WeakMap<t.Node, t.Node>,
): boolean => {
const before = bindingReadPosition(read, parents);
const after = directMutationPosition(effect, parents);
if (before.position === null || before.repeatingLoop || after === null)
return false;
if (positionPrecedes(before.position, after)) return true;
return (
before.position.body === after.body &&
before.position.index === after.index &&
before.position.declaratorIndex === after.declaratorIndex &&
expressionEvaluationOrder(read, effect, parents) === "before"
);
};
/** Order straight-line source effects in one activation, preserving deferred/repeating unknowns. */
export const semanticEffectTimingWithinActivation = (
site: t.Node,
effect: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticEffectTiming => {
if (effect !== undefined) {
if (
semanticDefiniteEffectPrecedesReference(site, effect, state.parentsByNode)
)
return "after";
if (
semanticDefiniteEffectPrecedesReference(effect, site, state.parentsByNode)
)
return "before";
}
return semanticEffectTiming(site, effect, state);
};
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),
),
};
};
/** Function ownership is independent of uncertain ordering within a loop. */
const bindingEnclosingFunction = (
read: t.Node,
parents: WeakMap<t.Node, t.Node>,
): t.Function | undefined => {
const cache = initializerPositionCache(parents).enclosingFunction;
const path: t.Node[] = [];
let current = read;
let result: t.Function | null = null;
while (true) {
const cached = cache.get(current);
if (cached !== undefined) {
result = cached;
break;
}
path.push(current);
const parent = parents.get(current);
if (parent === undefined || delayedInstanceFieldValue(parent, current))
break;
if (
t.isFunction(parent) &&
!semanticMethodDefinitionExpression(parent, current)
) {
if (current === parent.body) result = parent;
break;
}
current = parent;
}
for (const node of path) cache.set(node, result);
return result ?? undefined;
};
/** 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, repeatingLoop: parent };
break;
}
if (
parent === undefined ||
(t.isFunction(parent) &&
!semanticMethodDefinitionExpression(parent, current)) ||
delayedInstanceFieldValue(parent, current)
) {
result = { position: null, repeatingLoop: undefined };
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;
};
/** Repeated member reads share unknown ordering, while retaining their source sites. */
export const semanticCaptureOrderingIdentity = (
capture: t.Node,
parents: WeakMap<t.Node, t.Node>,
): t.Node => {
// Calls can have a distinct invocation effect at their own selection site.
if (!t.isMemberExpression(capture) && !t.isOptionalMemberExpression(capture))
return capture;
const loop = bindingReadPosition(capture, parents).repeatingLoop;
if (loop === undefined) return capture;
let child: t.Node = capture;
for (
let parent = parents.get(child);
parent !== undefined && parent !== loop;
parent = parents.get(child)
)
child = parent;
// These inputs run once, before the repeating portion of the loop.
if (
(t.isForStatement(loop) && child === loop.init) ||
((t.isForInStatement(loop) || t.isForOfStatement(loop)) &&
child === loop.right)
)
return capture;
return loop;
};
/** 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;
};
/** A hoisted function needs a module-level use after initialization, not a later declaration. */
const moduleFunctionFirstRead = (
declaration: t.FunctionDeclaration,
body: readonly t.Statement[],
parents: WeakMap<t.Node, t.Node>,
state?: JavaScriptSemanticAnalysisState,
): number | null => {
const cache = initializerPositionCache(parents).moduleFunctionReads;
let retained = cache.get(body);
const memberEffectsComplete = state?.memberMutationFactsComplete === true;
if (retained?.memberEffectsComplete !== memberEffectsComplete)
retained = undefined;
if (retained === undefined) {
const reads = new WeakMap<t.FunctionDeclaration, number | null>();
retained = {
reads,
memberEffectsComplete,
opaquePositions: new WeakMap(),
opaqueSitesIndexed: 0,
firstOpaqueRead: Infinity,
};
cache.set(body, retained);
const functions = new Map<string, t.FunctionDeclaration | null>();
for (const statement of body) {
const exported =
t.isExportNamedDeclaration(statement) ||
t.isExportDefaultDeclaration(statement);
if (
t.isExportNamedDeclaration(statement) &&
isTypeOnlyExportDeclaration(statement)
)
continue;
const node = exported ? statement.declaration : statement;
if (!t.isFunctionDeclaration(node)) continue;
reads.set(node, exported ? null : Infinity);
if (node.id == null) continue;
const name = node.id.name;
if (functions.has(name)) {
const previous = functions.get(name);
if (previous != null) reads.set(previous, null);
reads.set(node, null);
functions.set(name, null);
} else functions.set(name, node);
}
const indexedReads = reads;
const firstStatement = body[0];
const program =
firstStatement === undefined ? undefined : parents.get(firstStatement);
const classicScript =
t.isProgram(program) && javaScriptRuntimeContext(program) === "unknown";
const globalReceivers = new WeakMap<t.Node, boolean | null>();
const propertyReaders = new WeakMap<
t.Node,
ReturnType<typeof staticObjectPropertyReader>
>();
const ownMethodReaders = new WeakMap<t.Node, (name: string) => boolean>();
const ownMethodAtSource = (node: t.Node, name: string): boolean => {
let read = ownMethodReaders.get(node);
if (read === undefined) {
read = staticObjectOwnMethodReader(node);
ownMethodReaders.set(node, read);
}
return read(name);
};
const receiverPrototypeOwners = new WeakMap<
t.Node,
ReadonlySet<SemanticPrototypeOwner>
>();
const bindingReceivers = new WeakMap<
JavaScriptSemanticBindingState,
t.Node
>();
const globalReceiver = (input: t.Node): boolean | null => {
const pending: { node: t.Node; finish: boolean }[] = [
{ node: input, finish: false },
];
const visiting = new Set<t.Node>();
const origins = new WeakMap<
t.Node,
{
dependencies: t.Node[];
unknown: boolean;
global: boolean;
owners: Set<SemanticPrototypeOwner>;
}
>();
while (pending.length > 0) {
const frame = pending.pop();
if (frame === undefined) continue;
const current = frame.node;
if (frame.finish) {
const fact = origins.get(current);
if (fact === undefined) continue;
let global = fact.global;
let unknown = fact.unknown;
for (const dependency of fact.dependencies) {
for (const owner of receiverPrototypeOwners.get(dependency) ?? [])
fact.owners.add(owner);
const answer = globalReceivers.get(dependency);
if (answer === true) global = true;
if (answer === null || answer === undefined) unknown = true;
}
receiverPrototypeOwners.set(current, fact.owners);
globalReceivers.set(current, global ? true : unknown ? null : false);
visiting.delete(current);
continue;
}
if (globalReceivers.has(current)) continue;
if (visiting.has(current)) {
// A back edge has no finite identity proof; retain uncertainty.
globalReceivers.set(current, null);
continue;
}
visiting.add(current);
const fact = {
dependencies: [] as t.Node[],
unknown: false,
global: false,
owners: new Set<SemanticPrototypeOwner>(),
};
const node = unwrapJavaScriptExpression(current).node;
if (t.isArrayExpression(node)) fact.owners.add("Array");
else if (t.isFunction(node)) fact.owners.add("Function");
else if (t.isObjectExpression(node)) fact.owners.add("Object");
else if (t.isStringLiteral(node) || t.isTemplateLiteral(node))
fact.owners.add("String");
else if (t.isNumericLiteral(node)) fact.owners.add("Number");
else if (t.isBooleanLiteral(node)) fact.owners.add("Boolean");
else if (t.isRegExpLiteral(node)) fact.owners.add("RegExp");
else if (t.isBigIntLiteral(node)) fact.owners.add("BigInt");
if (node !== current) fact.dependencies.push(node);
else if (t.isThisExpression(node)) {
const owner = t.isProgram(program)
? javaScriptThisOwner(node, program, parents)
: "caller";
fact.global = owner === "global";
fact.unknown = owner === "caller";
} else if (t.isIdentifier(node)) {
const binding =
state === undefined
? undefined
: resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) {
if (["globalThis", "window", "self"].includes(node.name))
fact.global = true;
else fact.unknown = true;
} else {
const representative = bindingReceivers.get(binding);
if (representative !== undefined && representative !== current) {
fact.dependencies.push(representative);
} else {
bindingReceivers.set(binding, current);
if (
binding.initializers.length === 0 ||
binding.referenceInitializers.length > 0
)
fact.unknown = true;
for (const initializer of binding.initializers) {
if (initializer.projection.length > 0) fact.unknown = true;
else fact.dependencies.push(initializer.node);
}
}
}
} else if (t.isObjectExpression(node)) {
fact.unknown = node.properties.some(
(property) =>
t.isSpreadElement(property) ||
(t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
"__proto__"),
);
} else if (
!t.isArrayExpression(node) &&
!t.isLiteral(node) &&
!t.isFunction(node)
)
fact.unknown = true;
origins.set(current, fact);
pending.push({ node: current, finish: true });
for (const dependency of fact.dependencies)
pending.push({ node: dependency, finish: false });
}
return globalReceivers.get(input) ?? null;
};
interface ReceiverBound {
index: number;
node?: t.Node;
ambiguous: boolean;
}
const receiverBounds = new WeakMap<
JavaScriptSemanticBindingState,
ReceiverBound | null
>();
const receiverBound = (
root: JavaScriptSemanticBindingState,
): ReceiverBound | null => {
const pending = [{ binding: root, finish: false }];
const activeBindings = new Set<JavaScriptSemanticBindingState>();
const dependencies = new WeakMap<
JavaScriptSemanticBindingState,
JavaScriptSemanticBindingState[]
>();
const merge = (
left: ReceiverBound,
right: ReceiverBound,
): ReceiverBound => {
if (left.index < right.index) return left;
if (right.index < left.index) return right;
return {
...left,
ambiguous:
left.ambiguous || right.ambiguous || left.node !== right.node,
};
};
while (pending.length > 0) {
const frame = pending.pop()!;
const binding = frame.binding;
if (frame.finish) {
let bound: ReceiverBound | null = {
index: Infinity,
ambiguous: false,
};
for (const effects of [binding.memberWrites, binding.escapedPaths]) {
for (const effect of effects) {
const position =
effect.node === undefined
? null
: directMutationPosition(effect.node, parents);
if (
effect.node === undefined ||
position === null ||
position.body !== body
) {
bound = null;
break;
}
bound = merge(bound, {
index: position.index,
node: effect.node,
ambiguous: false,
});
}
if (bound === null) break;
}
for (const dependency of dependencies.get(binding) ?? []) {
const inherited = receiverBounds.get(dependency);
if (
bound === null ||
inherited === null ||
inherited === undefined
) {
bound = null;
break;
}
bound = merge(bound, inherited);
}
receiverBounds.set(binding, bound);
activeBindings.delete(binding);
continue;
}
if (receiverBounds.has(binding)) continue;
if (activeBindings.has(binding)) {
receiverBounds.set(binding, null);
continue;
}
activeBindings.add(binding);
const selected: JavaScriptSemanticBindingState[] = [];
for (const initializer of binding.initializers) {
const node = unwrapJavaScriptExpression(initializer.node).node;
if (!t.isIdentifier(node) || state === undefined) continue;
const dependency = resolveSemanticBindingState(
state,
node,
node.name,
);
if (dependency !== undefined) selected.push(dependency);
}
dependencies.set(binding, selected);
pending.push({ binding, finish: true });
for (const dependency of selected)
pending.push({ binding: dependency, finish: false });
}
return receiverBounds.get(root) ?? null;
};
// A single owner-specific bound serves every literal read in this body.
const prototypeBounds = new Map<
SemanticPrototypeOwner,
ReceiverBound | null
>();
for (const owner of semanticPrototypeOwners) {
let bound: ReceiverBound | null = { index: Infinity, ambiguous: false };
for (const effect of state?.prototypeMutationSites ?? []) {
if (effect.owner !== owner && effect.owner !== "Object") continue;
const position = directMutationPosition(effect.node, parents);
if (position === null || position.body !== body) {
bound = null;
break;
}
if (position.index < bound.index)
bound = {
index: position.index,
node: effect.node,
ambiguous: false,
};
else if (position.index === bound.index && effect.node !== bound.node)
bound = { ...bound, ambiguous: true };
}
prototypeBounds.set(owner, bound);
}
const receiverAtRead = (input: t.Node, read: t.Node): boolean | null => {
// Allocation provenance follows the same initializer DAG as receiver identity.
const identity = globalReceiver(input);
for (const owner of receiverPrototypeOwners.get(input) ?? []) {
const bound = prototypeBounds.get(owner);
if (bound === null) return null;
if (bound !== undefined && bound.index !== Infinity) {
const position = bindingReadPosition(read, parents);
if (
position.position === null ||
position.repeatingLoop ||
position.position.body !== body
)
return null;
if (position.position.index > bound.index) return null;
if (
position.position.index === bound.index &&
(bound.ambiguous ||
bound.node === undefined ||
state === undefined ||
semanticEffectTiming(read, bound.node, state) !== "after")
)
return null;
}
}
if (identity !== false || state === undefined || !memberEffectsComplete)
return identity;
const root = unwrapJavaScriptExpression(input).node;
if (!t.isIdentifier(root)) return identity;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return null;
const bound = receiverBound(binding);
if (bound === null) return null;
if (bound.index === Infinity) return identity;
const position = bindingReadPosition(read, parents);
if (
position.position === null ||
position.repeatingLoop ||
position.position.body !== body
)
return null;
if (position.position.index < bound.index) return identity;
if (position.position.index > bound.index || bound.ambiguous) return null;
return bound.node !== undefined &&
(semanticReadPrecedesEffectWithinActivation(
read,
bound.node,
parents,
) ||
semanticEffectTiming(read, bound.node, state) === "after")
? identity
: null;
};
const active = new Set<t.Function>();
const retainNamedRead = (
name: string,
parent: t.Node | null,
index: number,
): void => {
const fn = functions.get(name);
if (fn == null || active.has(fn)) return;
const current = indexedReads.get(fn);
if (current === null || current === undefined) return;
// Indirect activations and live exports have no proven module timing.
indexedReads.set(
fn,
active.size > 0 || t.isExportSpecifier(parent)
? null
: Math.min(current, index),
);
};
const opaquePositions = retained.opaquePositions;
const definitionKeys = new WeakMap<t.Node, t.Function>();
let unknownPropertyRead: number | null = Infinity;
for (let index = 0; index < body.length; index++) {
const statement = body[index];
if (statement === undefined) continue;
traverseJavaScriptAst(statement, {
skip: (node) =>
state?.erasedSyntaxNodes.has(node) === true ||
isErasedJavaScriptValueChild(node, parents.get(node) ?? null),
enter(node, parent) {
if (
semanticMethodDefinitionExpression(parent ?? undefined, node) &&
parent !== null &&
t.isFunction(parent)
) {
active.delete(parent);
definitionKeys.set(node, parent);
}
if (t.isFunction(node)) active.add(node);
// Direct eval sees lexical module/CommonJS bindings too. Its
// scope-owned sites are complete before ambient indexing starts.
if (
t.isCallExpression(node) &&
state?.opaqueEffects.sites.has(node)
) {
if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0 ? null : Math.min(unknownPropertyRead, index);
}
if (
classicScript &&
(t.isCallExpression(node) ||
t.isOptionalCallExpression(node) ||
t.isNewExpression(node) ||
t.isTaggedTemplateExpression(node))
) {
opaquePositions.set(node, active.size > 0 ? null : index);
}
if (classicScript) {
for (const source of semanticPropertyReadSources(
node,
parent,
state,
)) {
const projected =
source.iterationSources !== undefined ||
source.projection.length > 1;
if (!projected && receiverAtRead(source.node, node) === false)
continue;
// Select the receiver through the shared property-reference owner.
// Unknown/iterated sources retain possible activation.
if (projected && source.iterationSources === undefined) {
let receiver: t.Node | undefined = source.node;
for (const key of source.projection.slice(0, -1)) {
if (typeof key !== "string" && typeof key !== "number") {
receiver = undefined;
break;
}
if (receiver === undefined) break;
let read = propertyReaders.get(receiver);
if (read === undefined) {
read = staticObjectPropertyReader(receiver);
propertyReaders.set(receiver, read);
}
const selected = read(String(key));
receiver =
selected.status === "explicit" ? selected.value : undefined;
if (receiver === undefined) break;
}
if (
receiver !== undefined &&
receiverAtRead(receiver, node) === false
)
continue;
}
const key = source.projection[0];
if (!projected && typeof key === "string")
retainNamedRead(key, parent, index);
else if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0 ? null : Math.min(unknownPropertyRead, index);
}
}
let name: string | null = null;
if (
t.isIdentifier(node) &&
parent !== null &&
(t.isReferenced(node, parent) ||
(t.isAssignmentExpression(parent) &&
parent.left === node &&
semanticWriteSiteReadsFirst(parent)))
) {
const candidate = functions.get(node.name);
const binding =
state === undefined
? undefined
: resolveSemanticBindingState(state, node, node.name);
if (
candidate != null &&
state !== undefined &&
semanticResolutionBlocked(state, node, node.name)
) {
const current = indexedReads.get(candidate);
if (current !== null && current !== undefined)
indexedReads.set(
candidate,
active.size > 0 ? null : Math.min(current, index),
);
return;
}
if (
state === undefined ||
(candidate != null &&
binding?.initializers.some(
({ node: value }) => value === candidate,
))
)
name = node.name;
} else if (
classicScript &&
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
) {
const receiver = unwrapJavaScriptExpression(node.object).node;
const selected = semanticStaticPropertyKey(
node.property,
node.computed,
);
const ownMethod =
selected !== null &&
functions.has(selected) &&
(ownMethodAtSource(receiver, selected) ||
(state !== undefined &&
t.isIdentifier(receiver) &&
(() => {
const binding = resolveSemanticBindingState(
state,
receiver,
receiver.name,
);
if (
binding === undefined ||
receiverBound(binding)?.index !== Infinity
)
return false;
return (
binding.initializers.length === 1 &&
binding.initializers[0]!.projection.length === 0 &&
ownMethodAtSource(
unwrapJavaScriptExpression(
binding.initializers[0]!.node,
).node,
selected,
)
);
})()));
// Classic scripts expose hoisted functions through the global object.
// Property names are not referenced identifiers; keep their read timing too.
if (!ownMethod && receiverAtRead(receiver, node) !== false) {
name = selected;
if (name === null) {
if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0
? null
: Math.min(unknownPropertyRead, index);
return;
}
}
}
if (name !== null) retainNamedRead(name, parent, index);
},
exit(node) {
if (t.isFunction(node)) active.delete(node);
const method = definitionKeys.get(node);
if (method !== undefined) active.add(method);
},
});
}
// Unknown global keys share one timing fact across the module functions.
for (const fn of functions.values()) {
if (fn == null) continue;
const current = indexedReads.get(fn);
if (current !== null && current !== undefined)
indexedReads.set(
fn,
unknownPropertyRead === null
? null
: Math.min(current, unknownPropertyRead),
);
}
}
// Callee resolution can query timing during the ambient walk. Source reads
// stay fixed; consume each newly observed opaque call once without rescanning.
const sites = state?.opaqueEffects.globalSites ?? [];
while (retained.opaqueSitesIndexed < sites.length) {
const site = sites[retained.opaqueSitesIndexed++];
const position =
site === undefined ? undefined : retained.opaquePositions.get(site);
if (position === undefined || retained.firstOpaqueRead === null) continue;
retained.firstOpaqueRead =
position === null ? null : Math.min(retained.firstOpaqueRead, position);
}
const read = retained.reads.get(declaration);
const first =
read === undefined || read === null || retained.firstOpaqueRead === null
? null
: Math.min(read, retained.firstOpaqueRead);
return first !== undefined && Number.isFinite(first) ? first : null;
};
export const semanticBindingAtRead = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
context: WeakMap<t.Node, t.Node> | JavaScriptSemanticAnalysisState,
): SemanticBindingRead => {
const state = "parentsByNode" in context ? context : undefined;
const parents =
state === undefined
? (context as WeakMap<t.Node, t.Node>)
: state.parentsByNode;
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, repeatingLoop } = 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 (
!repeatingLoop &&
origin !== undefined &&
binding.initializers.length + binding.referenceInitializers.length === 1 &&
sourcePosition !== null &&
position !== null &&
positionPrecedes(sourcePosition, position)
)
return {
status: "ordered",
binding,
hasDefiniteInitializer,
};
const moduleConstBinding =
binding.scopeId.startsWith("scope:program:") &&
binding.declarationKinds.size === 1 &&
binding.declarationKinds.has("const") &&
!binding.mutable;
const enclosingFunction = moduleConstBinding
? bindingEnclosingFunction(read, parents)
: undefined;
let functionPosition: StatementPosition | null = null;
if (enclosingFunction !== undefined) {
const positions = initializerPositionCache(parents).functionPosition;
const cached = positions.get(enclosingFunction);
if (cached !== undefined) functionPosition = cached;
else {
const compute = (): StatementPosition | null => {
if (t.isStatement(enclosingFunction))
return statementPosition(enclosingFunction, parents);
for (
let owner = parents.get(enclosingFunction);
owner !== undefined;
owner = parents.get(owner)
) {
if (t.isFunction(owner)) return null;
if (t.isStatement(owner)) return statementPosition(owner, parents);
}
return null;
};
functionPosition = compute();
positions.set(enclosingFunction, functionPosition);
}
}
const firstFunctionRead =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
t.isFunctionDeclaration(enclosingFunction)
? moduleFunctionFirstRead(
enclosingFunction,
sourcePosition.body,
parents,
state,
)
: null;
const moduleConstInitializedBeforeActivation =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
(t.isFunctionDeclaration(enclosingFunction)
? firstFunctionRead !== null && sourcePosition.index < firstFunctionRead
: positionPrecedes(sourcePosition, functionPosition));
const uncertainOrder =
repeatingLoop ||
sourcePosition === null ||
position === null ||
sourcePosition.body !== position.body;
if (uncertainOrder && moduleConstInitializedBeforeActivation)
// Initialization coverage does not order a capture in another activation.
return { status: "unresolved", binding, hasDefiniteInitializer: true };
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 an initializer or completed return-expression capture in its body. */
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.isReturnStatement(parent) && parent.argument === current)
return statementPosition(parent, parents);
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)
);
};
/** A local declaration resets its lexical value before an ordinary member read. */
export const semanticBindingEffectFollowsRead = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
effect: t.Node,
parents: WeakMap<t.Node, t.Node>,
): boolean => {
const initializer = binding.initializers[0];
if (binding.initializers.length !== 1 || initializer === undefined)
return false;
const declaration = parents.get(initializer.node);
// A logical/compound assignment may preserve a value from a prior call.
if (
!t.isVariableDeclarator(declaration) ||
declaration.init !== initializer.node
)
return false;
const initialized = initializerPosition(initializer, parents);
const selected = bindingReadPosition(read, parents).position;
return (
initialized !== null &&
selected !== null &&
positionPrecedes(initialized, selected) &&
semanticReadPrecedesEffectWithinActivation(read, effect, parents)
);
};
/** 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)
);
};
/** Explicit wrapper facts and program `var` redeclarations retain their lexical identity. */
export const commonJsWrapperBindingDeclaration = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
if (
!["module", "exports", "require", "__dirname", "__filename"].includes(
name,
) ||
!t.isIdentifier(node, { name })
)
return undefined;
const binding = resolveSemanticBindingState(state, node, name);
if (binding === undefined) return undefined;
const scope = state.scopesById.get(binding.scopeId);
if (
scope?.kind !== "program" ||
![...binding.declarationKinds].every((kind) => kind === "var")
)
return undefined;
// The index owns explicit wrapper parameters, including ones with no source
// redeclaration. Babel's module classification does not invalidate that fact.
if (binding.hostOrigin === "commonjs-wrapper") return binding;
// A program var shares the wrapper binding only when the host is known.
return javaScriptRuntimeContext(state.program) === "commonjs" &&
binding.declarationKinds.size > 0
? binding
: undefined;
};
/** Retained value and pattern stores share one source-ordered write boundary. */
export const semanticBindingHasPriorValueWrite = (
site: t.Node,
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (binding.definitions.some(({ kind }) => kind === "function")) return true;
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
const read = semanticBindingAtRead(binding, site, state);
const retained =
read.status === "ordered"
? new Set([
...read.binding.initializers,
...read.binding.referenceInitializers,
])
: undefined;
const representedWrites = new Set<t.Node>();
const retainedWrites = new Set<t.Node>();
for (const source of [
...binding.initializers,
...binding.referenceInitializers,
]) {
let owner: t.Node | undefined = source.node;
while (owner !== undefined && !writes.includes(owner))
owner = state.parentsByNode.get(owner);
if (owner !== undefined) {
representedWrites.add(owner);
if (retained === undefined || retained.has(source))
retainedWrites.add(owner);
} else if (
(retained === undefined || retained.has(source)) &&
semanticEffectTiming(site, source.node, state) !== "after"
)
return true;
}
return writes.some(
(write) =>
(!representedWrites.has(write) || retainedWrites.has(write)) &&
semanticEffectTiming(site, write, state) !== "after",
);
};
/** A program-level `var` redeclaration still denotes its untouched CJS wrapper parameter. */
export const isUnmodifiedCommonJsWrapperBinding = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const binding = commonJsWrapperBindingDeclaration(node, name, state);
return (
binding !== undefined &&
!semanticBindingHasPriorValueWrite(node, binding, state)
);
};
/** Identify a global name only when no lexical or dynamic environment shadows it. */
export const isUnshadowedGlobal = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean =>
t.isIdentifier(node, { name }) &&
(resolveSemanticBindingState(state, node, name) === undefined
? !semanticResolutionBlocked(state, node, name)
: isUnmodifiedCommonJsWrapperBinding(node, name, state));