mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
fix(javascript): preserve incoming callback target uncertainty (#4243)
Preserve incoming callback uncertainty and resolve proven preceding writes. Share read-position and initializer identity facts with returned-reference analysis; cache completed projected work with actual cycle dependencies. Fixes #3880
This commit is contained in:
1 parent
1bd5c5c87e
commit
42fc6c53ae
8 files changed
+822
-118
No files matched your search
@@ -4,11 +4,8 @@ import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveLocalCallables,
|
||||
type LocalCallableResolution,
|
||||
} from "./javascriptSemanticCallResolution.js";
|
||||
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
semanticArrayIndex,
|
||||
type JavaScriptSemanticPropertyPath,
|
||||
@@ -16,6 +13,7 @@ import {
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticInitializer,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
/** A reference potentially handed to a caller or generator consumer. */
|
||||
@@ -27,9 +25,73 @@ export interface SemanticReturnedReference {
|
||||
interface Candidate {
|
||||
readonly node: t.Node;
|
||||
readonly path: readonly (string | number | null)[];
|
||||
readonly bindings: ReadonlySet<string>;
|
||||
readonly initializers: ReadonlySet<JavaScriptSemanticInitializer>;
|
||||
}
|
||||
|
||||
type InitializerContext = ReadonlySet<JavaScriptSemanticInitializer>;
|
||||
type CompletedExpansions = WeakMap<
|
||||
t.Node,
|
||||
Map<string, readonly InitializerContext[]>
|
||||
>;
|
||||
|
||||
interface ExpansionFrame {
|
||||
readonly item: Candidate;
|
||||
readonly node: t.Node;
|
||||
readonly pathKey: string;
|
||||
readonly parent: ExpansionFrame | null;
|
||||
readonly blockers: Set<JavaScriptSemanticInitializer>;
|
||||
}
|
||||
|
||||
type ExpansionTask =
|
||||
| {
|
||||
readonly kind: "visit";
|
||||
readonly item: Candidate;
|
||||
readonly parent: ExpansionFrame | null;
|
||||
}
|
||||
| { readonly kind: "complete"; readonly frame: ExpansionFrame };
|
||||
|
||||
const contextContains = (
|
||||
context: InitializerContext,
|
||||
required: InitializerContext,
|
||||
): boolean => {
|
||||
for (const initializer of required)
|
||||
if (!context.has(initializer)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const propagateBlockers = (
|
||||
blockers: InitializerContext,
|
||||
parent: ExpansionFrame | null,
|
||||
): void => {
|
||||
if (parent === null) return;
|
||||
for (const initializer of blockers)
|
||||
if (parent.item.initializers.has(initializer))
|
||||
parent.blockers.add(initializer);
|
||||
};
|
||||
|
||||
const completeExpansion = (
|
||||
frame: ExpansionFrame,
|
||||
expanded: CompletedExpansions,
|
||||
): void => {
|
||||
const paths =
|
||||
expanded.get(frame.node) ??
|
||||
new Map<string, readonly InitializerContext[]>();
|
||||
const contexts = paths.get(frame.pathKey) ?? [];
|
||||
const covered = contexts.find((context) =>
|
||||
contextContains(frame.blockers, context),
|
||||
);
|
||||
if (covered === undefined) {
|
||||
paths.set(frame.pathKey, [
|
||||
...contexts.filter(
|
||||
(context) => !contextContains(context, frame.blockers),
|
||||
),
|
||||
frame.blockers,
|
||||
]);
|
||||
expanded.set(frame.node, paths);
|
||||
}
|
||||
propagateBlockers(covered ?? frame.blockers, frame.parent);
|
||||
};
|
||||
|
||||
interface ObjectTargetIndex {
|
||||
readonly propertyByName: ReadonlyMap<
|
||||
string,
|
||||
@@ -50,73 +112,73 @@ interface ObjectTargetUnknown {
|
||||
export const createSemanticReturnedReferences = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): ((callee: t.Node) => readonly SemanticReturnedReference[]) => {
|
||||
const callableById = new Map(
|
||||
state.callables.map((callable) => [callable.callableId, callable]),
|
||||
);
|
||||
const bindingCache = new Map<string, LocalCallableResolution>();
|
||||
const results = new WeakMap<t.Node, readonly SemanticReturnedReference[]>();
|
||||
const objectTargets = new WeakMap<t.ObjectExpression, ObjectTargetIndex>();
|
||||
return (callee) => {
|
||||
const existing = results.get(callee);
|
||||
if (existing !== undefined) return existing;
|
||||
const functions = new Set<t.Node>();
|
||||
const resolution = resolveLocalCallables({
|
||||
node: callee,
|
||||
state,
|
||||
callableById,
|
||||
bindingCache,
|
||||
seenBindings: new Set(),
|
||||
});
|
||||
for (const id of resolution.callableIds) {
|
||||
const node = state.callableNodesById.get(id);
|
||||
if (node !== undefined) functions.add(node);
|
||||
}
|
||||
const pending: Candidate[] = [
|
||||
{ node: callee, path: [], bindings: new Set() },
|
||||
const work: ExpansionTask[] = [
|
||||
{
|
||||
kind: "visit",
|
||||
item: { node: callee, path: [], initializers: new Set() },
|
||||
parent: null,
|
||||
},
|
||||
];
|
||||
// A shared initializer can be reached through both arms of every alias
|
||||
// diamond. Expand an expression/path again only when its cycle context
|
||||
// permits a binding that an earlier traversal blocked.
|
||||
const expanded = new WeakMap<
|
||||
t.Node,
|
||||
Map<string, readonly ReadonlySet<string>[]>
|
||||
>();
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const item = pending[cursor];
|
||||
if (item === undefined) break;
|
||||
// Finish descendants before memoizing an expansion. Only actual external
|
||||
// cycle blockers affect reuse; branch history is not a dependency. An
|
||||
// acyclic expression/path is consequently expanded once across a diamond.
|
||||
const expanded: CompletedExpansions = new WeakMap();
|
||||
while (work.length > 0) {
|
||||
const task = work.pop();
|
||||
if (task === undefined) break;
|
||||
if (task.kind === "complete") {
|
||||
completeExpansion(task.frame, expanded);
|
||||
continue;
|
||||
}
|
||||
const { item, parent } = task;
|
||||
const node = unwrapJavaScriptExpression(item.node).node;
|
||||
const pathKey = JSON.stringify(item.path);
|
||||
const paths =
|
||||
expanded.get(node) ?? new Map<string, readonly ReadonlySet<string>[]>();
|
||||
const contexts = paths.get(pathKey) ?? [];
|
||||
if (
|
||||
contexts.some((context) =>
|
||||
[...context].every((binding) => item.bindings.has(binding)),
|
||||
)
|
||||
)
|
||||
const covered = expanded
|
||||
.get(node)
|
||||
?.get(pathKey)
|
||||
?.find((context) => contextContains(item.initializers, context));
|
||||
if (covered !== undefined) {
|
||||
// Cached cycle dependencies must reach the caller's proof too.
|
||||
propagateBlockers(covered, parent);
|
||||
continue;
|
||||
paths.set(pathKey, [
|
||||
...contexts.filter(
|
||||
(context) =>
|
||||
![...item.bindings].every((binding) => context.has(binding)),
|
||||
),
|
||||
item.bindings,
|
||||
]);
|
||||
expanded.set(node, paths);
|
||||
}
|
||||
const frame: ExpansionFrame = {
|
||||
item,
|
||||
node,
|
||||
pathKey,
|
||||
parent,
|
||||
blockers: new Set(),
|
||||
};
|
||||
work.push({ kind: "complete", frame });
|
||||
const pending: Candidate[] = [];
|
||||
if (t.isIdentifier(node)) {
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (binding === undefined || item.bindings.has(binding.bindingId))
|
||||
continue;
|
||||
const bindings = new Set([...item.bindings, binding.bindingId]);
|
||||
if (binding === undefined) continue;
|
||||
const selected = semanticBindingAtRead(
|
||||
binding,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
).binding;
|
||||
for (const initializer of [
|
||||
...binding.initializers,
|
||||
...binding.referenceInitializers,
|
||||
])
|
||||
...selected.initializers,
|
||||
...selected.referenceInitializers,
|
||||
]) {
|
||||
if (item.initializers.has(initializer)) {
|
||||
frame.blockers.add(initializer);
|
||||
continue;
|
||||
}
|
||||
pending.push({
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection, ...item.path],
|
||||
bindings,
|
||||
initializers: new Set([...item.initializers, initializer]),
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node)
|
||||
@@ -203,6 +265,11 @@ export const createSemanticReturnedReferences = (
|
||||
if (node.superClass !== null && node.superClass !== undefined)
|
||||
pending.push({ ...item, node: node.superClass });
|
||||
}
|
||||
for (let index = pending.length - 1; index >= 0; index--) {
|
||||
const child = pending[index];
|
||||
if (child !== undefined)
|
||||
work.push({ kind: "visit", item: child, parent: frame });
|
||||
}
|
||||
}
|
||||
const output = [...functions].flatMap((node) => {
|
||||
const callables = t.isClass(node)
|
||||
|
||||
Reference in new issue
Block a user