fix(javascript): evaluate each short-circuit left operand once (#7126)

Logical branch selection evaluated the left operand to decide which
branches stay reachable, and value evaluation then evaluated the selected
left branch again. In a left-nested `a || b || c ...` chain every level
repeated that, so semantic analysis doubled per operand and a 24-operand
chain exhausted the per-source deadline.

The selection now carries the left operand's value with the branch, and
value evaluation reuses it. Provenance still walks the branch nodes.
Results are unchanged.

Fixes #7115

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-25 05:25:54 -07:00
1 parent 7336a566ac
commit 81afb043f7
3 files changed
+178 -12

No files matched your search

@@ -721,8 +721,10 @@ const evaluateExpressionValue = (
if (t.isTemplateLiteral(node)) return evaluateTemplate(node, context);
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))
return mergeValues(
semanticSelectedBranches(node, context).map((branch) =>
evaluateExpression(branch, nestedContext(context)),
semanticSelectedBranches(node, context).map(
(branch) =>
branch.value ??
evaluateExpression(branch.node, nestedContext(context)),
),
);
if (t.isObjectExpression(node)) return evaluateObject(node, context);
@@ -774,27 +776,39 @@ const evaluateTemplate = (
return primitiveSet(candidates);
};
/**
* A branch that may produce the expression's value. A logical left operand
* carries the value its selection already evaluated, so value consumers reuse
* it instead of re-evaluating that operand; re-evaluating it at every level of
* a left-nested `a || b || c` chain doubles the work per operand.
*/
interface SemanticSelectedBranch {
readonly node: t.Node;
readonly value?: JavaScriptSemanticValue;
}
/** Value and provenance selection consume the same established branch facts. */
const semanticSelectedBranches = (
node: t.ConditionalExpression | t.LogicalExpression,
context: EvaluationContext,
): readonly t.Node[] => {
): readonly SemanticSelectedBranch[] => {
if (t.isConditionalExpression(node)) {
const test = evaluateExpression(node.test, nestedContext(context));
const choice = semanticCondition(test, Boolean);
return choice === undefined
? [node.consequent, node.alternate]
: [choice ? node.consequent : node.alternate];
? [{ node: node.consequent }, { node: node.alternate }]
: [{ node: choice ? node.consequent : node.alternate }];
}
const left = evaluateExpression(node.left, nestedContext(context));
const value = evaluateExpression(node.left, nestedContext(context));
const left = { node: node.left, value };
const choice = logicalRightOperand(
node.operator,
semanticCondition(left, Boolean),
semanticCondition(left, (value) => value === null, false),
semanticCondition(value, Boolean),
semanticCondition(value, (candidate) => candidate === null, false),
);
return choice === undefined
? [node.left, node.right]
: [choice ? node.right : node.left];
? [left, { node: node.right }]
: [choice ? { node: node.right } : left];
};
/** All retained alternatives must agree before discarding a branch. */
@@ -1320,7 +1334,7 @@ const provenanceForExpression = (
}
if (t.isConditionalExpression(node) || t.isLogicalExpression(node)) {
const candidates = semanticSelectedBranches(node, context).map((branch) =>
provenanceForExpression(branch, nestedContext(context)),
provenanceForExpression(branch.node, nestedContext(context)),
);
const origins = uniqueSemanticOrigins(
candidates.flatMap((candidate) => candidate.origins),