fix(javascript): invalidate stale facts across direct eval

Index unshadowed direct-eval sites once at the semantic owner. Distinguish
lexical rebinding, non-strict eval-introduced names, and possible mutation
of reachable objects. Reuse existing effect timing and capture proofs;
retain original definitions and module origins without inventing assignments
or parsing or executing eval payloads. Query object uncertainty conservatively
across the owning activation so aliases remain covered when names are shadowed
or leave block scope. Retain no binding-by-eval table.

Use the shared facts for values, read provenance, reference effects and child
argument shapes. Preserve indirect/optional/shadowed eval controls, unrelated
activations, immutable shapes, strict-mode bindings and prior primitive
captures. Remove obsolete reference plumbing and the source-wide eval blocker.

Validation: 306 focused checks across 16 files pass, including compiled CLI
and stdio MCP return projections, constructor uncertainty and child-process
consumers. Against the preceding SQLite commit, 9 of 23 selected checks fail
and 14 controls pass. Static/docs/test discovery (1559 files) pass. Two source
reviews found no remaining scoped blocker after alias and sloppy-name fixes.
No analyzed JavaScript, SQLite engine, heavy test or profiler was executed.
Conservative object reachability can intentionally lose exactness; large-case
opaque-query CPU and memory remain unmeasured.

Fixes #4875
This commit is contained in:
morluto committed 2026-10-18 05:08:37 +08:00
1 parent fc847ffe35
commit 9ac76d3d94
13 files changed
+446 -46

No files matched your search

@@ -1,3 +1,8 @@
import {
semanticBindingMayHaveOpaqueEffect,
semanticBindingCanBeRebound,
semanticOpaqueBindingReadMayHaveChanged,
} from "./javascriptSemanticOpaqueEffects.js";
import * as t from "@babel/types";
import {
@@ -196,14 +201,20 @@ export const evaluateSemanticBinding = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticValue =>
evaluateBinding(binding, {
state,
bindings: new Map(),
expressionDepth: 0,
primitiveBindingValues: primitiveBindingValuesFor(state),
dependence: { lowest: CALLER_INDEPENDENT },
memo: createEvaluationMemo(state),
});
semanticBindingCanBeRebound(binding) &&
semanticBindingMayHaveOpaqueEffect(binding, state)
? {
status: "unknown",
reason: "Direct eval may have changed this lexical binding.",
}
: evaluateBinding(binding, {
state,
bindings: new Map(),
expressionDepth: 0,
primitiveBindingValues: primitiveBindingValuesFor(state),
dependence: { lowest: CALLER_INDEPENDENT },
memo: createEvaluationMemo(state),
});
/** Evaluate an inert expression, optionally at an earlier reference capture. */
export const evaluateSemanticExpression = (
@@ -245,8 +256,8 @@ export const evaluateSemanticExpressionProvenance = (
export const evaluateSemanticProvenance = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): JavaScriptBindingProvenance =>
provenanceForBinding(binding, {
): JavaScriptBindingProvenance => {
const provenance = provenanceForBinding(binding, {
state,
bindings: new Map(),
expressionDepth: 0,
@@ -254,6 +265,14 @@ export const evaluateSemanticProvenance = (
dependence: { lowest: CALLER_INDEPENDENT },
memo: createEvaluationMemo(state),
});
return semanticBindingCanBeRebound(binding) &&
semanticBindingMayHaveOpaqueEffect(binding, state)
? semanticAmbiguousProvenance(
provenance.origins,
"Direct eval may have changed this runtime value.",
)
: provenance;
};
/** Follow a namespace binding as it was read at a specific source position. */
export const evaluateSemanticProvenanceAtRead = (
@@ -509,6 +528,19 @@ const evaluateBinding = (
continue;
projected = invalidateSemanticEscapedPath(projected, escape.path);
}
if (
(projected.status === "object" || projected.status === "array") &&
semanticBindingMayHaveOpaqueEffect(
binding,
context.state,
context.capturePoint,
"reference",
)
)
projected = {
status: "unknown",
reason: "Direct eval may have mutated this object reference.",
};
if (
callerIndependent &&
independent !== undefined &&
@@ -606,6 +638,17 @@ const evaluateExpressionValue = (
};
}
const atRead = read?.binding ?? binding;
if (
semanticOpaqueBindingReadMayHaveChanged(
binding,
context.capturePoint ?? node,
context.state,
)
)
return {
status: "unknown",
reason: "Direct eval may have changed this lexical binding.",
};
return evaluateBinding(atRead, nestedContext(context), atRead === binding);
}
if (t.isTemplateLiteral(node)) return evaluateTemplate(node, context);
@@ -1150,6 +1193,11 @@ const provenanceForBindingAtRead = (
read: t.Identifier,
context: EvaluationContext,
): JavaScriptBindingProvenance => {
if (semanticOpaqueBindingReadMayHaveChanged(binding, read, context.state))
return semanticAmbiguousProvenance(
provenanceForBinding(binding, context).origins,
"Direct eval may have changed this runtime value.",
);
const { binding: snapshot } = semanticBindingAtRead(
binding,
read,