fix(javascript): bound semantic expansion and resolve lexical receivers

This commit is contained in:
morluto
2026-10-09 00:20:48 +08:00
parent eaf5734083
commit 16f4609d68
26 changed files with 868 additions and 78 deletions
@@ -5,6 +5,7 @@ import { invalidateSemanticMutationPath } from "./javascriptSemanticMutationValu
import type {
JavaScriptBindingProvenance,
JavaScriptSemanticProperty,
JavaScriptSemanticResourceLimit,
JavaScriptSemanticValue,
} from "./javascriptSemanticIr.js";
import {
@@ -23,36 +24,57 @@ import {
uniqueSemanticOrigins,
} from "./javascriptSemanticProvenance.js";
import {
MAX_SEMANTIC_PRIMITIVE_CANDIDATES,
semanticPrimitiveCandidates as primitiveCandidates,
semanticPrimitiveSet as primitiveSet,
semanticPrimitiveValue as primitiveValue,
} from "./javascriptSemanticPrimitives.js";
import {
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
semanticResourceLimitUnknown,
semanticResourceLimitReason,
} from "./javascriptSemanticResourceLimits.js";
interface EvaluationContext {
readonly state: JavaScriptSemanticAnalysisState;
readonly bindings: ReadonlySet<string>;
readonly expressionDepth: number;
}
// Cap expression recursion independently from graph nodes and primitive unions.
const isSemanticResourceLimit = (
value: JavaScriptSemanticValue,
): value is Extract<
JavaScriptSemanticValue,
{ readonly status: "unknown" | "ambiguous" | "cycle" }
> & { readonly resourceLimit: JavaScriptSemanticResourceLimit } =>
value.status === "unknown" && value.resourceLimit !== undefined;
/** Evaluate one binding in the bounded constant-value lattice. */
export const evaluateSemanticBinding = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticValue =>
evaluateBinding(binding, { state, bindings: new Set() });
evaluateBinding(binding, { state, bindings: new Set(), expressionDepth: 0 });
/** Evaluate one arbitrary inert expression in the established lexical state. */
export const evaluateSemanticExpression = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticValue =>
evaluateExpression(node, { state, bindings: new Set() });
evaluateExpression(node, { state, bindings: new Set(), expressionDepth: 0 });
/** Follow module provenance through destructuring, members, and aliases. */
export const evaluateSemanticProvenance = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): JavaScriptBindingProvenance =>
provenanceForBinding(binding, { state, bindings: new Set() });
provenanceForBinding(binding, {
state,
bindings: new Set(),
expressionDepth: 0,
});
const evaluateBinding = (
binding: JavaScriptSemanticBindingState,
@@ -85,6 +107,8 @@ const evaluateExpression = (
node: t.Node,
context: EvaluationContext,
): JavaScriptSemanticValue => {
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT)
return semanticResourceLimitUnknown("expression-depth");
const literal = primitiveValue(node);
if (literal.found) return { status: "literal", value: literal.value };
if (t.isIdentifier(node)) {
@@ -128,18 +152,23 @@ const evaluateTemplate = (
let candidates = [""];
for (let index = 0; index < node.quasis.length; index += 1) {
const quasi = node.quasis[index];
// Semantic template evaluation retains its established raw fallback for
// synthetic AST inputs; exact atom readers remain cooked-only.
const text = quasi?.value.cooked ?? quasi?.value.raw ?? "";
candidates = candidates.map((prefix) => `${prefix}${text}`);
const expression = node.expressions[index];
if (expression === undefined) continue;
const values = primitiveCandidates(
evaluateExpression(expression, nestedContext(context)),
);
if (values === null)
const evaluated = evaluateExpression(expression, nestedContext(context));
const values = primitiveCandidates(evaluated);
if (values === null) {
if (isSemanticResourceLimit(evaluated)) return evaluated;
return {
status: "unknown",
reason: "Template expression is not a bounded primitive.",
};
}
if (candidates.length > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT / values.length)
return semanticResourceLimitUnknown("primitive-candidates");
candidates = candidates.flatMap((prefix) =>
values.map((value) => `${prefix}${String(value)}`),
);
@@ -311,8 +340,13 @@ const addPrimitiveValues = (
): JavaScriptSemanticValue => {
const left = primitiveCandidates(leftValue);
const right = primitiveCandidates(rightValue);
if (left === null || right === null)
if (left === null || right === null) {
if (isSemanticResourceLimit(leftValue)) return leftValue;
if (isSemanticResourceLimit(rightValue)) return rightValue;
return { status: "unknown", reason: "Non-primitive addition." };
}
if (left.length > MAX_SEMANTIC_PRIMITIVE_CANDIDATES / right.length)
return semanticResourceLimitUnknown("primitive-candidates");
const values = left.flatMap((leftValue) =>
right.map((rightValue) =>
typeof leftValue === "string" || typeof rightValue === "string"
@@ -327,11 +361,12 @@ const evaluateUnary = (
node: t.UnaryExpression,
context: EvaluationContext,
): JavaScriptSemanticValue => {
const argument = primitiveCandidates(
evaluateExpression(node.argument, nestedContext(context)),
);
if (argument === null)
const evaluated = evaluateExpression(node.argument, nestedContext(context));
const argument = primitiveCandidates(evaluated);
if (argument === null) {
if (isSemanticResourceLimit(evaluated)) return evaluated;
return { status: "unknown", reason: "Non-primitive unary operand." };
}
if (node.operator === "!")
return primitiveSet(argument.map((value) => !value));
if (node.operator === "+")
@@ -436,6 +471,11 @@ const provenanceForExpression = (
node: t.Node,
context: EvaluationContext,
): JavaScriptBindingProvenance => {
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT)
return semanticUnresolvedProvenance(
"unknown",
semanticResourceLimitReason("expression-depth"),
);
const required = semanticRequireOrigin(node, context.state);
if (required !== undefined) return semanticOriginsProvenance([required]);
if (t.isIdentifier(node)) {
@@ -506,6 +546,8 @@ const provenanceForExpression = (
const mergeValues = (
values: readonly JavaScriptSemanticValue[],
): JavaScriptSemanticValue => {
const resourceLimit = values.find(isSemanticResourceLimit);
if (resourceLimit !== undefined) return resourceLimit;
const primitives = values.flatMap(
(value) => primitiveCandidates(value) ?? [],
);
@@ -521,6 +563,7 @@ const nestedContext = (
bindingId?: string,
): EvaluationContext => ({
state: context.state,
expressionDepth: context.expressionDepth + 1,
bindings:
bindingId === undefined
? context.bindings