mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): carry a directly returned value's literal to its return site
Return sites evaluated their returned expression but only projected identifier references as aliases. A function that returned a member read such as `settings.index` linked only the receiver `settings` as a candidate alias, so backward provenance from the call result lost the known literal and reported ambiguity. Project a non-identifier return's evaluated primitive onto its return site, as binding values already are, and do not offer the expression's inner references as candidates when that value is one exact literal. Fixes #5585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
080c0e2b2b
commit
ad3d995604
3 files changed
+100
-7
No files matched your search
@@ -94,6 +94,37 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"traces a directly returned member read back to its literal (candidates: %s)",
|
||||
(candidates) => {
|
||||
const graph = graphFor(`
|
||||
const settings = { index: "products" };
|
||||
function viaLocal() { const value = settings.index; return value; }
|
||||
function viaMember() { return settings.index; }
|
||||
const a = viaLocal();
|
||||
const b = viaMember();
|
||||
`);
|
||||
for (const label of ["a", "b"]) {
|
||||
const binding = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === label,
|
||||
);
|
||||
if (binding === undefined) throw new Error(`Expected binding ${label}`);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: binding.node_id },
|
||||
direction: "backward-provenance",
|
||||
include_ambiguous_dynamic_edges: candidates,
|
||||
});
|
||||
expect(trace.status, label).toBe("found");
|
||||
expect(
|
||||
trace.nodes.filter(({ kind }) => kind === "literal"),
|
||||
label,
|
||||
).toEqual([
|
||||
expect.objectContaining({ properties: { value: "products" } }),
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("projects closure and direct interprocedural flow without execution", () => {
|
||||
const graph = graphFor(`
|
||||
const outer = 2;
|
||||
|
||||
@@ -197,6 +197,14 @@ export const projectSemanticReturnValues = (
|
||||
for (const callable of context.ir.callables)
|
||||
for (const site of callable.returnSites) {
|
||||
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
|
||||
// A non-identifier return evaluated to one exact primitive is defined by
|
||||
// its literal (see projectSemanticValues); references inside it, such as
|
||||
// a member read's receiver, are not candidates for the returned value.
|
||||
if (
|
||||
site.identityReferenceLocation === null &&
|
||||
site.value.status === "literal"
|
||||
)
|
||||
continue;
|
||||
const references = context.referenceNodesWithin(site.location);
|
||||
for (const reference of references)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
|
||||
@@ -34,6 +34,34 @@ export const projectSemanticValues = (
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
// A return through an identifier already reaches its value through the
|
||||
// resolved binding alias. Any other returned expression, such as a member
|
||||
// read, carries its evaluated primitive on the return site itself.
|
||||
for (const callable of context.ir.callables)
|
||||
for (const site of callable.returnSites) {
|
||||
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
|
||||
if (returnNode === undefined || site.identityReferenceLocation !== null)
|
||||
continue;
|
||||
const primitives =
|
||||
site.value.status === "literal"
|
||||
? [site.value.value]
|
||||
: site.value.status === "union"
|
||||
? site.value.values
|
||||
: [];
|
||||
for (const primitive of primitives)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: addLiteralNode(context, {
|
||||
key: `return:${site.returnSiteId}`,
|
||||
location: site.location,
|
||||
functionNodeId: returnNode.function_node_id,
|
||||
value: primitive,
|
||||
}),
|
||||
target: returnNode,
|
||||
relation: "defines",
|
||||
resolution:
|
||||
site.value.status === "literal" ? "resolved" : "candidate",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interface ValueProjectionInput {
|
||||
@@ -50,7 +78,10 @@ const projectValue = (input: ValueProjectionInput): void => {
|
||||
path.length === 0 ? "binding" : `property:${semanticPropertyPointer(path)}`;
|
||||
if (target === null) return;
|
||||
if (value.status === "literal") {
|
||||
const literal = addLiteralNode(context, binding, value.value, role);
|
||||
const literal = addLiteralNode(
|
||||
context,
|
||||
bindingLiteral(context, binding, value.value, role),
|
||||
);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: literal,
|
||||
target,
|
||||
@@ -59,7 +90,10 @@ const projectValue = (input: ValueProjectionInput): void => {
|
||||
});
|
||||
} else if (value.status === "union")
|
||||
for (const primitive of value.values) {
|
||||
const literal = addLiteralNode(context, binding, primitive, role);
|
||||
const literal = addLiteralNode(
|
||||
context,
|
||||
bindingLiteral(context, binding, primitive, role),
|
||||
);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: literal,
|
||||
target,
|
||||
@@ -134,11 +168,32 @@ const projectValue = (input: ValueProjectionInput): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const addLiteralNode = (
|
||||
interface LiteralOwner {
|
||||
/** Stable owner and role, such as a binding slot or a return site. */
|
||||
readonly key: string;
|
||||
readonly location:
|
||||
| JavaScriptSemanticBinding["definitions"][number]["location"]
|
||||
| null;
|
||||
readonly functionNodeId: string | null;
|
||||
readonly value: string | number | boolean | null;
|
||||
}
|
||||
|
||||
const bindingLiteral = (
|
||||
context: SemanticFlowProjectionContext,
|
||||
binding: JavaScriptSemanticBinding,
|
||||
value: string | number | boolean | null,
|
||||
role: string,
|
||||
): LiteralOwner => ({
|
||||
key: `${binding.bindingId}:${role}`,
|
||||
location: binding.definitions[0]?.location ?? null,
|
||||
functionNodeId:
|
||||
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
|
||||
value,
|
||||
});
|
||||
|
||||
const addLiteralNode = (
|
||||
context: SemanticFlowProjectionContext,
|
||||
{ key, location, functionNodeId, value }: LiteralOwner,
|
||||
) => {
|
||||
const encodedValue = JSON.stringify(value);
|
||||
const valueDigest = `value-sha256:${digestCanonicalValue(value, "Semantic literal")}`;
|
||||
@@ -149,14 +204,13 @@ const addLiteralNode = (
|
||||
kind: "literal",
|
||||
// Identity needs an exact value commitment, not another payload copy.
|
||||
// Keep the complete literal in properties for queries and Evidence export.
|
||||
roleKey: `literal:${binding.bindingId}:${role}:${valueKey}`,
|
||||
location: binding.definitions[0]?.location ?? null,
|
||||
roleKey: `literal:${key}:${valueKey}`,
|
||||
location,
|
||||
label:
|
||||
typeof value === "string" && encodedValue.length > stringLabel.length
|
||||
? stringLabel
|
||||
: encodedValue,
|
||||
functionNodeId:
|
||||
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
|
||||
functionNodeId,
|
||||
properties: { value },
|
||||
});
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user