mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04: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:
@@ -6,14 +6,17 @@ import type {
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
unwrapJavaScriptExpression,
|
||||
runtimeFunctionParameters,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
type JavaScriptSemanticInitializer,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
@@ -22,13 +25,15 @@ export interface LocalCallableResolution {
|
||||
readonly callableIds: readonly string[];
|
||||
readonly complete: boolean;
|
||||
readonly reason: string;
|
||||
/** A cycle cut depends on the active alias path and cannot be memoized. */
|
||||
readonly cycleDependent?: true;
|
||||
}
|
||||
|
||||
interface CallResolutionContext {
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
readonly seenInitializers: ReadonlySet<JavaScriptSemanticInitializer>;
|
||||
readonly bindingCache: WeakMap<t.Identifier, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Inputs needed to resolve one local call target. */
|
||||
@@ -36,8 +41,8 @@ export interface ResolveLocalCallablesInput {
|
||||
readonly node: t.Node;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
readonly seenInitializers: ReadonlySet<JavaScriptSemanticInitializer>;
|
||||
readonly bindingCache: WeakMap<t.Identifier, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Resolve local callable candidates without following dynamic properties. */
|
||||
@@ -47,7 +52,7 @@ export const resolveLocalCallables = (
|
||||
resolveCallables(input.node, {
|
||||
state: input.state,
|
||||
callableById: input.callableById,
|
||||
seenBindings: input.seenBindings,
|
||||
seenInitializers: input.seenInitializers,
|
||||
bindingCache: input.bindingCache,
|
||||
});
|
||||
|
||||
@@ -84,14 +89,39 @@ const resolveIdentifier = (
|
||||
node: t.Identifier,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const cached = context.bindingCache.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
return binding === undefined
|
||||
? {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unresolved call target ${node.name}.`,
|
||||
}
|
||||
: resolveBindingCallables(binding, context);
|
||||
if (binding === undefined)
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unresolved call target ${node.name}.`,
|
||||
};
|
||||
const read = semanticBindingAtRead(
|
||||
binding,
|
||||
node,
|
||||
context.state.parentsByNode,
|
||||
);
|
||||
const candidates = resolveBindingCallables(read.binding, context);
|
||||
// Knowing every local candidate does not prove that any write replaced the
|
||||
// incoming value, or that a write in another activation precedes this read.
|
||||
const covered =
|
||||
read.hasDefiniteInitializer ||
|
||||
semanticStableInitializer(binding, context.state) !== undefined;
|
||||
const resolution =
|
||||
candidates.complete && !covered
|
||||
? {
|
||||
...candidates,
|
||||
complete: false,
|
||||
reason: `Call target ${binding.name} may retain an incoming value or an unordered assignment.`,
|
||||
}
|
||||
: candidates;
|
||||
// Key by the immutable read, not binding identity: before/after snapshots
|
||||
// differ. Cache cycle-independent partial results too, keeping alias
|
||||
// diamonds linear even when their shared leaf has an unknown incoming value.
|
||||
if (!resolution.cycleDependent) context.bindingCache.set(node, resolution);
|
||||
return resolution;
|
||||
};
|
||||
|
||||
const resolveAlternatives = (
|
||||
@@ -108,6 +138,9 @@ const resolveAlternatives = (
|
||||
),
|
||||
complete: candidates.every(({ complete }) => complete),
|
||||
reason: "Conditional or logical call target has multiple candidates.",
|
||||
...(candidates.some((candidate) => candidate.cycleDependent)
|
||||
? { cycleDependent: true as const }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -115,28 +148,31 @@ const resolveBindingCallables = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const cached = context.bindingCache.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached;
|
||||
if (context.seenBindings.has(binding.bindingId))
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Call target alias cycle at ${binding.name}.`,
|
||||
};
|
||||
if (binding.initializers.length === 0)
|
||||
return missingBindingInitializer(binding);
|
||||
const candidateContext = {
|
||||
...context,
|
||||
seenBindings: new Set([...context.seenBindings, binding.bindingId]),
|
||||
};
|
||||
const candidates = binding.initializers.map(({ node, projection }) =>
|
||||
projection.length === 0
|
||||
? resolveCallables(node, candidateContext)
|
||||
: {
|
||||
const candidates = binding.initializers.map(
|
||||
(initializer): LocalCallableResolution => {
|
||||
if (context.seenInitializers.has(initializer))
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Projected call target ${binding.name} is unresolved.`,
|
||||
},
|
||||
cycleDependent: true,
|
||||
reason: `Call target initializer cycle at ${binding.name}.`,
|
||||
};
|
||||
return initializer.projection.length === 0
|
||||
? resolveCallables(initializer.node, {
|
||||
...context,
|
||||
seenInitializers: new Set([
|
||||
...context.seenInitializers,
|
||||
initializer,
|
||||
]),
|
||||
})
|
||||
: {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Projected call target ${binding.name} is unresolved.`,
|
||||
};
|
||||
},
|
||||
);
|
||||
const callableIds = uniqueCallableIds(
|
||||
candidates.flatMap((candidate) => candidate.callableIds),
|
||||
@@ -148,9 +184,10 @@ const resolveBindingCallables = (
|
||||
callableIds.length === 1 && candidates.every(({ complete }) => complete)
|
||||
? "Unique local callable target."
|
||||
: `Call target ${binding.name} has ambiguous or unsupported assignments.`,
|
||||
...(candidates.some((candidate) => candidate.cycleDependent)
|
||||
? { cycleDependent: true as const }
|
||||
: {}),
|
||||
};
|
||||
if (resolution.complete)
|
||||
context.bindingCache.set(binding.bindingId, resolution);
|
||||
return resolution;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user