mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
b28c0337fc
Reduce retained semantic allocations and memoize lexical scope ancestry while preserving semantic identities. Refs #1065 and #1563; the complete-directory memory failure remains open.
246 lines
7.5 KiB
TypeScript
246 lines
7.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticObjectOperation,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
semanticCallableIdForNode,
|
|
semanticReadsBeforeWrite,
|
|
} from "./javascriptSemanticProjection.js";
|
|
import {
|
|
semanticStaticPropertyKey,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
type JavaScriptSemanticAnalysisState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
|
|
|
/** Recover bounded static property reads, writes, spreads, and destructuring. */
|
|
export const collectJavaScriptSemanticObjects = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
): JavaScriptSemanticObjectOperation[] => {
|
|
const output: JavaScriptSemanticObjectOperation[] = [];
|
|
const callableIds = new Set(callables.map(({ callableId }) => callableId));
|
|
const callableStack: string[] = [];
|
|
const context: ObjectCollectionContext = { state, output };
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, parent) => {
|
|
const callableId = semanticCallableIdForNode(node);
|
|
if (callableId !== null && callableIds.has(callableId))
|
|
callableStack.push(callableId);
|
|
const ownerCallableId = callableStack.at(-1) ?? null;
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
|
collectMember(node, parent, ownerCallableId, context);
|
|
else if (
|
|
t.isSpreadElement(node) &&
|
|
parent !== null &&
|
|
t.isObjectExpression(parent)
|
|
) {
|
|
const identity = expressionObjectIdentity(node.argument, state);
|
|
addObjectOperation(
|
|
{
|
|
node,
|
|
kind: "spread",
|
|
ownerCallableId,
|
|
objectBindingId: identity.bindingId,
|
|
targetBindingId: null,
|
|
propertyPath: identity.path,
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
} else if (
|
|
t.isVariableDeclarator(node) &&
|
|
(t.isObjectPattern(node.id) || t.isArrayPattern(node.id))
|
|
)
|
|
collectDestructuring(node, ownerCallableId, state, output);
|
|
},
|
|
exit: (node) => {
|
|
const callableId = semanticCallableIdForNode(node);
|
|
if (callableId !== null && callableStack.at(-1) === callableId)
|
|
callableStack.pop();
|
|
},
|
|
});
|
|
return output;
|
|
};
|
|
|
|
interface ObjectCollectionContext {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly output: JavaScriptSemanticObjectOperation[];
|
|
}
|
|
|
|
const collectMember = (
|
|
node: t.MemberExpression | t.OptionalMemberExpression,
|
|
parent: t.Node | null,
|
|
ownerCallableId: string | null,
|
|
context: ObjectCollectionContext,
|
|
): void => {
|
|
const { state, output } = context;
|
|
// A chain is one property access; retaining every prefix duplicates work and
|
|
// would make full paths quadratic in deeply nested producer expressions.
|
|
if (
|
|
(t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) &&
|
|
parent.object === node
|
|
)
|
|
return;
|
|
const identity = expressionObjectIdentity(node, state);
|
|
const write =
|
|
(t.isAssignmentExpression(parent) && parent.left === node) ||
|
|
(t.isUpdateExpression(parent) && parent.argument === node);
|
|
const readsBeforeWrite = write && semanticReadsBeforeWrite(node, parent);
|
|
if (readsBeforeWrite)
|
|
addObjectOperation(
|
|
{
|
|
node,
|
|
kind: "read",
|
|
ownerCallableId,
|
|
objectBindingId: identity.bindingId,
|
|
targetBindingId: null,
|
|
propertyPath: identity.path,
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
addObjectOperation(
|
|
{
|
|
node,
|
|
kind: write ? "write" : "read",
|
|
ownerCallableId,
|
|
objectBindingId: identity.bindingId,
|
|
targetBindingId: null,
|
|
propertyPath: identity.path,
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
};
|
|
|
|
const collectDestructuring = (
|
|
node: t.VariableDeclarator,
|
|
ownerCallableId: string | null,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: JavaScriptSemanticObjectOperation[],
|
|
): void => {
|
|
const source = expressionObjectIdentity(node.init, state);
|
|
const visit = (pattern: t.Node, path: readonly string[]): void => {
|
|
if (t.isObjectPattern(pattern)) {
|
|
for (const property of pattern.properties) {
|
|
if (!t.isObjectProperty(property)) continue;
|
|
const name = semanticStaticPropertyKey(property.key, property.computed);
|
|
if (name !== null) visit(property.value, [...path, name]);
|
|
}
|
|
return;
|
|
}
|
|
if (t.isArrayPattern(pattern)) {
|
|
pattern.elements.forEach((element, index) => {
|
|
if (element !== null && !t.isRestElement(element))
|
|
visit(element, [...path, String(index)]);
|
|
});
|
|
return;
|
|
}
|
|
const target = bindingIdentifier(pattern);
|
|
if (target === null) return;
|
|
addObjectOperation(
|
|
{
|
|
node: pattern,
|
|
kind: "destructure",
|
|
ownerCallableId,
|
|
objectBindingId: source.bindingId,
|
|
targetBindingId:
|
|
resolveSemanticBindingState(state, target, target.name)?.bindingId ??
|
|
null,
|
|
propertyPath: source.path === null ? null : [...source.path, ...path],
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
};
|
|
visit(node.id, []);
|
|
};
|
|
|
|
interface AddObjectOperationInput {
|
|
readonly node: t.Node;
|
|
readonly kind: JavaScriptSemanticObjectOperation["kind"];
|
|
readonly ownerCallableId: string | null;
|
|
readonly objectBindingId: string | null;
|
|
readonly targetBindingId: string | null;
|
|
readonly propertyPath: readonly string[] | null;
|
|
}
|
|
|
|
const addObjectOperation = (
|
|
input: AddObjectOperationInput,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: JavaScriptSemanticObjectOperation[],
|
|
): void => {
|
|
output.push({
|
|
// These high-volume IDs must not retain intermediate concatenations.
|
|
objectOperationId: [
|
|
"object",
|
|
input.kind,
|
|
String(input.node.start ?? -1),
|
|
String(input.node.end ?? -1),
|
|
].join(":"),
|
|
kind: input.kind,
|
|
location: range(input.node),
|
|
ownerCallableId: input.ownerCallableId,
|
|
objectBindingId: input.objectBindingId,
|
|
targetBindingId: input.targetBindingId,
|
|
propertyPath: input.propertyPath,
|
|
resolution:
|
|
input.objectBindingId === null ||
|
|
input.propertyPath === null ||
|
|
(input.kind === "destructure" && input.targetBindingId === null)
|
|
? "partial"
|
|
: "complete",
|
|
});
|
|
};
|
|
|
|
interface ObjectIdentity {
|
|
readonly bindingId: string | null;
|
|
readonly path: readonly string[] | null;
|
|
}
|
|
|
|
const expressionObjectIdentity = (
|
|
node: t.Node | null | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): ObjectIdentity => {
|
|
const path: string[] = [];
|
|
let current = node;
|
|
let staticPath = true;
|
|
while (current !== null && current !== undefined) {
|
|
current = unwrapJavaScriptExpression(current).node;
|
|
if (
|
|
t.isMemberExpression(current) ||
|
|
t.isOptionalMemberExpression(current)
|
|
) {
|
|
const name = semanticStaticPropertyKey(
|
|
current.property,
|
|
current.computed,
|
|
);
|
|
if (name === null) staticPath = false;
|
|
else path.push(name);
|
|
current = current.object;
|
|
} else break;
|
|
}
|
|
return {
|
|
bindingId: t.isIdentifier(current)
|
|
? (resolveSemanticBindingState(state, current, current.name)?.bindingId ??
|
|
null)
|
|
: null,
|
|
path: staticPath ? path.reverse() : null,
|
|
};
|
|
};
|
|
|
|
const bindingIdentifier = (node: t.Node): t.Identifier | null => {
|
|
if (t.isIdentifier(node)) return node;
|
|
if (t.isAssignmentPattern(node) && t.isIdentifier(node.left))
|
|
return node.left;
|
|
return null;
|
|
};
|